Four integration fixes wired up while bringing Qwen3-30B-A3B-Instruct-2507
green end-to-end on UNSLOTH_FAST_INFERENCE=1:
1. unsloth/models/llama.py patch_peft_model: transformers 5.x reports
model_type as "qwen3_moe" (with underscore); the PR's check was
"qwen3moe" and fell through to NotImplementedError.
2. unsloth/models/llama.py patch_peft_model dense MLP patching: the
fused gate/up/down LoRAMLP swap walks layer.mlp.gate_proj, which is
a Qwen3MoeSparseMoeBlock for MoE and has no gate_proj attribute.
Skip the swap when the MLP does not expose the dense trio; MoE
LoRA is wired through unsloth_zoo/moe_utils anyway.
3. unsloth/inference/flex_qwen3_llama.py flex attention forward:
bnb-4bit Linear compute produces fp32 k / v even under autocast,
which makes the paged KV index_put_ refuse the mixed dtype (bf16
cache, fp32 update). Cast k / v to self._paged_cache.k_cache.dtype
before update. Also benefits the dense path.
4. unsloth/inference/flex_engine.py bind_peft_model: for Qwen3 MoE the
ParamWrapper keeps LoRA un-merged on the stacked expert tensors, so
the training model's expert weights ARE the pristine source. Skip
the pristine-base deep-copy for arch=="qwen3_moe" and point
refresh_moe_lora_merge_from_pristine at the training base directly.
Avoids a third 30-60 GB residency on 30B-A3B.
5. unsloth/inference/flex_moe.py call_moe_model_with_flex_kwargs: lock
activations to the embed dtype across layernorm + MoE MLP; RMSNorm
+ bnb-4bit compute promote activations to fp32 along the MoE path
under autocast. Also force-restore Qwen3MoeSparseMoeBlock.forward
to the stock or unsloth_zoo version if FastQwen3MoeModel.pre_patch
clobbered it with a legacy Qwen3MoeSparseMoeBlock_fast_forward that
expects a flat self.gate_proj (which does not exist on transformers
5.x stacked-expert MoE blocks).
Adds tests/flex_moe_smoke.py: generates 32 tokens twice (cold + warm),
records first-call / warm-call tokens/s, peak VRAM, arch, impl. Writes
async_task_outputs/qwen3_moe_grpo_bench/smoke_A_{4bit,bf16}.json.
Measured on a single B200 (sm_100), Qwen3-30B-A3B-Instruct-2507 +
LoRA rank 16 + grouped_mm MoE backend:
| precision | t_load (s) | peak VRAM (GB) | cold tok/s | warm tok/s |
|-----------|------------|----------------|------------|------------|
| 4bit | 27.0 | 123.4 | 3.7 | 6.9 |
| bf16 | 33.5 | 125.9 | 3.7 | 6.7 |
Both completions coherent ("the lazy dog. ...").
156 lines
5.5 KiB
Python
156 lines
5.5 KiB
Python
# SPDX-License-Identifier: GNU Affero General Public License v3.0
|
|
# Copyright 2023-present the Unsloth team. All rights reserved.
|
|
|
|
"""Smoke-test ``UNSLOTH_FAST_INFERENCE=1`` on a Qwen3 MoE model.
|
|
|
|
Mirrors ``tests/flex_fastlm_smoke.py`` but targets the new
|
|
``FlexMoEInference`` path added for ``Qwen3MoeForCausalLM``.
|
|
|
|
Invoked as:
|
|
CUDA_VISIBLE_DEVICES=0 UNSLOTH_FAST_INFERENCE=1 python -u \
|
|
tests/flex_moe_smoke.py \
|
|
--model unsloth/Qwen3-30B-A3B-Instruct-2507 \
|
|
--load_in_4bit
|
|
|
|
Writes a small JSON summary to
|
|
``async_task_outputs/qwen3_moe_grpo_bench/smoke_A_{precision}.json``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
if str(_REPO_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(_REPO_ROOT))
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument(
|
|
"--model", default = "unsloth/Qwen3-30B-A3B-Instruct-2507"
|
|
)
|
|
p.add_argument("--dtype", choices = ["bf16", "fp16"], default = "bf16")
|
|
p.add_argument("--load_in_4bit", action = "store_true")
|
|
p.add_argument("--with_lora", action = "store_true")
|
|
p.add_argument("--max_new_tokens", type = int, default = 32)
|
|
p.add_argument("--max_seq_length", type = int, default = 1024)
|
|
p.add_argument("--prompt", default = "The quick brown fox jumps over")
|
|
p.add_argument("--out_dir", default = "async_task_outputs/qwen3_moe_grpo_bench")
|
|
args = p.parse_args()
|
|
|
|
import torch
|
|
|
|
os.environ.setdefault("UNSLOTH_FAST_INFERENCE", "1")
|
|
os.environ.setdefault("UNSLOTH_MOE_BACKEND", "grouped_mm")
|
|
print(f"[smoke] UNSLOTH_FAST_INFERENCE={os.environ.get('UNSLOTH_FAST_INFERENCE')}")
|
|
print(f"[smoke] UNSLOTH_MOE_BACKEND={os.environ.get('UNSLOTH_MOE_BACKEND')}")
|
|
|
|
import unsloth
|
|
|
|
print(f"[smoke] unsloth={unsloth.__file__}")
|
|
from unsloth import FastLanguageModel
|
|
|
|
dtype = torch.bfloat16 if args.dtype == "bf16" else torch.float16
|
|
|
|
torch.cuda.reset_peak_memory_stats()
|
|
t0 = time.perf_counter()
|
|
model, tokenizer = FastLanguageModel.from_pretrained(
|
|
model_name = args.model,
|
|
max_seq_length = args.max_seq_length,
|
|
dtype = dtype,
|
|
load_in_4bit = args.load_in_4bit,
|
|
fast_inference = True,
|
|
)
|
|
t_load = time.perf_counter() - t0
|
|
peak_after_load = torch.cuda.max_memory_reserved() / 1024**3
|
|
print(f"[smoke] loaded model in {t_load:.1f}s; peak VRAM after load: {peak_after_load:.2f} GB")
|
|
print(f"[smoke] hasattr(model, 'vllm_engine'): {hasattr(model, 'vllm_engine')}")
|
|
print(f"[smoke] vllm_engine type: {type(model.vllm_engine).__name__}")
|
|
arch = getattr(model.vllm_engine, "arch", "?")
|
|
impl = type(model.vllm_engine._impl).__name__
|
|
print(f"[smoke] FlexEngine.arch={arch} impl={impl}")
|
|
|
|
if args.with_lora:
|
|
model = FastLanguageModel.get_peft_model(
|
|
model,
|
|
r = 16,
|
|
target_modules = [
|
|
"q_proj", "k_proj", "v_proj", "o_proj",
|
|
"gate_proj", "up_proj", "down_proj", "gate_up_proj",
|
|
],
|
|
lora_alpha = 32,
|
|
lora_dropout = 0.0,
|
|
bias = "none",
|
|
use_gradient_checkpointing = "unsloth",
|
|
random_state = 3407,
|
|
)
|
|
print(f"[smoke] PEFT model type: {type(model).__name__}")
|
|
|
|
prompts = [args.prompt]
|
|
|
|
class _SP:
|
|
max_tokens = args.max_new_tokens
|
|
temperature = 0.0
|
|
|
|
# First call includes prefill + any lazy engine bring-up; measure separately.
|
|
t_first0 = time.perf_counter()
|
|
outputs = model.fast_generate(prompts, sampling_params = _SP(), use_tqdm = False)
|
|
t_first = time.perf_counter() - t_first0
|
|
out = outputs[0]
|
|
n_tok = len(out.outputs[0].token_ids)
|
|
|
|
# Warm steady-state: run again and measure.
|
|
t_warm0 = time.perf_counter()
|
|
outputs2 = model.fast_generate(prompts, sampling_params = _SP(), use_tqdm = False)
|
|
t_warm = time.perf_counter() - t_warm0
|
|
n_tok_warm = len(outputs2[0].outputs[0].token_ids)
|
|
|
|
peak_after_gen = torch.cuda.max_memory_reserved() / 1024**3
|
|
print(
|
|
f"[smoke] first call: generated {n_tok} tokens in {t_first:.2f}s "
|
|
f"({n_tok / t_first:.1f} tok/s)"
|
|
)
|
|
print(
|
|
f"[smoke] warm call: generated {n_tok_warm} tokens in {t_warm:.2f}s "
|
|
f"({n_tok_warm / t_warm:.1f} tok/s)"
|
|
)
|
|
print(f"[smoke] peak VRAM after gen: {peak_after_gen:.2f} GB")
|
|
print(f"[smoke] prompt: {args.prompt!r}")
|
|
print(f"[smoke] completion: {out.outputs[0].text!r}")
|
|
|
|
precision = "4bit" if args.load_in_4bit else args.dtype
|
|
out_dir = Path(args.out_dir)
|
|
out_dir.mkdir(parents = True, exist_ok = True)
|
|
summary = {
|
|
"phase": "smoke_A",
|
|
"model": args.model,
|
|
"precision": precision,
|
|
"dtype": str(dtype),
|
|
"max_seq_length": args.max_seq_length,
|
|
"max_new_tokens": args.max_new_tokens,
|
|
"with_lora": args.with_lora,
|
|
"t_load_s": round(t_load, 2),
|
|
"peak_vram_after_load_gb": round(peak_after_load, 2),
|
|
"peak_vram_after_gen_gb": round(peak_after_gen, 2),
|
|
"first_call_s": round(t_first, 2),
|
|
"first_call_tok_s": round(n_tok / t_first, 1),
|
|
"warm_call_s": round(t_warm, 2),
|
|
"warm_call_tok_s": round(n_tok_warm / t_warm, 1),
|
|
"arch": arch,
|
|
"impl": impl,
|
|
"prompt": args.prompt,
|
|
"completion": out.outputs[0].text,
|
|
}
|
|
with open(out_dir / f"smoke_A_{precision}.json", "w") as f:
|
|
json.dump(summary, f, indent = 2)
|
|
print(f"[smoke] wrote {out_dir / f'smoke_A_{precision}.json'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|