Skip to content

fix(functions): add optional bias slot to PackedMultiHeadAttention fallback - #449

Merged
justinchuby merged 2 commits into
mainfrom
squad/packed-mha-bias-slot
Aug 3, 2026
Merged

fix(functions): add optional bias slot to PackedMultiHeadAttention fallback#449
justinchuby merged 2 commits into
mainfrom
squad/packed-mha-bias-slot

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the formal-input arity of the standard-ONNX fallback ir.Function for com.microsoft::PackedMultiHeadAttention by adding the optional bias slot.

Root cause

ORT's PackedMultiHeadAttention has a 6–7 input positional signature:

query, key(opt), value(opt), bias(opt), token_offset, cumulative_sequence_length, attention_bias(opt)

Because token_offset / cumulative_sequence_length occupy positional slots 5 and 6, the optional bias at slot 4 must exist as a formal input (present-as-absent at call sites via "").

Call sites in mobius already correctly emit 6 inputs (q, k, v, "", token_offset, cu_seqlens):

  • src/mobius/models/_qwen3_vl_vision.py
  • src/mobius/rewrite_rules/_packed_attention.py

But the fallback ir.Function declared only 5 formals, dropping the bias slot. This made onnx-genai's function-inline admission reject the call:

call.input.len()=6 > func.input.len()=5  →  FunctionArityMismatch

for the Qwen3.6-35B-A3B vision_encoder. onnx-genai is spec-correct here; the bug was purely the missing bias formal in mobius.

Fix

  • Insert bias as the 4th formal input, between value and token_offset, yielding the correct positional order: query, key, value, bias, token_offset, cumulative_sequence_length (6 formals).
  • Add the matching bias_input parameter to the body body(...) in the same position. The body does not use bias — the fallback reconstructs the block-diagonal attention bias from cumulative_sequence_length — so it is present only to preserve positional slot alignment (treated like the already-unused token_offset_input).
  • Update the module/function docstrings to list bias: (optional) — unused in fallback in the correct positional order.

Test

Added tests/packed_multi_head_attention_function_test.py, which asserts:

  • the function declares exactly 6 formal inputs in the exact positional order query, key, value, bias, token_offset, cumulative_sequence_length (not just the count — bias must be at index 3);
  • a 6-input call node is admissible against it (len(call inputs) <= len(func inputs));
  • the function domain/name identity (com.microsoft / PackedMultiHeadAttention).

All 3 new tests pass; tests/ep_optimization_test.py (30 tests) passes as a regression check; ruff check + format clean.

…llback

The ORT com.microsoft::PackedMultiHeadAttention op has a 6-7 input
positional signature: query, key(opt), value(opt), bias(opt),
token_offset, cumulative_sequence_length, attention_bias(opt). Because
token_offset/cumulative_sequence_length occupy positional slots 5 and 6,
the optional bias at slot 4 must exist as a formal input.

The fallback ir.Function declared only 5 formals (dropping the bias
slot), so onnx-genai's function-inline admission rejected the 6-input
call sites with a FunctionArityMismatch (call.input.len()=6 >
func.input.len()=5) for the Qwen3.6-35B-A3B vision_encoder.

Insert the bias formal between value and token_offset. The body ignores
bias (the block-diagonal bias is reconstructed from cu_seqlens); it is
present only to preserve positional slot alignment. Add a regression
test asserting the 6 formals and their positional order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 13491cd09c76d0

Model Sub-model Changes Status

No architecture changes detected.


Legend: ⚪ No change · 🔵 Minor (attrs/inits) · 🟡 Moderate (nodes added/removed) · 🔴 Major (interface changed)

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 13491cd09c76d0

Model Metric Baseline Current Delta
bert (feature-extraction) model_size_bytes 359 KB 359 KB +0.0%
bert (feature-extraction) num_nodes 60 60 +0.0%
falcon model_size_bytes 364 KB 364 KB +0.0%
falcon num_nodes 68 68 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 107 107 +0.0%
gpt2 model_size_bytes 388 KB 388 KB +0.0%
gpt2 num_nodes 54 54 +0.0%
llama model_size_bytes 425 KB 425 KB +0.0%
llama num_nodes 62 62 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 58 58 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 98 98 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 60 60 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 56 56 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 62 62 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 58 58 +0.0%
qwen3_5_moe (hybrid-text-generation) model_size_bytes 506 KB 506 KB +0.0%
qwen3_5_moe (hybrid-text-generation) num_nodes 275 275 +0.0%
qwen3_5_text (hybrid-text-generation) model_size_bytes 458 KB 458 KB +0.0%
qwen3_5_text (hybrid-text-generation) num_nodes 129 129 +0.0%
qwen3_5_vl (hybrid-qwen-vl) model_size_bytes 977 KB 977 KB +0.0%
qwen3_5_vl (hybrid-qwen-vl) num_nodes 413 413 +0.0%
t5 (seq2seq) model_size_bytes 836 KB 836 KB +0.0%
t5 (seq2seq) num_nodes 166 166 +0.0%
whisper (speech-to-text) model_size_bytes 1008 KB 1008 KB +0.0%
whisper (speech-to-text) num_nodes 128 128 +0.0%

No performance regressions.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the standard-ONNX fallback ir.Function signature for com.microsoft::PackedMultiHeadAttention by adding the missing optional bias input slot so function inlining/admission no longer fails due to positional arity mismatch.

Changes:

  • Add bias as the 4th formal input (slot-alignment fix) in the PackedMHA fallback function body.
  • Update the fallback function docstrings/comments to document the unused-but-required bias slot.
  • Add a unit test asserting the fallback function’s formal input order and identity.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/mobius/functions/packed_multi_head_attention.py Adds the missing bias formal input (slot 4) to match the PackedMHA positional signature and updates docs/comments accordingly.
tests/packed_multi_head_attention_function_test.py Adds regression tests to lock down the fallback function’s formal-input order/arity and identity.

Comment thread src/mobius/functions/packed_multi_head_attention.py
Comment on lines +4 to +7
"""Unit tests for the PackedMultiHeadAttention fallback ``ir.Function``.

These guard the formal-input arity/order of the standard-ONNX fallback for
``com.microsoft::PackedMultiHeadAttention``. ORT's op has a 6-7 input
@justinchuby

Copy link
Copy Markdown
Member Author

VERDICT: APPROVE

Independent review by Harry (reviewer, opus-4.8). I did not author this change. Reviewed at commit 864c04f on branch squad/packed-mha-bias-slot in a detached worktree off origin/main.

1. Positional-wiring integrity — CORRECT.
build_function calls trace_function(gb.op, *trace_args) and builds graph_inputs from the same inputs list, so body parameters map to formals strictly by order (verified by reading onnxscript._internal.builder.build_function). The body now takes (op, query_input, key_input, value_input, bias_input, token_offset_input, cumulative_sequence_length_input) and the formals list is [query, key, value, bias, token_offset, cumulative_sequence_length] — perfectly aligned. token_offset and cumulative_sequence_length still land on the right formals. The inner op.Attention(...) receives query_batched, key_batched, value_batched, attention_bias, where attention_bias is the block-diagonal mask RECONSTRUCTED from cumulative_sequence_length_input — NOT the new bias_input. No miscompute.

2. bias is genuinely inert. grep shows bias_input appears only as the parameter name (line 75) and in an explanatory comment (line 79). It is never fed to any op — exactly as inert as token_offset_input.

3. Call-site parity — CORRECT (all 3 sites). Note: the actual call sites are src/mobius/components/_qwen3_vl_vision.py and _qwen25_vl_vision.py (shared _emit_packed_mha), plus the rewrite rule — the paths in the review brief were slightly off, but all were located and checked:

  • rewrite_rules/_packed_attention.py:170PackedMultiHeadAttention(q, k, v, None, token_offset, cu_seqlens_i32)
  • components/_qwen3_vl_vision.py:252(query_mha, key_mha, value_mha, None, token_offset, cu_seqlens_int32)
  • components/_qwen25_vl_vision.py(query_mha, key_mha, value_mha, None, token_offset, cu_seqlens_int32)
    All three emit 6 inputs with None (present-as-absent) in slot 4 (bias), matching the new 6-formal signature. No call site puts bias in the wrong slot.

4. Test non-vacuous + mutation check. tests/packed_multi_head_attention_function_test.py asserts the FULL positional order (actual_order == EXPECTED_INPUT_ORDER) and explicitly func.inputs[3].name == "bias", not just the count. Mutation check performed: I temporarily moved the bias formal to last; the test FAILED with At index 3 diff: 'token_offset' != 'bias'. Reverted; 3 passed in ~2s. Guard is effective.

5. Scope. git show --stat confirms only 2 files changed (the function + its new test), 74 insertions — mobius-only, no onnx-genai changes, no scope creep. Docstrings (module + bias slot lines) updated correctly.

Test result: 3 passed in 1.71s.

The fix correctly restores the 6th formal (bias at slot 4) so onnx-genai's function-inline admission accepts the 6-input call nodes, with zero behavioral change to the fallback decomposition. Approving.

@justinchuby

Copy link
Copy Markdown
Member Author

@copilot please fix review comments

… test

Rename the computed attention bias in the module docstring pseudo-code to
attn_bias so it is not confused with the new formal `bias` input, and move
the fallback-function test next to its implementation under
src/mobius/functions/ to match the existing convention.
Copilot AI review requested due to automatic review settings August 3, 2026 17:27
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ justinchuby
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/mobius/functions/packed_multi_head_attention_test.py:55

  • test_packed_mha_admits_six_input_call is currently a tautology: it only checks 6 <= len(func.inputs), which is already guaranteed by test_packed_mha_declares_six_positional_formal_inputs. This doesn’t actually exercise the failure mode described in the docstring (InlinePass/function admission rejecting a 6-input call node). Consider rewriting this test to build a minimal model containing a com.microsoft::PackedMultiHeadAttention node with a missing bias slot (""), register the function body, and run InlinePass to ensure inlining succeeds and the node is expanded.
def test_packed_mha_admits_six_input_call() -> None:
    func = packed_multi_head_attention()

    # A call site emits 6 inputs: (q, k, v, "", token_offset, cu_seqlens).
    # onnx-genai admits a call when len(call inputs) <= len(func inputs), so
    # the function must declare at least 6 formals for the call to be inlined.
    call_input_count = 6
    assert call_input_count <= len(func.inputs)

@justinchuby
justinchuby merged commit 95c442e into main Aug 3, 2026
21 of 22 checks passed
@justinchuby
justinchuby deleted the squad/packed-mha-bias-slot branch August 3, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants