Skip to content

Commit da92170

Browse files
authored
Fix synthetic parity CI failures from transformers 5.13 upgrade (#390)
Five `tests/synthetic_parity_test.py` cases (`olmo3`, `deepseek_v3`, `deepseek_v2_2`, `longcat_flash`, `granitemoehybrid`) broke after the transformers 5.13 upgrade, each from a distinct HF-side behavior change. ### MLA head-count mismatch — `deepseek_v2`, `deepseek_v3`, `longcat_flash` HF's SDPA path now applies `repeat_kv` whenever `num_key_value_groups > 1`. MLA reconstructs full-head K/V from a latent, so the tiny configs' `num_key_value_heads=2` caused K/V to be duplicated (4→8 heads) against the 4-head query: `RuntimeError: The size of tensor a (4) must match the size of tensor b (8)`. - Set `num_key_value_heads == num_attention_heads` for the MLA entries in `tests/_test_configs.py` (no-op for the mobius MLA path, which ignores `num_key_value_heads`). ### granitemoehybrid — `tests/synthetic_parity_test.py` + `granitemoehybrid.py` - Legacy `layer_types` values `mamba`/`attention` are rejected by HF's validator; the test now emits `linear_attention`/`full_attention`, and the redundant `_HF_EXTRA_CONFIG` override forcing legacy names is removed. - HF renamed the routed-expert tensors; `preprocess_weights` now remaps them (identical layouts, names only), keeping legacy names for back-compat: ```python ".block_sparse_moe.experts.gate_up_proj" -> ".block_sparse_moe.input_linear.weight" ".block_sparse_moe.experts.down_proj" -> ".block_sparse_moe.output_linear.weight" ".block_sparse_moe.router.weight" -> ".block_sparse_moe.gate.weight" ``` Without this remap the ONNX expert weights stayed random (cosine 0.996). ### olmo3 Near-tie argmax with `cosine=0.9999`, `max_abs_diff=0.0146` — a benign QK-norm + sliding/full-attention FP-accumulation difference. Added an `olmo3: 0.02` atol override, consistent with existing entries like `gemma3_text`. --------- Signed-off-by: copilot Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 0253746 commit da92170

3 files changed

Lines changed: 49 additions & 14 deletions

File tree

src/mobius/models/granitemoehybrid.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -439,11 +439,14 @@ def preprocess_weights(
439439
440440
Handles:
441441
1. Weight tying (embed_tokens ↔ lm_head)
442-
2. MoE gate: block_sparse_moe.router.layer.weight → block_sparse_moe.gate.weight
443-
444-
Fused expert weights (``input_linear``, ``output_linear``) and shared-MLP
445-
weights pass through directly — the ONNX model stores them in the same
446-
fused layout as HuggingFace.
442+
2. MoE gate: block_sparse_moe.router[.layer].weight → block_sparse_moe.gate.weight
443+
3. Fused expert weights: HF renamed the routed-expert tensors from
444+
``block_sparse_moe.{input,output}_linear.weight`` to
445+
``block_sparse_moe.experts.{gate_up,down}_proj`` (transformers >=5.x).
446+
The layouts are identical, so only the names are remapped.
447+
448+
Shared-MLP weights (``shared_mlp.{input,output}_linear``) pass through
449+
directly — the ONNX model stores them in the same fused layout as HF.
447450
"""
448451
if self.config.tie_word_embeddings:
449452
if "model.embed_tokens.weight" not in state_dict:
@@ -452,11 +455,25 @@ def preprocess_weights(
452455

453456
new_state_dict: dict[str, torch.Tensor] = {}
454457
for key, value in state_dict.items():
455-
# MoE gate: router.layer.weight → gate.weight
456-
new_key = key.replace(
458+
new_key = key
459+
# MoE gate: router.layer.weight (legacy) / router.weight (current) → gate.weight
460+
new_key = new_key.replace(
457461
".block_sparse_moe.router.layer.",
458462
".block_sparse_moe.gate.",
459463
)
464+
new_key = new_key.replace(
465+
".block_sparse_moe.router.weight",
466+
".block_sparse_moe.gate.weight",
467+
)
468+
# Routed-expert weights: HF renamed the fused 3D tensors.
469+
new_key = new_key.replace(
470+
".block_sparse_moe.experts.gate_up_proj",
471+
".block_sparse_moe.input_linear.weight",
472+
)
473+
new_key = new_key.replace(
474+
".block_sparse_moe.experts.down_proj",
475+
".block_sparse_moe.output_linear.weight",
476+
)
460477
new_state_dict[new_key] = value
461478

462479
return new_state_dict

tests/_test_configs.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757

5858
LONGROPE_FACTORS = [1.0] * (int(TINY_HEAD_DIM * 0.5) // 2)
5959

60+
# NOTE (MLA models): Multi-head Latent Attention models (DeepSeek-V2/V3,
61+
# LongCat-Flash, ...) reconstruct full-head K/V from a shared latent, so they do
62+
# not use grouped-query attention. Their tiny configs must set
63+
# num_key_value_heads == num_attention_heads; otherwise HuggingFace's repeat_kv()
64+
# in the SDPA path duplicates the already-full-head K/V tensors, producing a
65+
# head-count mismatch against the query.
66+
6067

6168
def _base_config(config_cls=None, **overrides) -> ArchitectureConfig:
6269
"""Create a tiny ArchitectureConfig for graph-build and parity tests.
@@ -550,6 +557,8 @@ def _base_config(config_cls=None, **overrides) -> ArchitectureConfig:
550557
(
551558
"deepseek_v3",
552559
{
560+
# MLA: kv heads must equal attn heads (see MLA note near top of file).
561+
"num_key_value_heads": TINY_HEADS,
553562
"q_lora_rank": 32,
554563
"kv_lora_rank": 16,
555564
"qk_nope_head_dim": 16,
@@ -581,6 +590,8 @@ def _base_config(config_cls=None, **overrides) -> ArchitectureConfig:
581590
(
582591
"deepseek_v2",
583592
{
593+
# MLA: kv heads must equal attn heads (see MLA note near top of file).
594+
"num_key_value_heads": TINY_HEADS,
584595
"q_lora_rank": 32,
585596
"kv_lora_rank": 16,
586597
"qk_nope_head_dim": 16,
@@ -604,6 +615,8 @@ def _base_config(config_cls=None, **overrides) -> ArchitectureConfig:
604615
(
605616
"deepseek_v2_moe",
606617
{
618+
# MLA: kv heads must equal attn heads (see MLA note near top of file).
619+
"num_key_value_heads": TINY_HEADS,
607620
"q_lora_rank": 32,
608621
"kv_lora_rank": 16,
609622
"qk_nope_head_dim": 16,
@@ -774,6 +787,8 @@ def _base_config(config_cls=None, **overrides) -> ArchitectureConfig:
774787
"longcat_flash",
775788
{
776789
"_config_cls": LongcatFlashConfig,
790+
# MLA: kv heads must equal attn heads (see MLA note near top of file).
791+
"num_key_value_heads": TINY_HEADS,
777792
"q_lora_rank": 16,
778793
"kv_lora_rank": 8,
779794
"qk_nope_head_dim": 8,
@@ -1085,6 +1100,8 @@ def _base_config(config_cls=None, **overrides) -> ArchitectureConfig:
10851100
(
10861101
"deepseek_v2",
10871102
{
1103+
# MLA: kv heads must equal attn heads (see MLA note near top of file).
1104+
"num_key_value_heads": TINY_HEADS,
10881105
"q_lora_rank": 32,
10891106
"kv_lora_rank": 16,
10901107
"qk_nope_head_dim": 16,

tests/synthetic_parity_test.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
# Gemma4 text: per-layer input embedding + softcapping + QK-norm FP accumulation.
191191
# Argmax correct, cosine=0.985 — model is functionally correct.
192192
"gemma4_text": 0.15,
193+
# Olmo3: QK-norm + sliding/full attention FP accumulation → ~0.015 max diff.
194+
# Argmax near-tie, cosine=0.9999 — model is functionally correct.
195+
"olmo3": 0.02,
193196
}
194197

195198
# Model types with known ONNX-vs-HF divergences, tracked as xfail.
@@ -394,9 +397,6 @@
394397
"first_k_dense_replace": 0,
395398
"n_shared_experts": 1,
396399
},
397-
# GraniteMoeHybrid requires layer_types (defaults to None, causing runtime error).
398-
# HF accepts 'mamba' and 'attention' (not 'linear_attention'/'full_attention').
399-
"granitemoehybrid": {"layer_types": ["mamba", "attention"]},
400400
# HunYuanMoEV1 requires head_dim (defaults to None, causing pow(None, float) error).
401401
"hunyuan_v1_moe": {"head_dim": TINY_HEAD_DIM},
402402
# Llama4Text requires head_dim to match our tiny num_heads x head_dim = hidden_size.
@@ -540,13 +540,14 @@ def _create_hf_config(model_type: str, config_overrides: dict):
540540
for lt in hf_kwargs["layer_types"]
541541
]
542542

543-
# GraniteMoeHybrid uses layers_block_type (HF field) with "mamba"/"attention" values.
544-
# Convert our layer_types (which may use "mamba2"/"full_attention" internal names or
545-
# the HF-format values from _HF_EXTRA_CONFIG) to layers_block_type for HF.
543+
# GraniteMoeHybrid uses layers_block_type (HF field) with layer-type values.
544+
# Convert our internal "mamba2"/"full_attention" names to the current HF values
545+
# ("linear_attention"/"full_attention"); the legacy "mamba"/"attention" names
546+
# are no longer accepted by HF's layer-type validator.
546547
if hf_model_type in ("granitemoehybrid",) and "layer_types" in hf_kwargs:
547548
layer_types = hf_kwargs.pop("layer_types")
548549
hf_kwargs["layers_block_type"] = [
549-
"attention" if lt in ("full_attention", "attention") else "mamba"
550+
"full_attention" if lt in ("full_attention", "attention") else "linear_attention"
550551
for lt in layer_types
551552
]
552553

0 commit comments

Comments
 (0)