Extends flex fast-inference to `unsloth/gemma-4-26B-A4B-it` (30 layers, 128 experts top-k 8, H=2816, ~3.8B active of 25.2B). Mirrors the FlexGptOssInference / FlexMoEInference template with Gemma 4-specific wiring: - Dual dense MLP + MoE per decoder layer (Gemma4TextMLP alongside Gemma4TextExperts; outputs summed before the residual add, then multiplied by the per-layer `layer_scalar` buffer). - Per-layer sliding-window dispatch (25 sliding @ 1024 tokens, 5 full attention) via twin BlockMask built once per generate() entry. - Two-tier RoPE: sliding layers use rope_theta=10K with full head_dim rotation; full-attn layers use rope_type=proportional with theta=1M and partial_rotary_factor=0.25 (the inv_freq's zero-padded tail makes the generic rotate_half a no-op on the unrotated dims, so a single rotary helper covers both). - Per-head Q/K/V RMSNorm applied before RoPE / KV write. - attention_k_eq_v=True on full-attn layers (v_proj is None): value is the raw k_proj output, followed only by v_norm (with_scale=False). - Rebind Gemma4TextExperts.forward to forward_native_grouped_mm so decode uses the grouped_mm backend (the slow Python loop in the stock forward is neither fast nor CUDA-graph-capturable). The routing weights already include per_expert_scale via Gemma4TextRouter.forward, so no extra folding is needed. - CUDA graph capture and UNSLOTH_FLEX_COMPILE_WALKER=1 inherit from the MoE template (single bucket ladder, single pool across buckets). Arch detection: - `_detect_arch` distinguishes dense vs MoE Gemma 4 via `text_config.num_experts > 1` (same class name covers both variants). - `bind_peft_model` extends the MoE no-deepcopy shortcut to gemma4_moe. Validation (B200, bf16, 3 chat prompts, 64 tokens): - Dense 31B sanity check: coherent completion via existing FlexGemma4Inference. - Flex (cudagraph) vs HF naive: 24/24, 2/2, 4/4 tokens bitwise match. - Merge parity: 7/7 cases bitwise (rank 16 / 64, 1 + 2 adapters, bf16 + fp32, E=128, 2I=1408, H=2816 / H, I=704). - Throughput bs=8/16/32/48: 510 / 1030 / 1564 / 916 tok/s vs HF naive 134 tok/s at bs=8 (3.8x-11.7x). - GRPO smoke: DAPO-Math-17k, seed 3407, max_steps=5 — stable (see follow-up comment on PR). Out of scope: E2B/E4B KV-shared + per-layer-input variants (guarded with NotImplementedError); bnb-4bit stacked experts (no such class ships for Gemma 4 today).
191 lines
6.2 KiB
Python
191 lines
6.2 KiB
Python
# SPDX-License-Identifier: GNU Affero General Public License v3.0
|
|
# Copyright 2023-present the Unsloth team. All rights reserved.
|
|
|
|
"""Token parity for FlexGemma4Inference / FlexGemma4MoEInference vs pure HF.
|
|
|
|
Usage::
|
|
CUDA_VISIBLE_DEVICES=2 UNSLOTH_FAST_INFERENCE=1 python -u \\
|
|
tests/flex_gemma4_parity.py --backend flex --model unsloth/gemma-4-31B-it
|
|
|
|
CUDA_VISIBLE_DEVICES=3 python -u tests/flex_gemma4_parity.py \\
|
|
--backend hf --model unsloth/gemma-4-31B-it
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
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))
|
|
|
|
|
|
CHAT_PROMPTS = [
|
|
"In one sentence, what is Paris?",
|
|
"What is 23 + 19? Answer in one word.",
|
|
"Continue: The quick brown fox jumps over",
|
|
]
|
|
|
|
|
|
def _run_flex(args, dtype, *, capture: bool, lora_path=None):
|
|
import torch
|
|
os.environ["UNSLOTH_FAST_INFERENCE"] = "1"
|
|
import unsloth # noqa
|
|
from unsloth import FastLanguageModel
|
|
|
|
if not capture:
|
|
# Force the eager decode path across arches.
|
|
try:
|
|
from unsloth.inference.flex_gemma4 import FlexGemma4Inference
|
|
FlexGemma4Inference.capture_decode_cudagraph = lambda self: None
|
|
except Exception:
|
|
pass
|
|
try:
|
|
from unsloth.inference.flex_gemma4_moe import FlexGemma4MoEInference
|
|
FlexGemma4MoEInference.capture_decode_cudagraph = lambda self: None
|
|
except Exception:
|
|
pass
|
|
|
|
model, tok = FastLanguageModel.from_pretrained(
|
|
model_name=args.model,
|
|
max_seq_length=args.max_seq_length,
|
|
dtype=dtype,
|
|
load_in_4bit=False,
|
|
fast_inference=True,
|
|
max_batch_size=4,
|
|
gpu_memory_utilization=0.6,
|
|
)
|
|
|
|
if lora_path is not None:
|
|
model.load_adapter(lora_path, adapter_name="default")
|
|
print(f"[parity-flex] LoRA attached from {lora_path}")
|
|
|
|
prompts = [
|
|
tok.apply_chat_template(
|
|
[{"role": "user", "content": p}],
|
|
tokenize=False,
|
|
add_generation_prompt=True,
|
|
)
|
|
for p in CHAT_PROMPTS[: args.num_prompts]
|
|
]
|
|
|
|
class _SP:
|
|
max_tokens = args.max_new_tokens
|
|
temperature = 0.0
|
|
|
|
_ = model.fast_generate(prompts, sampling_params=_SP(), use_tqdm=False)
|
|
outs = model.fast_generate(prompts, sampling_params=_SP(), use_tqdm=False)
|
|
token_ids = [list(o.outputs[0].token_ids) for o in outs]
|
|
texts = [o.outputs[0].text for o in outs]
|
|
return token_ids, texts, tok
|
|
|
|
|
|
def _run_hf(args, dtype, *, lora_path=None):
|
|
import torch
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
|
|
tok = AutoTokenizer.from_pretrained(args.model)
|
|
|
|
try:
|
|
model = AutoModelForCausalLM.from_pretrained(
|
|
args.model, dtype=dtype, device_map="cuda",
|
|
attn_implementation="eager",
|
|
)
|
|
except Exception:
|
|
# Multimodal Gemma 4 (ConditionalGeneration) — load the top-level class.
|
|
from transformers import AutoModelForImageTextToText
|
|
model = AutoModelForImageTextToText.from_pretrained(
|
|
args.model, dtype=dtype, device_map="cuda",
|
|
attn_implementation="eager",
|
|
)
|
|
model.eval()
|
|
|
|
if lora_path is not None:
|
|
from peft import PeftModel
|
|
model = PeftModel.from_pretrained(model, lora_path)
|
|
model.eval()
|
|
print(f"[parity-hf] LoRA attached from {lora_path}")
|
|
|
|
if tok.pad_token_id is None:
|
|
tok.pad_token_id = tok.eos_token_id
|
|
tok.padding_side = "left"
|
|
|
|
prompts = [
|
|
tok.apply_chat_template(
|
|
[{"role": "user", "content": p}],
|
|
tokenize=False,
|
|
add_generation_prompt=True,
|
|
)
|
|
for p in CHAT_PROMPTS[: args.num_prompts]
|
|
]
|
|
inputs = tok(prompts, return_tensors="pt", padding=True).to("cuda")
|
|
out = model.generate(
|
|
**inputs,
|
|
max_new_tokens=args.max_new_tokens,
|
|
do_sample=False,
|
|
temperature=1.0,
|
|
pad_token_id=tok.pad_token_id,
|
|
)
|
|
prompt_len = inputs["input_ids"].shape[1]
|
|
eos = tok.eos_token_id
|
|
pad = tok.pad_token_id
|
|
token_ids = []
|
|
texts = []
|
|
for row in out:
|
|
ids = row[prompt_len:].tolist()
|
|
while ids and ids[-1] in (eos, pad):
|
|
ids.pop()
|
|
token_ids.append(ids)
|
|
texts.append(tok.decode(ids, skip_special_tokens=False))
|
|
return token_ids, texts, tok
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument("--model", default="unsloth/gemma-4-31B-it")
|
|
p.add_argument("--backend", choices=["flex", "flex_eager", "hf"], required=True)
|
|
p.add_argument("--max_new_tokens", type=int, default=64)
|
|
p.add_argument("--num_prompts", type=int, default=3)
|
|
p.add_argument("--lora_path", default=None)
|
|
p.add_argument("--max_seq_length", type=int, default=1024)
|
|
p.add_argument("--out_dir", default="async_task_outputs/gemma4_moe_bench")
|
|
args = p.parse_args()
|
|
|
|
import torch
|
|
dtype = torch.bfloat16
|
|
|
|
if args.backend == "flex":
|
|
token_ids, texts, _ = _run_flex(args, dtype, capture=True, lora_path=args.lora_path)
|
|
elif args.backend == "flex_eager":
|
|
token_ids, texts, _ = _run_flex(args, dtype, capture=False, lora_path=args.lora_path)
|
|
else:
|
|
token_ids, texts, _ = _run_hf(args, dtype, lora_path=args.lora_path)
|
|
|
|
out_dir = Path(args.out_dir)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
model_tag = args.model.replace("/", "_")
|
|
suffix = "_lora" if args.lora_path else ""
|
|
out_path = out_dir / f"parity_{model_tag}_{args.backend}{suffix}.json"
|
|
with open(out_path, "w") as f:
|
|
json.dump(
|
|
{
|
|
"backend": args.backend,
|
|
"model": args.model,
|
|
"prompts": CHAT_PROMPTS[: args.num_prompts],
|
|
"token_ids": token_ids,
|
|
"texts": texts,
|
|
},
|
|
f,
|
|
indent=2,
|
|
)
|
|
print(f"[parity-{args.backend}] wrote {out_path}")
|
|
for i, (pp, tt) in enumerate(zip(CHAT_PROMPTS[: args.num_prompts], texts)):
|
|
print(f"[parity-{args.backend}] prompt {i}: {pp!r}")
|
|
print(f"[parity-{args.backend}] completion {i}: {tt!r}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|