flex: fix Qwen3 MoE smoke regressions (dtype / MoE MLP / peft patching)
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. ...").
This commit is contained in:
parent
f0115f8d70
commit
a1aec618ce
5 changed files with 245 additions and 13 deletions
156
tests/flex_moe_smoke.py
Normal file
156
tests/flex_moe_smoke.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# 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()
|
||||
|
|
@ -765,14 +765,32 @@ class FlexEngine:
|
|||
|
||||
# Materialise the pristine source the first time we see a LoRA.
|
||||
if self._pristine_base is None:
|
||||
# The inference model has already been flex-patched; its
|
||||
# linear weights (what LoRA merges into) are still pristine,
|
||||
# so we can clone it and just not call flex attention on the
|
||||
# pristine copy.
|
||||
with weight_pool(self._cumem_allocator):
|
||||
self._pristine_base = copy.deepcopy(self._inference_model)
|
||||
self._pristine_base.eval()
|
||||
self._impl.base_model = self._pristine_base
|
||||
if self.arch == "qwen3_moe":
|
||||
# For Qwen3 MoE, LoRA lives on the stacked-expert
|
||||
# ParamWrapper and never merges in-place into the expert
|
||||
# tensors during training (see
|
||||
# unsloth_zoo.temporary_patches.moe_utils
|
||||
# _patched_param_wrapper_forward). That means the
|
||||
# training model's expert weights ARE the pristine
|
||||
# source — no third 30-60 GB deep-copy is needed. Point
|
||||
# the LoRA-refresh helper at the training model's base
|
||||
# directly. This keeps the 30B MoE model at 2x residency
|
||||
# instead of 3x.
|
||||
try:
|
||||
pristine = training_peft_model.get_base_model()
|
||||
except AttributeError:
|
||||
pristine = training_peft_model
|
||||
self._pristine_base = pristine
|
||||
self._impl.base_model = pristine
|
||||
else:
|
||||
# Dense path: the inference model has already been
|
||||
# flex-patched; its linear weights (what LoRA merges
|
||||
# into) are still pristine, so we clone it and just do
|
||||
# not call flex attention on the pristine copy.
|
||||
with weight_pool(self._cumem_allocator):
|
||||
self._pristine_base = copy.deepcopy(self._inference_model)
|
||||
self._pristine_base.eval()
|
||||
self._impl.base_model = self._pristine_base
|
||||
|
||||
if self._inference_peft is None:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -85,25 +85,30 @@ def call_moe_model_with_flex_kwargs(model, input_ids, position_ids, flex_kwargs)
|
|||
_sin = _sin[position_ids]
|
||||
position_embeddings = (_cos, _sin)
|
||||
hidden_states = inputs_embeds
|
||||
# RMSNorm + bnb-4bit Linear compute can promote activations to fp32
|
||||
# along the Qwen3 MoE path even under autocast. Lock activations to
|
||||
# the embed dtype so paged-KV writes (which index_put_ into a
|
||||
# pre-allocated bf16 cache) see a matching dtype.
|
||||
compute_dtype = inputs_embeds.dtype
|
||||
for layer in base.layers:
|
||||
# Attention block — identical to dense Qwen3 / Llama.
|
||||
residual = hidden_states
|
||||
hidden_states = layer.input_layernorm(hidden_states)
|
||||
hidden_states = layer.input_layernorm(hidden_states).to(compute_dtype)
|
||||
hidden_states, _ = layer.self_attn(
|
||||
hidden_states,
|
||||
position_embeddings = position_embeddings,
|
||||
**flex_kwargs,
|
||||
)
|
||||
hidden_states = residual + hidden_states
|
||||
hidden_states = residual + hidden_states.to(compute_dtype)
|
||||
# MoE MLP.
|
||||
residual = hidden_states
|
||||
hidden_states = layer.post_attention_layernorm(hidden_states)
|
||||
hidden_states = layer.post_attention_layernorm(hidden_states).to(compute_dtype)
|
||||
mlp_out = layer.mlp(hidden_states)
|
||||
if isinstance(mlp_out, tuple):
|
||||
hidden_states = mlp_out[0]
|
||||
else:
|
||||
hidden_states = mlp_out
|
||||
hidden_states = residual + hidden_states
|
||||
hidden_states = residual + hidden_states.to(compute_dtype)
|
||||
hidden_states = base.norm(hidden_states)
|
||||
return hidden_states
|
||||
|
||||
|
|
@ -136,6 +141,41 @@ class FlexMoEInference:
|
|||
peft_model = None,
|
||||
cumem_allocator = None,
|
||||
):
|
||||
# FastQwen3MoeModel.pre_patch (unsloth/models/qwen3_moe.py) installs
|
||||
# a legacy Qwen3MoeSparseMoeBlock_fast_forward that expects
|
||||
# ``self.gate_proj``; transformers 5.x Qwen3MoE uses
|
||||
# ``self.gate`` / ``self.experts`` instead, so that forward is dead
|
||||
# code on this env. Unsloth-zoo's ``patch_qwen3_moe`` re-patches it
|
||||
# to the correct ``sparse_moe_block_forward``, but Unsloth's
|
||||
# pre_patch can run later and silently clobber it (patch_function
|
||||
# bails via can_safely_patch on a second pass). Force-restore the
|
||||
# stock HF forward here so the flex walker sees a working MLP.
|
||||
try:
|
||||
import transformers.models.qwen3_moe.modeling_qwen3_moe as _hf_mod
|
||||
_BlockCls = _hf_mod.Qwen3MoeSparseMoeBlock
|
||||
cur_forward = getattr(_BlockCls, "forward", None)
|
||||
cur_name = getattr(cur_forward, "__name__", "")
|
||||
if "fast_forward" in cur_name or cur_name == "Qwen3MoeSparseMoeBlock_fast_forward":
|
||||
# Prefer unsloth_zoo's patched version if present;
|
||||
# fall back to the stock HF forward otherwise.
|
||||
unique = getattr(_BlockCls, "_original_forward_Qwen3MoeSparseMoeBlock", None) or getattr(_BlockCls, "_Qwen3MoeSparseMoeBlock_original_forward", None)
|
||||
if unique is not None:
|
||||
_BlockCls.forward = unique
|
||||
else:
|
||||
# Re-run unsloth_zoo patch to install sparse_moe_block_forward.
|
||||
from unsloth_zoo.temporary_patches.qwen3_moe import patch_qwen3_moe
|
||||
patch_qwen3_moe()
|
||||
# If patch_function still skipped due to can_safely_patch,
|
||||
# fall back to stock HF as a last resort.
|
||||
cur_forward_after = getattr(_BlockCls, "forward", None)
|
||||
cur_name_after = getattr(cur_forward_after, "__name__", "")
|
||||
if "fast_forward" in cur_name_after:
|
||||
# Lazy-load pristine forward by reloading the module.
|
||||
import importlib
|
||||
_fresh_mod = importlib.reload(_hf_mod)
|
||||
_BlockCls.forward = _fresh_mod.Qwen3MoeSparseMoeBlock.forward
|
||||
except Exception:
|
||||
pass
|
||||
assert max_seq_length % page_size == 0
|
||||
# Startup sanity checks. If any of these fail the architecture
|
||||
# isn't a Qwen3-MoE variant we know how to drive.
|
||||
|
|
|
|||
|
|
@ -161,7 +161,15 @@ def make_flex_attention_forward(page_table: PageTable):
|
|||
# Write to paged KV cache. For prefill, assign_prefill_no_paging
|
||||
# writes into [1, H, MAX_S, D]; for decode, assign() writes into the
|
||||
# B decode slots.
|
||||
# Match the pre-allocated KV cache dtype; bnb-4bit Linear compute
|
||||
# can produce fp32 k/v even under autocast, and the paged-cache
|
||||
# index_put_ refuses mixed dtypes.
|
||||
if self._paged_cache is not None and flex_input_pos is not None:
|
||||
cache_dtype = self._paged_cache.k_cache.dtype
|
||||
if k.dtype != cache_dtype:
|
||||
k = k.to(cache_dtype)
|
||||
if v.dtype != cache_dtype:
|
||||
v = v.to(cache_dtype)
|
||||
k, v = self._paged_cache.update(flex_input_pos, k, v, flex_batch_idx)
|
||||
|
||||
# Flex attention. The block mask routes each query to the correct
|
||||
|
|
|
|||
|
|
@ -3408,7 +3408,7 @@ class FastLlamaModel:
|
|||
apply_lora_mlp = apply_lora_mlp_swiglu
|
||||
elif model_type == "falcon_h1":
|
||||
apply_lora_mlp = apply_lora_mlp_swiglu
|
||||
elif model_type == "qwen3moe":
|
||||
elif model_type == "qwen3_moe":
|
||||
apply_lora_mlp = apply_lora_mlp_swiglu
|
||||
else:
|
||||
raise NotImplementedError(f"Unsloth: {model_type} is not yet implemented!")
|
||||
|
|
@ -3482,6 +3482,16 @@ class FastLlamaModel:
|
|||
|
||||
# MLP patching
|
||||
mlp_module = layer.mlp
|
||||
# Qwen3 MoE uses Qwen3MoeSparseMoeBlock which holds
|
||||
# stacked expert tensors on .experts; the dense
|
||||
# gate/up/down fusion does not apply. MoE LoRA is
|
||||
# wired through unsloth_zoo/moe_utils instead.
|
||||
if not (
|
||||
hasattr(mlp_module, "gate_proj")
|
||||
and hasattr(mlp_module, "up_proj")
|
||||
and hasattr(mlp_module, "down_proj")
|
||||
):
|
||||
continue
|
||||
gate_proj = mlp_module.gate_proj
|
||||
up_proj = mlp_module.up_proj
|
||||
down_proj = mlp_module.down_proj
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue