Skip to content

Simplify composite ONNX GenAI metadata - #478

Merged
justinchuby merged 152 commits into
mainfrom
justinchuby/simplify-composite-metadata-producer
Aug 21, 2026
Merged

Simplify composite ONNX GenAI metadata#478
justinchuby merged 152 commits into
mainfrom
justinchuby/simplify-composite-metadata-producer

Conversation

@justinchuby

@justinchuby justinchuby commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

Produce the canonical ONNX GenAI pipeline.workflow ABI across Mobius exports and validate it against onnx-genai#828.

  • remove serialized model.io, legacy phase/strategy duplication, and runtime-private scheduling/allocation policy;
  • emit typed SSA workflows for decoder generation, static cache, speculative decoding, adapters, VLM, diffusion/image edit, video, speech, codec/TTS, masked diffusion, ESM-2, and ProtBert;
  • describe graph-visible state semantics including append, indexed-scatter, and fixed-size recurrent replacement;
  • emit canonical encoder embedding profiles and application-sourced opaque auxiliary inputs;
  • assign deterministic symbols to anonymous dynamic ONNX dimensions instead of invalid YAML null;
  • stop emitting duplicated workflow ir_version and onnx_opsets, leaving schema and ONNX artifacts authoritative;
  • pin cross-repository CI to ONNX GenAI commit 509cd4e9c4471f4cbc59fe44b47168f0ae128fe3;
  • merge current main into the branch without rebasing.

Validation

  • 164 metadata producer tests passed, 4 skipped;
  • all 13 generated package families match committed fixtures and pass the authoritative validate_metadata parser/semantic validator;
  • 11 ONNX GenAI engine conformance tests passed using real generated Mobius packages;
  • changed files pass lintrunner formatting.

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

@justinchuby
justinchuby requested review from a team and a lite review from Copilot August 12, 2026 17:09
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

🏗️ Architecture Diff

Comparing 40975721219f0b

Model Sub-model Changes Status
bert (feature-extraction) model 23 🟡
falcon model 0
gemma2 model 0
gemma4 (gemma4) decoder 0
gemma4 (gemma4) embedding 0
gemma4 (gemma4) vision_encoder 0
gemma4_text model 0
gpt2 model 0
llama model 0
llama (static-cache) model 0
mamba (ssm-text-generation) model 0
phi3 model 0
phi3 (static-cache) model 0
qwen model 0
qwen (static-cache) model 0
qwen2 model 0
qwen2 (static-cache) model 0
qwen2_moe model 0
qwen2_moe (static-cache) model 0
qwen3 model 0
qwen3 (static-cache) model 0
qwen3_5_moe (hybrid-text-generation) model 0
qwen3_5_text (hybrid-text-generation) model 0
qwen3_5_vl (hybrid-qwen-vl) decoder 0
qwen3_5_vl (hybrid-qwen-vl) embedding 0
qwen3_5_vl (hybrid-qwen-vl) vision_encoder 0
qwen3_moe model 0
qwen3_moe (static-cache) model 0
qwen3_next (hybrid-text-generation) model 0
t5 (seq2seq) decoder 0
t5 (seq2seq) encoder 0
whisper (speech-to-text) decoder 0
whisper (speech-to-text) encoder 0
bert (feature-extraction) / model — 23 change(s)

Op summary: 60 → 68 nodes

--- base
+++ head
@@ -10,6 +10,14 @@
 Add
 Add
 LayerNormalization
+Cast
+Unsqueeze
+Shape
+Shape
+Shape
+Concat
+Expand
+Unsqueeze
 Transpose
 MatMul
 Add

Added nodes:

  • + Cast
  • + Unsqueeze
  • + Shape
  • + Shape
  • + Shape
  • + Concat
  • + Expand
  • + Unsqueeze

Connectivity changes:

  • node[4] Range: input_ids [43, 42, 44] → [44, 43, 45]
  • node[5] Cast: input_ids [45] → [46]
  • node[6] Unsqueeze: input_ids [46, 4] → [47, 4]
  • node[7] Gather: input_ids [5, 47] → [5, 48]
  • node[9] Add: input_ids [41, 48] → [42, 49]
  • node[10] Add: input_ids [50, 49] → [51, 50]
  • node[11] LayerNormalization: input_ids [51, 7, 8] → [52, 7, 8]
  • node[25] Add: input_ids [52, 65] → [66, 13]
  • node[33] Add: input_ids [73, 22] → [53, 74]
  • node[39] Transpose: input_ids [27] → [22]
  • node[40] MatMul: input_ids [76, 80] → [80, 81]
  • node[41] Add: input_ids [81, 28] → [82, 23]
  • node[49] Add: input_ids [76, 89] → [90, 29]
  • node[57] Add: input_ids [97, 38] → [85, 98]

Initializer changes:

  • count 38 → 39; dtype distribution: INT64: 1 → 2

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

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

Performance Comparison

Comparing 79b48bcdd9eaaa

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 66 66 +0.0%
gemma2 model_size_bytes 428 KB 428 KB +0.0%
gemma2 num_nodes 105 105 +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 60 60 +0.0%
llama (static-cache) model_size_bytes 425 KB 425 KB +0.0%
llama (static-cache) num_nodes 56 56 +0.0%
mamba (ssm-text-generation) model_size_bytes 296 KB 296 KB +0.0%
mamba (ssm-text-generation) num_nodes 94 94 +0.0%
phi3 model_size_bytes 421 KB 421 KB +0.0%
phi3 num_nodes 58 58 +0.0%
phi3 (static-cache) model_size_bytes 421 KB 421 KB +0.0%
phi3 (static-cache) num_nodes 54 54 +0.0%
qwen2 model_size_bytes 425 KB 425 KB +0.0%
qwen2 num_nodes 60 60 +0.0%
qwen2 (static-cache) model_size_bytes 425 KB 425 KB +0.0%
qwen2 (static-cache) num_nodes 56 56 +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 264 264 +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 126 126 +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 428 428 +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 updates the ONNX GenAI inference metadata emitter to produce a normalized composite schema, reducing duplicated/ambiguous scheduling and I/O contract representation across model, pipeline.strategy, and pipeline.phases.

Changes:

  • Removes duplicated run_on scheduling fields from pipeline.strategy.stages, making pipeline.phases the single scheduling/presence source of truth.
  • Removes top-level model.io emission for composite packages (e.g., native VLM), keeping I/O contracts scoped to pipeline.models.<component>.io (while preserving model.io for bare single-model packages via the non-pipeline path).
  • Explicitly classifies diffusion denoisers as run_on: every_step in pipeline.phases.

Reviewed changes

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

File Description
src/mobius/integrations/onnx_genai/inference_metadata.py Normalizes composite metadata by deduplicating stage scheduling fields and scoping I/O contracts to per-component locations; sets diffusion denoiser phase scheduling explicitly.
src/mobius/integrations/onnx_genai/inference_metadata_test.py Updates tests to assert the new normalized schema (phases-based scheduling; decoder I/O read from pipeline.models.decoder.io; no composite model.io).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

assert not any(transform["op"] == "normalize" for transform in transforms)
assert metadata["model"]["io"]["token_input"] == "input_ids"
assert metadata["model"]["io"]["kv_inputs"] == [
assert "model" not in metadata or "io" not in metadata["model"]
@justinchuby
justinchuby marked this pull request as draft August 12, 2026 18:03


class _PolicyPackage(Protocol):
def add_policy_component(self, name: str, component: PolicyComponent) -> None: ...
@justinchuby

Copy link
Copy Markdown
Member Author

Cross-repo contract audit against onnx-genai 4c3c4b6 found these concrete mismatches in 487d6ca:

  1. Metadata emits fp32 while the workflow schema/validator uses float32 (also float16/bfloat16). Please emit canonical workflow dtype names.
  2. Seeded sampler currently uses scalar temperature, seed, counter, and next_counter; the published contract requires batched temperature[B] and counter RNG seed[B], offset[B], next_offset[B].
  3. EOS graph has only token_ids/eos_token_ids -> terminated; the current termination role also requires iteration and max_iterations. I will split/relax the runtime contract so EOS predicate and loop max remain independently composable rather than forcing limit math into EOS.
  4. Euler graph exposes sample, derivative, scalar sigma, sigma_next; the current role's step/schedule mapping cannot describe both sigma values. I will revise the semantic role to explicit current/next schedule values.
  5. Masked update needs semantic mappings for confidence and threshold; the current role's integer step is not equivalent. I will revise this contract.
  6. Speculative artifact consumes probabilities/uniform and returns acceptance mask/count, whereas the current verifier role expects score tensors, accepted tokens, and done. I will revise the role to primitive acceptance math; token correction/state update remains separate workflow components.
  7. Token state update grows [B,T] -> [B,T+1]; current validation incorrectly requires identical current/next shapes. I will change it to dtype/rank compatibility and rely on state recurrence bounds.
  8. add_policy_components_to_workflow currently emits ports only, without each component's required policy semantic mapping/effect declaration. Those must be emitted once the revised fields land.

Please continue making sampler RNG batched/counter-based and canonicalizing dtypes. I will push the contract corrections and runtime E2E fixtures to #828, then post the new SHA here for exact producer alignment.

@justinchuby

Copy link
Copy Markdown
Member Author

Runtime E2E review of dd1cb838 found three remaining blocking mismatches in build_decoder_workflow_metadata:

  1. Decoder logits is rank 3 [B,T,V], but both sampler artifacts declare rank 2 [B,V]. onnx-genai now validates SSA edge contracts and will reject this package. Please ship/invoke an ONNX final-position selector (or make the sampler artifact explicitly accept rank-3 logits and select the last position); the runtime will not add host slicing.
  2. Workflow loop semantics are continue while condition is true. The generated graph binds condition: loop.done, which stops after the first ordinary token and continues after EOS. Please invoke an ONNX boolean inverter and bind a continue value, or change the termination artifact to expose continuation explicitly.
  3. loop.iteration is a fixed application input and is never loop-carried/updated. The termination graph's max-iteration predicate therefore does not advance. The host loop bound still caps execution, but the policy output is wrong. Please add an ONNX counter state-update component/carry or remove the redundant limit predicate from the termination artifact in a coordinated contract revision.

Also, emitted tokens is declared rank-1 [B] with mode: append; that only represents a batch-1 flat stream. For batched output the boundary must define [B,T] accumulation (or use per-row event emission with the serving contract). I am making the runtime fail rather than silently inspect only batch row 0.

@justinchuby

Copy link
Copy Markdown
Member Author

ONNX GenAI branch phi/effect-merge blocker is fixed at ad8e5e4f1a766eb95787c97266a6ed554e1b28ec on PR #828.

Emit branches with:

outputs:
  selected.tokens:
    cases: { "true": accepted.tokens, "false": corrected.tokens }
effects:
  rng:
    incoming: rng.0
    cases: { "true": rng.accepted, "false": rng.corrected }
    produces: rng.joined

Include default in each output/effect mapping whenever the branch has a default node. All phi contracts must unify. Case-local successor tokens may differ (or match each other), but produces is a distinct joined token. Only phi outputs and explicit emits escape branch scope.

Exact contract/docs: crates/onnx-genai-metadata/src/schema/ir.rs and docs/WORKFLOW_POLICY_COMPONENTS.md; generated schema is updated. Speculative token/KV/RNG/emit E2E passes.

@justinchuby

Copy link
Copy Markdown
Member Author

ONNX GenAI preprocessing-to-workflow SSA blocker is fixed at justinchuby/onnx-genai@f72782c64fbe04ac6e12593a69ecff47781f023b (draft PR #828).

Producer target:

  • declare adapter component { kind: adapter, abi: onnx-genai.image-preprocess, version: "1" } and pin manifest.adapter_abis["onnx-genai.image-preprocess"]: "1"
  • adapter input encoded: uint8 rank 1 [encoded_bytes]
  • add exact TensorContract to every preprocessing.image.outputs[]; source is processor-local and required; name is the workflow SSA value
  • invoke with explicit port maps, e.g. outputs: { pixel_values: image.pixel_values, grid: image.grid }
  • output cannot be optional in workflow metadata
  • feed those SSA values to vision/embedding/decoder invokes; do not emit legacy component.input names

Canonical documentation: docs/WORKFLOW_POLICY_COMPONENTS.md (Versioned adapter invocation). Schema: schema/inference_metadata.schema.json. Runtime E2E covers encoded request image -> preprocessing adapter -> pixel/grid SSA -> vision -> embedding -> decoder -> post-adapter emit. Branch phi/effect support remains at prior commit ad8e5e4f1a766eb95787c97266a6ed554e1b28ec.

@justinchuby

Copy link
Copy Markdown
Member Author

Follow-up audit of current Mobius head 151014eb found remaining generated-workflow blockers:

  1. Decoder state declaration still says state.token.initializer: request.<token_input> ([B,T]) while the state contract/current value is [B,1]. ONNX GenAI now validates declared state initializers statically and against the concrete value at loop entry (21f6994d). Set the initializer to the setup-produced [B,1] token value (token.setup) or restructure setup.
  2. loop.iteration is still a literal zero and is not updated/carried. The termination max-iteration input therefore remains semantically wrong. Use an ONNX counter/state-update component and carry its [B] output; no host loop-index fallback will be added.
  3. Decoder setup samples/emits one token, then the loop runs max_iterations body executions. That can emit max_output_tokens + 1; setup EOS also cannot prevent the first body execution because the declared loop condition is produced in the body. Either make setup initialization-only and perform all output steps in the body, or ship ONNX policy components/dataflow that compute the remaining body budget and gate the first body.
  4. The same setup-plus-body accounting should be checked in masked diffusion: setup performs one masked update, then the body may run the full requested iteration count.

TTS does not need a new host induction primitive: per the component-centric contract, initialize an integer state cell and invoke/carry a generic ONNX counter update inside the nested loop; use that SSA value for step_index/position selection.

Please generate one concrete package/YAML at the fixed head so I can run ONNX GenAI load+execution cross-repo.

@justinchuby
justinchuby force-pushed the justinchuby/simplify-composite-metadata-producer branch from bde3452 to 774448b Compare August 12, 2026 20:18
@justinchuby

Copy link
Copy Markdown
Member Author

ONNX GenAI loop induction blocker is fixed in justinchuby/onnx-genai@e877270123d38d49012e70ecbf7254e720cd3431 (PR #828).

Producers may now declare:

iteration:
  value: loop.i
  contract: { dtype: int64, rank: 1, shape: [batch] }

and bind loop.i directly to solver/masked-update/termination step, RNG counters, schedule selectors, or TTS step_index. It is zero-based, body/condition-scoped, deterministic, and lexically distinct for nested loops. No ONNX counter component is needed for the control-flow induction variable; reverse/remaining values still require an ordinary ONNX component.

Please migrate decoder/diffusion/masked/TTS loop iteration inputs to this field. The separate decoder setup/body output-count and initializer issues from the prior comment still need producer fixes before cross-repo execution.

@justinchuby

Copy link
Copy Markdown
Member Author

Cross-repo execution against ONNX GenAI #828 found one remaining producer-side contract mismatch (still present at current head 8f96651c0031ed082229ee404210c50de4a55216):

  • Decoder/VLM workflows carry decoder.setup.logits as invariant state.logits, then update it from decoder.body.logits (workflow_metadata.py around lines 1150-1195 and 2041-2105). A normal decoder produces setup logits [B, prompt_sequence, V] and body logits [B, 1, V], so the generic runtime correctly rejects the first recurrence as an invariant shape change.
  • Please carry last-token logits instead: invoke last_token_logits after the setup decoder, initialize a rank-2 [B,V] state from that result, invoke it after each body decoder, and carry the rank-2 result; alternatively restructure the loop so full rank-3 logits are not a carried invariant.

Other exact 774448b generated-package results:

  • masked diffusion executes end-to-end with the actual Mobius policy artifacts;
  • codec executes end-to-end;
  • decoder executes when the synthetic decoder is constrained to [B,1,V], confirming request binding, invoke, loop, KV growth, effects, and emit paths.

ONNX GenAI fixes discovered by this run: scalar defaults now materialize unbound symbolic axes as singleton tensors, and component shape symbols are invocation-local (with adapter allocation retaining package-scope symbols). Permanent conformance/regression tests are being pushed to #828.

Comment thread src/mobius/generation/_policy_components.py Fixed
Comment thread src/mobius/integrations/onnx_genai/workflow_metadata.py Fixed
@justinchuby

Copy link
Copy Markdown
Member Author

Fixed the decoder/VLM logits recurrence in 87bd5b4: setup and body decoder outputs are normalized through the generated last_token_logits component, and only rank-2 [B,V] values are carried invariantly. I generated a decoder with setup [B,T,V] and body [B,1,V] and ran ONNX GenAI conformance at 2ad2e0d; mobius_decoder_workflow_executes passed for 3 tokens. Diffusion also executed end-to-end for two Euler iterations on the same runtime. Muse metadata was regenerated and validates semantically; local SHA-256 is f1a33d7f162c350e79a37d11d8be5be0690a3c8c6cc0e71ce2b428c368dccabf.

@justinchuby

Copy link
Copy Markdown
Member Author

Remaining cross-repo blockers after 87bd5b4: (1) real speculative rejection cannot be represented faithfully because WorkflowNode::Emit has no accepted-length/validity operand, while verifier KV state also needs rollback/truncation to the accepted prefix; the current fixed-shape branch fixture is schema-valid but is not a correct rejection execution. (2) Real Qwen3-TTS still needs talker/code-predictor KV recurrence and the trained group-0/predictor embedding transition; the current representative nested-loop fixture validates structure but is not a real-package runtime E2E. Independent decoder, masked diffusion, codec, and Euler diffusion runtime paths pass; VLM/Muse validates against the latest schema.

Comment thread src/mobius/integrations/onnx_genai/workflow_metadata_test.py Fixed
@justinchuby

Copy link
Copy Markdown
Member Author

Performance acceptance instrumentation is pushed in e864abe.

The new paired-run gate rejects non-identical model/runtime/EP/device/precision/batch/shape/sampling/RNG/KV/capture/warmup conditions, then checks throughput, TTFT, peak memory, H2D/D2H counts+bytes, device syncs, session/kernel boundaries, device residency, and required island capture/replay. Required plans cover decoder+min-p+termination, speculative accept/reject, and grammar-delimited islands.

Current measured upstream baseline at ONNX GenAI 8bacf8c is not release-passing:

  • H200 decoder steady throughput workflow/native: 0.903 (9.7% regression).
  • H200 min-p throughput: 0.957 (within 5%), but warm TTFT 4.36/3.64 ms (19.8% regression).
  • Cold startup: decoder 467/49 ms, min-p 231/18 ms, rooted in first-run output-extent discovery/stable-binding construction.
  • Synthetic decoder/min-p islands captured once/replayed 503 times; speculative verifier/policy also captured/replayed.

Real Mobius package/KV/per-row serving measurements are still absent. Additionally, application_overridable samplers remain excluded by is_fusible_component, so the actual Mobius overrideable sampler path cannot yet demonstrate the required fused island. PR remains draft/not performance-ready.

@justinchuby

Copy link
Copy Markdown
Member Author

Architecture cleanup progress:

  • 66e5281: removed the closed PolicyRole enum. Policy ONNX artifacts now embed versioned namespaced contract IDs, and workflow declarations derive ID/version from artifact metadata.
  • ada6303: decoder and masked-diffusion loops now consume lexical loop induction directly; generated iteration state and iteration_increment.onnx are removed. Serialized invokes contain semantic tensor mappings only, with no effect-token fields/read-write chains.
  • Full Mobius suite: 3694 passed, 52 skipped; Ruff passed.

Muse concise metadata currently measures 600 lines / 546 mapping fields / 16,339 bytes, down from the reviewed 1,183-line source (583 lines, 49.3% reduction). A pre-lowering flattened-field baseline was not retained, so only the current 546-field count is evidence-backed.

Remaining producer work is blocked on exact current #828 contracts, reported at issuecomment-5274711168: per-row Emit.valid_length[B]/ragged append and logical state lengths, continue_when polarity, generic KV alias/group/sequence-axis semantics, and a shipped semantic-validator CLI. Current 8bacf8c still requires one-element emit length, so removing speculative ReduceMin now would create metadata the shipped validator/executor rejects. Legacy schema fixtures/continue-Not paths will be deleted when those contracts land; PR remains draft.

@justinchuby

Copy link
Copy Markdown
Member Author

ONNX GenAI producer blockers are resolved at justinchuby/onnx-genai@8215649100a0a27be15b04045fddde777c8248fc on PR #828. Producer migration can resume.

Use schema/inference_metadata.schema.json from that commit and run the shipped package validator:
cargo run -p onnx-genai-metadata --bin validate_metadata -- <generated-package-dir>

Key migration: continue_when is pre-test/zero-trip; emit valid_length: int64[B] without reduction and consume output.row.<row>; declare serving active/done/accepted_len/slot_ids; bind cache state to a KV service_group with sequence_axis, layout, semantic logical_lengths: int64[B], storage, and past/present aliases; request sources no longer duplicate the semantic role.

Full exact checklist and YAML are in docs/WORKFLOW_POLICY_COMPONENTS.md; the detailed coordination comment is on #828.

@justinchuby

Copy link
Copy Markdown
Member Author

Cross-repo execution against onnx-genai #828 imported 3b0445a immutably. Decoder/masked/codec execute; four packages fail before workflow runtime:

  • VLM embedding/model.onnx: inputs_embeds is declared float but produced from int64 (emit_inputs_embeds).
  • Diffusion text_encoder/model.onnx: encoder_hidden_states is declared float but produced from int64.
  • Speculative proposer/model.onnx: graph outputs proposed_tokens/scores have no producers.
  • TTS code_predictor/model.onnx: missing initializer producer for code_predictor.layers.0.input_layernorm.weight.

All seven metadata files pass the callable validator, demonstrating why runtime E2E is now mandatory. ONNX CI/test wiring is ready and will pin the corrected producer commit once these graph defects are regenerated.

@justinchuby

Copy link
Copy Markdown
Member Author

Confirmed corrected producer head 92bc47ef: all seven checked-in packages now execute under #828 (7/7), including VLM preprocessing, Euler loop, nested TTS, and speculative grammar forced-token rejection/correction. One remaining producer diagnostic: the TTS E2E emits ORT memory-pattern shape-reuse warnings as sequence shapes grow ({1,1,6} != {1,1,2..4}, {1,6} != {1,2..4}, later {1,1,7}). Execution is correct and ORT falls back, but shared dim_param names appear to claim equality across changing sequence dimensions. Please tighten those graph shape symbols in a follow-up to preserve allocator/capture quality.

@justinchuby
justinchuby marked this pull request as ready for review August 13, 2026 05:28
@justinchuby

Copy link
Copy Markdown
Member Author

Final validation at 9c0cc54b: ONNX GenAI metadata/schema validation passed; all 7/7 checked workflow packages execute (decoder, VLM, diffusion, masked diffusion, codec, TTS, speculative); deterministic fixture regeneration passed; Ruff passed; Mobius suite passed (3696 passed, 52 skipped). Muse metadata: 764 lines, 720 fields, 20,545 bytes, SHA-256 5f4019a492f2278728743e9e2658be00455c0952076098b3cbfae5d0514c0191. The Integration (fast) rerun remains queued without a runner; its prior failures were isolated DeepSeek numerical-parity tests unrelated to this metadata/serialization diff. No merge or HF upload performed.

@justinchuby

Copy link
Copy Markdown
Member Author

CI follow-up: the Integration (fast) rerun completed with the identical two pre-existing DeepSeek numerical-parity failures (test_deepseek_v2_lite_prefill_logits_match, test_deepseek_non_mla_decoder_prefill_logits_match). All other checks passed again, including ONNX GenAI metadata and L1/L3/L4/L5. The failures do not exercise package save, workflow metadata, TTS, or regenerated fixtures.

@justinchuby

Copy link
Copy Markdown
Member Author

Real Muse H200 follow-up is pushed at c332bec (producer dtype fix d7927c3).

Paired native result on ORT 1.28/H200, exact 68-token prompt, 128 new tokens, 1 warmup + 3 runs, CUDA Graph/shared KV: 63.3653 tok/s median, 49.0217 ms TTFT (2.60% above the historical 61.76 tok/s baseline).

The real metadata workflow currently cannot complete in frozen ONNX GenAI a341c463; exact runtime blockers found while executing the 52-layer package:

  1. text-only VLM cannot omit request.image (optional tensor defaults unsupported);
  2. image adapter rejects derived total_patches before preprocessing;
  3. CUDA loop carries/continue masks cannot be cloned/inspected without host materialization;
  4. cuDNN fused GQA fails for the real prefill shape, requiring unfused fallback.

Mobius also fixed BF16 decoder logits → float32 sampler ABI and added the exact runnable native/workflow harness. I returned the PR to draft and have not replaced the HF metadata, because the currently frozen runtime cannot execute the published workflow without runtime changes.

justinchuby and others added 15 commits August 21, 2026 05:40
The formatter disagrees with the hand-wrapping in the new test module.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
The static-cache package declares both a `model.io.static_cache` port ABI
and a workflow that binds it, because the two answer different questions
and two different consumers read them. The pinned runtime rejected that
pair outright and told authors to move the ABI to
`pipeline.models.<component>.io`, a key the workflow IR had already
removed, while its own decode backend refused to load a static-cache
graph that did not declare one. No package could satisfy both.

The new pin permits the overlap and cross-checks it instead: the ABI's
write-destination port must be bound by a state group whose update is an
indexed scatter, exactly one component may claim it, and every per-layer
key and value pair the ABI names must be a pair that group actually
advances. All eleven fixture packages validate against it, which is what
this pin buys.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
A package could state its decode-step ABI twice: once as `model.io`, and
once as the component ports, invoke bindings and state groups a workflow
engine executes. Two writable answers to one question is a defect whatever
their contents, because nothing forces them to agree and a reader of one
never learns the other said something else. The previous commit made that
worse by adding `model.io` to static-cache exports specifically, which also
meant a bare single-file decoder and a composite package were suddenly
different *kinds* of document rather than one shape with a different number
of components.

`pipeline.workflow` is now the only description, for every export. `model`
keeps package-wide geometry and capabilities and never a port ABI. A runtime
that wants an optimized single-graph path derives it by lowering the
one-component workflow, and a derivation cannot disagree with its source.

Three facts had to move into the workflow for that to be true rather than
merely asserted.

Every ONNX component now declares `ports.inputs`/`ports.outputs`: a full
contract for exactly the graph's inputs and outputs, no more and no fewer. A
subset would let a consumer quietly fall back to opening the artifact; a
superset would be a promise the graph does not keep.

Every ONNX component declares `ports.roles` — what it *does* with a value
bound to a port. An invocation records which SSA value reaches a port, not
whether that port is tokens, a mask or logits, and recovering the difference
from a port's spelling is the name-guessing this format refuses everywhere
else. Mobius mints these names in its own task builders, so it states the
mapping between its vocabulary and the runtime's rather than inferring one; a
port outside that vocabulary gets no role, because a workflow that guesses is
worse than one that stays silent.

State port aliases declare `role` and `layer`. A layer's key buffer and its
value buffer are the same dtype and the same shape, and a cell's label sorts
lexicographically so `cache_10` precedes `cache_2` — a consumer pairing
per-layer buffers positionally would silently transpose two layers' caches
with nothing failing. Both fields are emitted together or not at all, so a
recurrent or convolution cache is never handed a fabricated index that would
corrupt the very ordering the index exists to fix. `IndexedScatter` gains
`kv_length_ports` beside `write_indices_ports`, because a valid length and a
write cursor are both rank-1 integer vectors and are indistinguishable from
each other by shape.

`_add_explicit_io_to_file` is deleted. It was the only route by which an
export could gain a `model.io`, it had no caller, and dead code that can mint
a forbidden key is exactly the kind of thing that gets resurrected. The
`adapter` fixture's hand-written decoder declared `ports: {}`; it now declares
its real ports, since a component that describes nothing is not a description.

`tests/canonical_workflow_contract_test.py` is what keeps this from lapsing.
It asks one set of shape-agnostic questions — workflow present, no `model.io`,
declared ports exactly equal to the graph's, every invoke binding and state
alias resolving to a declared port, scatter control ports declared, decode
step reconstructible from the workflow alone — of dynamic, static-cache, FP8,
heterogeneous and composite packages, and of all 11 checked-in fixtures. None
of the assertions names a feature, so a future feature cannot grow a private
top-level block while every feature-specific test keeps passing.

Verified against ONNX GenAI 02e22dd6, which lands the matching lowering: 11/11
packages validate and 11/11 execute under the runtime conformance suite,
including the fixed-capacity decode path, with no `model.io` in any package.
That last result is what makes removing the second copy safe rather than
merely tidy. The CI pin moves to that commit.

Tests: 4654 passed, 58 skipped in the fast suite; lintrunner clean; fixtures
regenerate byte-identically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Rebasing onto main brought in inspect_components, which reports a package's
components from the task's model_roles. Qwen3-TTS builds four loop-wiring
graphs — code_predictor_prefill, code_predictor_step_embedder,
code_predictor_indices and talker_text_step — that were never declared there.
Two things followed from that omission.

build_from_module looks each component up in model_roles and falls back to the
"decoder" role when it is absent, so these parameter-free graphs were offered
the GQA and QKV-packing passes meant for attention stacks. And
inspect_components reported six components for a package that contains ten, so
a caller planning per-component work would silently skip four artifacts that
ModelPackage.save writes to disk.

Declare them with a "glue" role: a graph that reads every tensor it uses from
its own graph inputs, carries no weights, and has no fusion to gate. Give them
empty HF_COMPONENT_SOURCES entries, because mobius authors them and no
HuggingFace sub-module backs them.

Guard both directions. arch_validation_test now fails any task that builds a
component it does not declare, and exempts glue components from the
"must have initializers" check — a wiring graph holding only hoisted constants
is what correctness looks like, not a lost-weights bug. A network-free unit
test pins the same invariant for Qwen3-TTS in the fast suite.

The rebase also resolved two conflicts semantically: __all__ keeps main's
inspect_components alongside the branch's generation and
fingerprint_model_weights, and unet_parity_test keeps main's _run_onnx helper
while taking the branch's FLOAT timestep, which is the dtype the denoiser port
now declares.

Repin the onnx-genai validation checkout to f19f2e71, the current head of the
schema branch after its rebase; the previous pin is no longer on that lineage.
11/11 fixtures validate and 11/11 conformance tests execute against it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
The validation checkout pinned f19f2e71, which is a reachable ancestor of
justinchuby/onnx-genai@justinchuby/simplify-composite-metadata but no longer
its head. An ancestor pin is only as durable as the branch's history: that
branch has been force-pushed repeatedly, and each rewrite risks orphaning the
commit CI resolves, which would fail the checkout rather than the assertion.
Pinning the head keeps the reference on the branch for as long as possible and
makes the SHA verifiable with a single git ls-remote.

c344c2c7 adds only documentation and a test doc-comment on top of f19f2e71 --
no schema, validator or runtime change -- so it cannot move the contract Mobius
emits against. Re-verified against the new pin rather than assumed: all 11
emitted packages validate, and all 11 workflow conformance tests pass,
including mobius_static_cache_workflow_executes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Every package these tests build has two layers, and two layers cannot
distinguish a correct layer annotation from a broken one. Below ten cells a
label sorts the same way under every rule -- lexicographic, numeric, insertion
order -- so a `layer` taken from a cell's position instead of parsed from its
port name satisfies each existing assertion exactly as the correct value does.
Real decoders have twenty to eighty layers, which puts production permanently
in the region the suite could not reach.

The failure that region hides is silent. A layer's key and value buffers share
a shape and a dtype, so two transposed caches raise nothing: no validation
error, no shape mismatch, only subtly wrong tokens.

So build twelve-layer dynamic, static-cache and hybrid decoders and assert
what only holds there: that the labels really do sort out of layer order at
this depth (otherwise the rest proves nothing), that each declared layer
restates the index its port name carries, that ordering by layer and half
recovers the buffer lists the runtime collects, and that a hybrid's two
alternating groups own layers a cell's position within its group never equals.

Verified by mutation rather than assumed: dropping the parse from
_cache_layer_index leaves all 101 pre-existing metadata assertions green and
fails seven of these, across all three shapes.

Also pin GenAI validation to 0d4738e7, which makes both facts these tests rely
on enforceable rather than merely documented -- a group binding key/value
roles must now declare kv_length_ports, and every such alias must carry a
layer. A real twelve-layer static-cache export validates against it, declaring
layers 0 through 11; all 11 fixtures validate and all 11 conformance tests
pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
1d8cfefe is additive over the previously pinned 0d4738e7: it adds an
end-to-end test that a package carrying no `model:` block still resolves the
static-cache ABI and executes through the scatter driver, plus the canonical
fixture that test runs against. No validator or schema behaviour changes.

That test is worth pinning because it closes the last gap between what this
producer emits and what is proven to run. Until now the evidence that a
workflow-only package was sufficient came from validation and from Mobius's
own conformance run; the ABI resolution itself was argued from call sites.
It is now asserted against a real graph on the consumer side.

Re-verified against the new pin rather than inherited from the previous run:
all 11 emitted packages validate and all 11 conformance tests pass. Compared
the emitted static-cache package against the new canonical fixture directly --
`update.write_indices_ports`, `update.kv_length_ports`, per-alias `role` and
`layer`, `components.<c>.ports.roles` and the absence of a top-level `model:`
block all agree, so the two describe the same contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
The previously pinned 1d8cfefe is no longer reachable from
justinchuby/onnx-genai@justinchuby/simplify-composite-metadata: that branch was
rebased onto current main, so every SHA on it changed and the old head became
an orphan. `git merge-base --is-ancestor 1d8cfefe 60f41354` is false. CI still
resolved the old SHA because GitHub serves unreferenced objects, which is the
failure mode worth avoiding -- the pin kept working while pointing at a commit
that is on no branch and whose content nobody is maintaining.

60f41354 is the current head of that branch and of PR #828. The rebase carried
no content change, so the two facts this pin exists to check are unchanged: the
workflow-derived scatter proof and the kv_length_ports / role / layer
enforcement.

Re-verified against the new head rather than carried over: all 11 emitted
packages validate and all 11 workflow conformance tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
The branch was rebased onto current main again, so 60f41354 is now
unreachable from it (`git merge-base --is-ancestor 60f41354 2498e0bc` is
false), the same way 1d8cfefe was before it. As noted last time, an orphaned
pin keeps reporting green because GitHub serves unreferenced objects, so this
has to be caught by checking lineage rather than by waiting for a red job.

2498e0bc also changes how a component's port roles resolve. A declared role
now names its port on its own; the port contracts are consulted only to break
a tie between two ports claiming the same role. Previously a role was honoured
only if the port also appeared in `ports.inputs`, so a producer that declared
roles without transcribing its graph had the declaration silently dropped and
was matched by port spelling instead.

This producer declares both, so it was never affected and nothing here has to
change. Keeping the contracts is deliberate: they are optional now, not
forbidden, and they live inside `pipeline.workflow`, which is the one canonical
representation -- they are not a competing statement of the ABI the way a
second top-level block would be. They also carry the dtype, rank and shape a
consumer needs to allocate without opening the ONNX file, and the tests that
guard against contract drift assert on them. Dropping them would remove
information and weaken those guards to satisfy a preference, not a rule.

Re-verified against the new head: 11 of 11 packages validate, 11 of 11
conformance tests pass, 111 metadata tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
A component backed by a shipped `.onnx` now declares only `ports.roles`. The
artifact travels inside the package and is authoritative for which ports exist
and what dtype, rank and shape each one has, so restating that in YAML created
a second writable statement of one fact with nothing keeping the two in
agreement — the same defect as `model.io`, one level further down. The runtime
resolves ports against the live session, which catches a name the graph does
not expose rather than agreeing with a stale echo of it.

What no graph can state is what a port *means*: `input_ids` and `position_ids`
are both rank-2 int64, and nothing in the file says which is the autoregressive
sequence. That is the whole remaining declaration, and it is what lets the
decode ABI resolve without recognizing a spelling. The static-cache ABI is
unaffected: it was already derived from the state-service group's aliases and
the scatter's `write_indices_ports` / `kv_length_ports`.

Policy graphs keep their contracts, and that boundary was measured rather than
assumed. A workflow SSA value inherits its dtype, rank and request axis from
the port that produced it, so those contracts are the type annotations of the
workflow's own dataflow, not a description of an external interface. Dropping
them made 4 of 11 packages invalid — `<node>.when is row-wise but
<node>.value declares no request_aligned batch_layout` — because a validator
reads metadata without the artifacts and has no other source for the axis.

The contract tests move with the truth they check. Roles, invocation bindings,
state pairs and the two scatter control ports now resolve against the graph
itself instead of against the metadata's agreement with its own copy, which is
strictly stronger: a role naming a port the artifact does not expose now fails
where before it only had to match a line the same producer wrote.

`TestRolesAloneCarryTheDecodeAbi` is the regression guard for the omission.
Every port of every roled component is renamed to an opaque label and the ABI
must resolve identically, so nothing recognizable is left to have matched on;
deleting the role table must then break exactly the sequence and logits
binding and leave the cache half untouched, which is what proves the roles are
carrying the fact rather than decorating it.

Verified against ONNX GenAI 2498e0bc (confirmed as the remote branch and PR
#828 head): 11/11 packages valid, 11/11 runtime conformance including
`mobius_static_cache_workflow_executes`, 4532 fast-suite tests green,
lintrunner clean. 1376 lines of transcription removed from the fixtures.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
`2498e0bc` was the branch head when it was pinned and is still reachable —
`git merge-base --is-ancestor 2498e0bc 0d1f1702` succeeds — so this is not the
orphaned-pin failure. It is the weaker version of it: the pin names a commit
that is no longer what the branch says, so CI was answering a question about a
state that had moved on, and any new check added upstream would not have run
here until something happened to notice.

Nothing about the pinned commit was wrong. The rule is simply that a pin should
name the ref's head, because an ancestor is a state nobody is maintaining and
the gap only ever widens.

`0d1f1702` adds the artifact to the canonical `tiny-llm-scatter-workflow`
fixture and a test that validates that package as a directory rather than as a
YAML string, which is the same entry point our fixtures go through. Re-ran both
gates against it: 11/11 packages valid and 11/11 runtime conformance, with
TensorScatter firing per layer per step on the fixed-capacity path. Unchanged
from the previous head, which is the expected result — that head added a test
and a fixture and no validation logic — but it is checked rather than assumed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
`7324351a` moves the document-level invariants onto `load_metadata_package`.
Until now they ran only for callers who already held a parsed document, which
excluded everyone loading a package from disk — including the
`validate_metadata` binary our CI job invokes. So the entry point our fixtures
actually go through was enforcing a strictly weaker rule set than the one we
believed we were being checked against.

All 11 packages were re-run against it rather than assumed to be unaffected,
because a strictness increase on the exact path we validate through is the case
most likely to surface something: 11/11 valid, 11/11 runtime conformance, with
TensorScatter still firing per layer per step on the fixed-capacity path.

They pass for a structural reason rather than a lucky one. The rule that gained
teeth forbids a package from carrying both `model.io` and a workflow, and ours
carry no `model:` block at all — there is nothing for a coexistence check to
find. This is the first external confirmation that emitting one serialized ABI
was worth doing for a reason beyond tidiness: the packages that were relying on
the loader's silence are the ones this commit would have broken.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
A component named in an `indexed_scatter` group's `write_indices_ports` or
`kv_length_ports` is being handed the write cursor and valid length of a
preallocated cache. Exactly one consumer reads those: the driver that scatters
into the buffer at an index, and it binds its ports from the resolved decode
ABI, every field of which is found by role. So a component handed the cursor
while declaring no sequence role cannot be resolved as a decoder at all, and
the package silently degrades to inferring ports from shapes — the behaviour
the canonical form exists to remove.

Nothing upstream catches it, and the reason is structural rather than an
oversight. Identifying the decoder requires a sequence role, so a component
that omits one is invisible to the check that would have caught it. The
upstream sole-decoder guard works around that by firing only on workflows with
a single ONNX component — but policy graphs are ONNX components, and every
package we emit ships ten, so the guard is disabled by construction on
precisely the packages that matter. Measured against `6e2ddc78`: dropping the
role from the shipped `static_cache` fixture still reports `valid`.

That leaves the producer as the only place the contradiction is visible, so it
is asserted here, on both the built packages and the shipped fixtures. The
second copy is not redundant — a fixture is hand-edited and regenerated far
more often than the producer is changed, and the mutation above is exactly the
edit that would slip through.

The rule is scoped by what the ports are for rather than by counting anything.
`speculative` owns attention state and declares no sequence role, which is
correct: it is driven through explicit invoke bindings and never asks for a
single-decoder ABI. It binds no scatter cursor, so the obligation does not
reach it, and no exemption list is needed to say so.

Pin moves to `6e2ddc78`, which adds that upstream guard. Re-ran both gates
against it: 11/11 valid, 11/11 conformance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
…ucer

Two changes that both remove a second copy of something the tree already
states once.

`add_explicit_package_io` wrote `metadata["model"]["io"]`. It had no
production callers, and ONNX GenAI is removing the serialized field with no
compatibility shim, so the only thing it could still do was tempt a future
caller into emitting an ABI the runtime discards at load. Removed with its
six tests; the surviving cross-attention test keeps its coverage under a name
that says what it checks. The static-cache error message no longer names
`model.io.static_cache`, a key nothing emits.

The eleven conformance packages committed 146 binary files and about 14 MB of
graphs and weights, including a 5 MB blob. They are a deterministic function
of the generator, CI already regenerated and compared the whole tree, and no
reviewer can read a diff of them. Only the metadata is committed now.
Textproto is the right answer for ONNX GenAI's 26 KB synthetic fixtures and
the wrong one here: these carry real weight blobs that a text encoding would
grow, and this repository does not use the protobuf APIs a textproto writer
needs. Tests that need a graph build them once per session through the new
`materialized_workflow_packages` fixture; generation takes about four seconds.

This decides where CI must point, and the failure would otherwise have been
silent in the wrong direction. A checkout of the committed tree alone does not
validate — `component 'cache_length_update' artifact ... cannot be opened` —
which is correct, because a workflow claims to describe something executable.
Verified by archiving the staged tree and running the validator against it.
Validation and conformance now run against the regenerated tree, and the
conformance harness no longer guesses a default path that resolved inside the
ONNX GenAI checkout it is copied into.

Two guards keep the removal from hollowing out the assertions that remain.
The comparison step checks that every artifact the committed metadata names
was really produced, so a generator that stopped emitting one is caught
rather than leaving a test with nothing to compare. `_ports` asserts the same
per package, because two of its three callers skip components they cannot
resolve and would have passed vacuously against an empty directory; a
mutation pointing the fixture at a missing package fails all eleven.

`test_describes_itself_only_through_the_workflow` also tightened from "no
`model.io`" to "no second ABI at all", including the legacy
`pipeline.models`, which is the rule the runtime actually enforces.

Verified: 11/11 validate_metadata and 11/11 runtime conformance against the
regenerated tree at ONNX GenAI 6e2ddc78, canonical contract suite 145 passed,
fast suite 4541 passed, lintrunner clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Rebasing 141 commits onto `79b48bc0` conflicted in the five files where this
branch's workflow migration overlaps the two new model integrations. The
conflicts were resolved in favour of this branch, which is right for the
metadata architecture and wrong for everything else main had just added, so
the endpoint was then checked against main hunk by hunk rather than trusted.

Every `def`/`class` main added was confirmed present, which surfaced what the
resolution had silently dropped. `--revision` was threaded through the asset
writers by #509 and did not survive: `_write_clip_tokenizer`,
`_write_hf_tokenizer` and `_copy_runtime_assets` lost the keyword from their
signatures while their bodies still referenced it, and
`write_onnx_genai_config` lost the parameter entirely. The last one is the
dangerous shape — it has `**kwargs`, so a pinned revision would have been
absorbed and ignored rather than raising, and the package would still build
and still validate while its tokenizer came from whatever the branch tip
happened to be. That is precisely the failure a pin exists to prevent.
Restored at all nine sites, plus `_write_text_runtime_assets`, which is this
branch's own helper and needed the same parameter to pass it on.

Also restored: audio-processor emission for speech-language packages, which
#509 added to the multimodal dispatch and this branch's rewrite of that branch
did not carry. Expressed as `_has_audio_encoder` rather than main's inline
`"audio_encoder" in pkg` so a package object without `keys()` is a False
rather than a TypeError.

Two of main's tests could not be taken verbatim.
`test_revision_is_forwarded_to_detection_and_build` was overwritten wholesale
by a test of this branch's and is restored byte-identically alongside it.
`test_dispatch_audio_only_multimodal_pipeline` asserts on `pipeline.models`,
the legacy composite ABI this branch replaces, so its still-relevant half —
that the revision reaches the feature extractor — is covered by a new test
against the workflow instead.

`test_runtime_onnx_genai_routes_vlm_through_workflow_emitter` pins the writer
call exactly and now expects the threaded `revision`.

Verified: 4580 passed (up from 4541; the increase is main's new tests now
running), 11/11 `validate_metadata` and 11/11 runtime conformance against
ONNX GenAI `6e2ddc78`, lintrunner clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
@justinchuby
justinchuby force-pushed the justinchuby/simplify-composite-metadata-producer branch from f559607 to 43ef64a Compare August 21, 2026 05:59
justinchuby and others added 10 commits August 21, 2026 06:25
`6e2ddc78` was the branch head when it was pinned, and it is now reachable
from no ref on that repository — `git ls-remote` returns zero matches for it
and no remote ref has it as an ancestor. The branch was rebased onto a newer
base, which rewrote every commit; the same change now exists as `fb358d42`
with a different hash.

The failure this avoids is a slow one. `actions/checkout` keeps succeeding
against an unreferenced object until GitHub collects it, and then fails with
`reference is not a tree` — at a time nobody controls, on a commit nobody can
inspect, in a job unrelated to whatever change is in flight. Repinned to the
live head `52339e10`, verified with `ls-remote` rather than an API lookup,
which answers just as happily for an orphan.

Not a fast-forward, so both suites were re-run rather than assumed: 11/11
`validate_metadata` and 11/11 runtime conformance against `52339e10`,
including `mobius_static_cache_workflow_executes`. The rebase carried the
sole-decoder sequence-role rule through unchanged and adds a CUDA device-memory
sampler fix that our packages do not exercise.

The document had the same fragility as the workflow file and less reason for
it, so its two commit citations are replaced by descriptions of what changed.
`.github/workflows/main.yml` is now the only place naming a hash, because it
is the only place that has to fetch an exact tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
…ttable

The pinned commit 52339e10 is no longer reachable from any ref: the GenAI
branch was rebased a second time and now heads at 0497c6f4.  An unreferenced
commit is GC-eligible, and `actions/checkout` fails with "reference is not a
tree" whenever the collection happens to run -- a red build at a time nobody
controls, on a commit nobody can inspect.  Verified with `git ls-remote`
(zero refs match) and `git merge-base --is-ancestor` (not a fast-forward, so
the move could not be assumed benign).  `gh api .../commits/<sha>` still
answers 200 for such objects, which is why it must not be the check used.

Repinned to the live branch head, re-verified rather than assumed: all 11
generated packages validate against it, and all 11 execute under the runtime
conformance suite, with TensorScatter firing per layer per step on the
static-cache package.

Also ignore validation/.  CI and the materialized_workflow_packages fixture
regenerate the full 14 MB of packages there, and the directory was untracked
but not ignored -- one `git add -A` would have restored exactly the megabytes
of unreviewable graphs that were just removed from the index.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
The pinned commit has now been rewritten out from under CI twice in a day,
because the branch it names is rebased on a schedule this repository does not
control.  Each time, the pin kept resolving until the unreferenced object was
collected, at which point `actions/checkout` would fail with "reference is not
a tree" on a commit nobody can inspect.  Bumping the pin each time treats the
symptom; the cause is that reachability of the pinned object is someone else's
to revoke.

Anchor it: the tag mobius-pr478-pin in justinchuby/onnx-genai now points at the
exact commit this workflow pins, so the object stays reachable across rebases
of the branch it came from.  The workflow still pins the SHA, not the tag --
the SHA is what makes a run reproducible, and the tag exists only to keep it
alive.  The pin itself is unchanged and remains the commit both suites were
last verified against.

Also correct the check the docs recommend.  `git ls-remote | grep <sha>` asks
whether a commit is a ref *tip*, which a healthy pin stops being as soon as one
more commit lands on the branch; on its own it reports a false orphan.  The
question that decides whether checkout resolves the commit, and whether it can
be collected, is `git merge-base --is-ancestor <sha> <branch-tip>`.  Both beat
`gh api .../commits/<sha>`, which answers 200 for unreferenced objects and so
fails open.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
The GenAI branch was rebased again -- twice within about two hours, this time
because origin/main advanced underneath it -- so the previously pinned commit
is no longer reachable from PR #828's head.  CI never became at risk: the tag
mobius-pr478-pin added in the previous commit kept the pinned object
referenced, which is exactly the failure this anchor exists to absorb.  The
tag has been moved to the new pin, per the rule recorded alongside it.

Verified rather than taken on assurance, because a pin move is gated on both
suites: all 11 generated packages validate at the new head, and all 11 execute
under the runtime conformance suite, with TensorScatter firing per layer per
step on the static-cache package.

The upstream summary described this push as touching neither the schema, the
validator, decoder_abi.rs, nor the workflow recognizer.  That is right in
substance: diffing the metadata crate between the two pins leaves
validation.rs and decoder_abi.rs untouched, and the only change is in
parser.rs, where MtpProposerSpec::mtp_state_output becomes Option<String> so a
proposal-local head can decline to thread recurrent state.  That is the
speculator sidecar descriptor, not the package load path, and no package this
producer emits reaches it.

Signed-off-by: Justin Chu <justinchu@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Protein language models were the first real users of the encoder path, and
exporting them exposed four defects that all shared one cause: encoders were
being treated as decoders that happened to stop early.

Metadata. An encoder package fell through to the decoder producer and emitted a
greedy autoregressive loop -- max_output_tokens, eos ids, a sampler, a KV cache
-- for a bidirectional model that cannot generate anything. The new
encoder-embedding producer emits what actually happens: one invoke, one emit, an
`embedding` profile carrying mask-aware mean pooling, and
`batch_invariance: row_independent`. The declared inputs are read from the
artifact rather than the task signature, because optimization prunes unused
graph inputs -- ESM-2 has no token-type embedding, so its metadata must not
promise one.

Batching. The rank-2 int64 attention mask was passed straight to op.Attention,
which cannot broadcast it, so every encoder was broken for batch > 1 and merely
added a 0/1 bias at batch 1. BERT now builds a 4D bool padding mask once and
shares it across layers.

DistilBert. It discarded `attention_mask` outright: padded rows changed 99.4% of
their values. The mask is now threaded through the encoder layers. This was
found by the new padded-batch integration test, not by inspection, which is the
argument for the test.

ESM-2. It was registered to the generic BERT module, which cannot load the
checkpoint: ESM-2 uses rotary embeddings, pre-norm blocks, a final embedding
LayerNorm, token dropout, and no token-type embeddings. `models/esm.py`
implements it, with submodule names arranged so initializer names match
HuggingFace directly and renaming reduces to stripping the `esm.` prefix.

ProtBert. Its config declares no `model_type`, so `AutoConfig` refuses it and
the builder misrouted to the diffusers path. `model_type` is now inferred from
`architectures`, and `--config` accepts a local directory.

Verified against real weights on H200. ESM-2 (facebook/esm2_t6_8M_UR50D, MIT)
and ProtBert (Rostlab/prot_bert) reproduce HuggingFace to a relative L2 error of
1.2e-4 and 1.7e-5 on human haemoglobin, insulin, and lysozyme; the larger CUDA
gap is TF32 and disappears with use_tf32=0. Padding invariance, batch-order
invariance, and pad-token isolation are bit-exact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
(cherry picked from commit d6c90f199f5b58a253a18414573d7b2999f2d6f5)
Pin validation to the final ONNX GenAI metadata commit, update encoder embedding profiles and auxiliary inputs to the canonical schema, and assign stable symbols to anonymous dynamic dimensions so generated packages validate without null shapes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Refresh PR #478 on the current Mobius main branch without rewriting its history.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Let schema_version govern workflow syntax and let each ONNX artifact remain authoritative for its opset imports. Regenerate every producer fixture and pin cross-repository validation to the corresponding ONNX GenAI schema commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Correct the producer documentation to keep ONNX port contracts and opsets artifact-owned, retain semantic roles and state aliases, and support independently shaped K/V tensors across layers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Advance the cross-repository conformance pin to the ONNX GenAI revision containing the finalized heterogeneous KV design documentation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
@justinchuby
justinchuby marked this pull request as ready for review August 21, 2026 21:01
@justinchuby
justinchuby merged commit 57c149d into main Aug 21, 2026
19 of 23 checks passed
@justinchuby
justinchuby deleted the justinchuby/simplify-composite-metadata-producer branch August 21, 2026 21:01
cancel-in-progress: true

jobs:
onnx-genai-metadata:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Remove or made separate

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.

6 participants