Skip to content

Commit 3dc671c

Browse files
Copilotjustinchuby
andauthored
Stamp weight dtype on GQA PackQKV Concat/Transpose intermediates (#435)
The `PackQKVForGQA` / `PackQKVWithBiasForGQA` rewrites build the packed weight with `op.Concat` / `op.Transpose`, whose output values carry no declared type. When those intermediates are later folded into initializers there is no declared dtype to inherit, so an fp16 model could end up with fp32 packed weights — the drop that #351 mitigated downstream. The issue asked whether this still reproduces. It does. On `main`, a CPU-EP llama build gives: ``` Concat node_Concat_0 [FLOAT, FLOAT, FLOAT] -> None Transpose node_Transpose_1 [None] -> None ``` ### Changes - **`_group_query_attention.py`** — new `_propagate_dtype(source, *targets)` helper resolves the source parameter's dtype through the existing `initializer_dtype()` (declared type, `const_value` fallback, fail-closed on mismatch) and stamps it on the rewrite-created values. - **`PackQKVForGQA.rewrite`** — stamps `packed_w` / `packed_wt` from `q_w`. - **`PackQKVWithBiasForGQA.rewrite`** — same, plus `packed_bias` from `bias_q`. The bias `Concat` folds into an initializer too and had the identical untyped-output problem, so it is covered here rather than left for a follow-up. - **Tests** — parameterized fp32/fp16 regression tests for both rewrite paths, asserting the `Concat`/`Transpose`/bias-`Concat` outputs declare the model dtype. They fail on unmodified source (`assert None == FLOAT16`). ```python packed_w = op.Concat(q_w, k_w, v_w, axis=0) packed_wt = op.Transpose(packed_w, perm=[1, 0]) # Concat/Transpose outputs have no declared type; carry the weight dtype # forward so the folded packed initializer keeps the model dtype. _propagate_dtype(q_w, packed_w, packed_wt) ``` The #351 fold-pass stamping and `const_value` fallback are left untouched as defense-in-depth. Note: the `onnxruntime_easy` collection blocker cited as the reason this was deferred from #351 no longer applies — it ships in the `testing` extra, so `src/mobius/rewrite_rules/_group_query_attention_test.py` runs directly. <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #355 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Justin Chu <justinchuby@users.noreply.github.com>
1 parent 95c442e commit 3dc671c

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

src/mobius/rewrite_rules/_group_query_attention.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,33 @@
4343

4444
from __future__ import annotations
4545

46+
import onnx_ir as ir
4647
from onnxscript.rewriter._basics import MatchFailureError, MatchResult
4748
from onnxscript.rewriter._rewrite_rule import (
4849
RewriteRuleClassBase,
4950
RewriteRuleSet,
5051
)
5152

53+
from mobius._passes._dtype_utils import initializer_dtype
54+
55+
56+
def _propagate_dtype(source: ir.Value, *targets: ir.Value) -> None:
57+
"""Stamp ``source``'s dtype onto rewrite-created intermediate values.
58+
59+
Values produced by the replacement builder (``op.Concat(...)``,
60+
``op.Transpose(...)``) carry no declared type. When those intermediates are
61+
later folded into initializers, a missing declared type forces the fold
62+
passes to recover the dtype from ``const_value`` — or, absent that, to
63+
default to ``FLOAT``, silently widening fp16 weights. Stamping the dtype of
64+
the parameter the value is derived from keeps the type consistent from the
65+
point the packed weight is created.
66+
"""
67+
dtype = initializer_dtype(source)
68+
if dtype is None:
69+
return
70+
for target in targets:
71+
target.dtype = dtype
72+
5273

5374
def _has_unequal_kv_head_dimensions(k, v, past_key, past_value) -> bool:
5475
"""Return whether static K/V shapes prove incompatible GQA head dimensions."""
@@ -364,6 +385,9 @@ def rewrite(
364385
packed_w = op.Concat(q_w, k_w, v_w, axis=0)
365386
# Transpose packed weight: (q_out+k_out+v_out, hidden) → (hidden, q_out+k_out+v_out)
366387
packed_wt = op.Transpose(packed_w, perm=[1, 0])
388+
# Concat/Transpose outputs have no declared type; carry the weight dtype
389+
# forward so the folded packed initializer keeps the model dtype.
390+
_propagate_dtype(q_w, packed_w, packed_wt)
367391
packed_qkv = op.MatMul(hidden, packed_wt)
368392

369393
# Recover remaining GQA inputs and attributes from the matched node
@@ -487,10 +511,14 @@ def rewrite(
487511
packed_w = op.Concat(q_w, k_w, v_w, axis=0)
488512
# Transpose packed weight: (q_out+k_out+v_out, hidden) → (hidden, q_out+k_out+v_out)
489513
packed_wt = op.Transpose(packed_w, perm=[1, 0])
514+
# Concat/Transpose outputs have no declared type; carry the weight dtype
515+
# forward so the folded packed initializer keeps the model dtype.
516+
_propagate_dtype(q_w, packed_w, packed_wt)
490517
packed_mm = op.MatMul(hidden, packed_wt)
491518

492519
# Concat biases: (q_out + k_out + v_out,)
493520
packed_bias = op.Concat(bias_q, bias_k, bias_v, axis=0)
521+
_propagate_dtype(bias_q, packed_bias)
494522
# GQA has no bias input — the Add stays in the graph.
495523
packed_qkv = op.Add(packed_mm, packed_bias)
496524

src/mobius/rewrite_rules/_group_query_attention_test.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,50 @@ def test_packed_qkv_with_bias_uses_concat_nodes(self):
701701
"Transpose input should be Concat of W_q, W_k, W_v"
702702
)
703703

704+
@pytest.mark.parametrize("dtype", [ir.DataType.FLOAT, ir.DataType.FLOAT16])
705+
def test_packed_weight_intermediates_declare_weight_dtype(self, dtype):
706+
"""Concat/Transpose intermediates carry the projection weight dtype.
707+
708+
The replacement builder leaves new values untyped. Without an explicit
709+
stamp, folding ``Transpose(Concat(W_q, W_k, W_v))`` into an initializer
710+
has no declared type to inherit and can widen fp16 weights to fp32.
711+
"""
712+
config = dataclasses.replace(_LLAMA_CONFIG, dtype=dtype)
713+
m = build_from_module(registry.get("llama")(config), config)["model"]
714+
715+
rewrite(m, pattern_rewrite_rules=group_query_attention_rules())
716+
rewrite(m, pattern_rewrite_rules=pack_qkv_for_gqa_rules())
717+
718+
gqa_nodes = [n for n in m.graph if n.op_type == "GroupQueryAttention"]
719+
assert len(gqa_nodes) == config.num_hidden_layers
720+
721+
for gqa in gqa_nodes:
722+
transpose = gqa.inputs[0].producer().inputs[1].producer()
723+
assert transpose.op_type == "Transpose"
724+
concat = transpose.inputs[0].producer()
725+
assert concat.op_type == "Concat"
726+
assert concat.outputs[0].dtype == dtype
727+
assert transpose.outputs[0].dtype == dtype
728+
729+
@pytest.mark.parametrize("dtype", [ir.DataType.FLOAT, ir.DataType.FLOAT16])
730+
def test_packed_bias_intermediate_declares_bias_dtype(self, dtype):
731+
"""The packed-bias Concat intermediate carries the bias dtype."""
732+
config = dataclasses.replace(_QWEN2_BIAS_CONFIG, dtype=dtype)
733+
m = build_from_module(registry.get("qwen2")(config), config)["model"]
734+
735+
rewrite(m, pattern_rewrite_rules=group_query_attention_rules())
736+
rewrite(m, pattern_rewrite_rules=pack_qkv_for_gqa_rules())
737+
738+
gqa_nodes = [n for n in m.graph if n.op_type == "GroupQueryAttention"]
739+
assert len(gqa_nodes) == config.num_hidden_layers
740+
741+
for gqa in gqa_nodes:
742+
add = gqa.inputs[0].producer()
743+
assert add.op_type == "Add"
744+
bias_concat = add.inputs[1].producer()
745+
assert bias_concat.op_type == "Concat"
746+
assert bias_concat.outputs[0].dtype == dtype
747+
704748
def test_packed_qkv_with_bias_runs_with_ort(self):
705749
"""Biased packed-QKV GQA model runs correctly with ORT."""
706750
model = registry.get("qwen2")(_QWEN2_BIAS_CONFIG)

0 commit comments

Comments
 (0)