Skip to content

feat(weight-free): weight-free ONNX export for causal LMs (dynamo path) - #1

Open
amarquic wants to merge 26 commits into
smedhe:enable_dynamo_for_causallmfrom
amarquic:enable_weightfree_for_causallm
Open

feat(weight-free): weight-free ONNX export for causal LMs (dynamo path)#1
amarquic wants to merge 26 commits into
smedhe:enable_dynamo_for_causallmfrom
amarquic:enable_weightfree_for_causallm

Conversation

@amarquic

Copy link
Copy Markdown

Summary

This PR adds weight-free ONNX export support for causal LMs on top of Sharvari's dynamo export branch.

Weight-free export exports the ONNX graph with no embedded weights. Weights are stored in the original safetensors checkpoint and loaded by the QAIC compiler at compile time via weight_spec.json. This enables:

  • Near-zero RAM during export (model on meta device)
  • Reuse of the same ONNX across dtype/quantization variants
  • Faster re-compilation without re-exporting

Key changes

New infrastructure:

  • exporter/weight_free.py — core weight-free dynamo export: meta model trace, promotes initializers to external data, builds weight_spec.json
  • exporter/weight_spec.py — WeightSpec format: maps ONNX initializer names → safetensors checkpoint keys
  • exporter/checkpoint_transforms.py — checkpoint preparation pipeline: MoE expert stacking, dtype conversion, MXFP4 dequant, auto .bin→safetensors conversion

Base class wiring:

  • base/modeling_qeff.py_export(): use_weight_free_export flag, meta device guard, tmp ONNX path, weight_spec embedding in ONNX metadata
  • transformers/models/modeling_auto.pyQEFFAutoModelForCausalLM: _checkpoint_transforms, CCL Dim fix, use_weight_free_export through compile chain

Model-level fixes for dynamo tracing:

  • Llama, Qwen3, Phi3, Qwen3-MoE, GLM4-MoE, Grok-1, GPT-OSS, GPT2, Falcon — device alignment, meta device guards, rotary embedding fixes
  • cache_utils.pydevice=position_ids.device on all torch.arange calls

Infrastructure fixes:

  • base/onnx_transforms.pyRewriteUnsupportedOpsTransform (SplitToSequence→Split, CastLike→Cast)
  • utils/export_utils.py_RetainedState rename guard for dynamo path
  • utils/torch_patches.pytemporarily_enable/disable_nested_compile_regions
  • customop/ — backward stubs, CtxGatherCB shape fix, GemmaRMSNorm runtime offset

Examples:

  • examples/text_generation/weight_free_export_verify.py — single-model smoke-test
  • examples/text_generation/continuous_batching_weightfree.py — weight-free + CB
  • examples/performance/compute_context_length/weight_free_ccl_verify.py — weight-free + CCL

Validated models

Qwen3-30B-A3B, Qwen3-MoE, GPT-OSS, GPT2, Gemma-2-2B, Gemma-7B, Llama-3.2-1B, Phi3, GLM4-MoE

Usage

from accelerate import init_empty_weights
with init_empty_weights():
    meta_model = AutoModelForCausalLM.from_config(config)
model = QEFFAutoModelForCausalLM(meta_model, pretrained_model_name_or_path=model_name)
model.compile(use_dynamo=True, use_weight_free_export=True, ctx_len=4096, ...)

Test commands run

python3 examples/text_generation/weight_free_export_verify.py --model_name Qwen/Qwen3-30B-A3B-Instruct-2507
python3 examples/text_generation/weight_free_export_verify.py --model_name google/gemma-2-2b
python3 examples/text_generation/weight_free_export_verify.py --model_name meta-llama/Llama-3.2-1B
python3 examples/text_generation/continuous_batching_weightfree.py --model_name Qwen/Qwen3-30B-A3B-Instruct-2507

amarquic added 19 commits July 13, 2026 13:13
…sal LMs

New files:
- exporter/weight_free.py: core weight-free dynamo export — builds meta model,
  traces with FakeTensors, promotes ONNX initializers to external data,
  builds weight_spec.json mapping ONNX names to safetensors checkpoint keys
- exporter/weight_spec.py: WeightSpec format — describes external weight files
  and tensor→checkpoint-key mappings consumed by the QAIC compiler
- exporter/checkpoint_transforms.py: checkpoint preparation pipeline —
  DtypeConversionCheckpointTransform, MoEExpertStackingCheckpointTransform
  (stacks per-expert tensors into batched layout), and
  GptOssMxfp4ExpertDequantSplitCheckpointTransform (dequant MXFP4 experts)
- exporter/__init__.py: export new checkpoint transform classes
- scripts/preprocess_moe_checkpoint.py: standalone MoE checkpoint prep script
- examples/text_generation/weight_free_export_verify.py: e2e weight-free
  export verification (export → ORT → parity check)
- examples/text_generation/weight_free_export_simple.py: minimal weight-free
  export example for causal LMs

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
- gemma.py, gemma2.py: align cos/sin device for dynamo tracing
- glm4_moe.py: __qeff_init__ uses meta-device guard for weight-free;
  register gate_proj/up_proj/down_proj_t directly on self.experts;
  remove get_all_projections() helper; fix rotary_dim computation
- granite.py: full_like for causal mask; device alignment
- granitemoe.py: weight-free subfunction export support
- grok1.py: replace bool-mask expert loop with gather-BMM pattern;
  add __qeff_init__ stacking per-expert weights; meta-device guard
- llama.py: fix decoder layer for dynamo — buffers, nested compile regions,
  rotary signature alignment
- phi.py, phi3.py: QEffPhi3MLP to avoid SplitToSequence from chunk(2);
  device alignment fixes
- qwen2.py: cos/sin device alignment
- qwen3.py: extend qeff_apply_rotary_pos_emb signature and align cos/sin
- qwen3_moe.py: weight-free dynamo export; expert weight stacking with
  meta-device guard; RAM reduction via view aliases (no clone)

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…xport()

- _export(): add use_weight_free_export flag; pop mla_absorption/
  prefill_seq_len/ctx_len from export_kwargs; resolve weight_spec_path;
  guard _model_offloaded_check() — skip for meta-device weight-free models;
  set up tmp_onnx_dir; route to export_weight_free_onnx() when flag is set;
  use effective_transform_owner so weight-free model's transform list applies;
  embed weight_spec JSON in ONNX metadata; add finally cleanup of tmp dir;
  create symlink to prepared checkpoint directory after export
- get_onnx_path(): add use_weight_free_export and mla_absorption params;
  pass use_weight_free_export through to _export() kwargs
- _compile(): add use_weight_free_export param; pass through to get_onnx_path()

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…path

Rewrites SplitToSequence→Split, CastLike→Cast, and aten_split/aten_getitem
call patterns emitted by the dynamo exporter. Required for models using
.chunk() (e.g. Phi3 SwiGLU MLP) that produce QAIC-unsupported ops.

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…orCausalLM

- Add Any import; add checkpoint transform imports
- Add _checkpoint_transforms to QEFFAutoModelForCausalLM with
  GptOssMxfp4ExpertDequantSplitCheckpointTransform, MoEExpertStackingCheckpointTransform,
  DtypeConversionCheckpointTransform in priority order
- Set _onnx_transforms = [RewriteUnsupportedOpsTransform]
- Add use_weight_free_export param to export() and compile(); pass it
  through to _export() so the weight-free path is accessible via the
  standard compile(use_weight_free_export=True) call
- Build dynamic_shapes for weight-free path same as dynamo path

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…eight-free

- Add device=position_ids.device to all torch.arange calls in cache update
  methods so meta-tensor device matches during weight-free subfunction export
- Replace torch.tensor(0.0, dtype=float32) with torch.zeros_like() in
  invalid-mask branches for correct shape inference under FakeTensor tracing
- Add QEffHybridChunkedCache utility methods needed for weight-free export

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
- ctx_scatter_gather.py, ctx_scatter_gather_cb.py: add no-op backward()
  stubs to all CtxScatter/Gather autograd Functions so torch.export can
  trace through the autograd graph structure during dynamo export
- rms_norm.py: replace QEffGemmaRMSNorm.__qeff_init__ weight mutation with
  a runtime forward() that applies weight + 1.0 via select_interface; the
  __qeff_init__ copy_ fails on meta-device during weight-free export since
  meta tensors cannot hold data

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…dynamo path

- _setup_onnx_subfunctions: apply torch patches only on TorchScript path
  (not dynamo); resolve target_classnames from get_submodules_for_export
  at the call site for dynamo vs inside the function for TorchScript
- _cleanup_onnx_subfunctions: undo torch patches only when they were applied
- _RetainedState rename block: skip for dynamo path so
  PreserveNestedCacheRetainedStateTransform can locate dangling outputs

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…gions

- temporarily_enable_nested_compile_regions: wraps selected module forward()
  methods with @nested_compile_region at export time so dynamo materializes
  repeated decoder block functions (used by weight-free subfunction export)
- temporarily_disable_nested_compile_regions: unwraps nested_compile_region-
  decorated forwards before export to avoid premature subgraph boundaries
  (used by weight-free non-subfunction path)

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
ctx_indices is (batch, 1, ctx_len) — use dim -1 not dim 1 to get ctx_len.

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…o path

get_sampling_inputs_and_outputs now accepts and populates dynamic_shapes
alongside dynamic_axes, building Dim entries for each sampler input so
the dynamo export path gets consistent symbolic shapes for samplers.

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…ee export

- Add Phi3MLP → QEffPhi3MLP to KVCacheTransform: QEffPhi3MLP wraps the MLP
  to avoid SplitToSequence from chunk(2) in dynamo ONNX, which QAIC does
  not support
- Wire QEffGrok1MoeBlock.__qeff_init__ into KVCacheExternalModuleMapperTransform:
  stacks per-expert weights during init so the gather-BMM forward works
  correctly for weight-free export

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
In this branch the function lives in compile/mdp_generator.py not
utils/_utils.py — importing it via utils/__init__.py caused ImportError.

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…calls

Prevents meta/cpu device mismatch during weight-free export tracing.

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…arse_moe key mapping

checkpoint_transforms.py:
- Add _convert_bin_to_safetensors(): auto-converts .bin checkpoints to safetensors
  on first use via save_pretrained(safe_serialization=True); idempotent
- CheckpointTransformPipeline.apply(): detect .bin files and convert before transform
- MoEExpertStackingCheckpointTransform: extend EXPERT_RE and _LayerStacker.add() to
  handle Mixtral w1/w3/w2 naming (w1→gate, w3→up, w2→down)

weight_free.py:
- _resolve_checkpoint_dir(): fall back to downloading .bin files when no safetensors
  exist on HuggingFace Hub (e.g. GPT-J-6B); CheckpointTransformPipeline auto-converts
- _find_checkpoint_key(): add lookup 5 to bridge mlp↔block_sparse_moe rename —
  transformers 5.x renamed the MoE block attribute, ONNX uses 'mlp' but Mixtral
  checkpoint has 'block_sparse_moe'

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…, Mistral, Mixtral, OLMo2

falcon/modeling_falcon.py:
- QEffFalconModel.__qeff_init__: meta device guard for sin/cos — skip storing
  as nn.Parameter on meta device to avoid meta tensor ONNX initializer failure

gpt_oss/modeling_gpt_oss.py:
- qeff_apply_rotary_pos_emb: add device/dtype alignment for weight-free tracing
- ctx_indices/short_read_idx: add device=position_ids.device to torch.arange

granitemoe/modeling_granitemoe.py:
- QEffGraniteMoeAttention.__qeff_init__: set num_heads/num_key_value_heads as
  instance attributes — required by @nested_compile_region inside decoder layer
- past_key_value_update: fix 3-tuple unpack to 4-tuple
- eager_attention_forward: use full_like for causal mask to avoid config.torch_dtype
- QEffGraniteMoeTopKGating: drop .to(dtype) after softmax (SoftMax+Cast → QAIC crash)
- QEffGraniteMoeDecoderLayer: return hidden_states directly instead of tuple

mistral/modeling_mistral.py:
- qeff_apply_rotary_pos_emb: add device/dtype alignment

mixtral_moe/modeling_mixtral.py:
- QEffMixtralExperts.__qeff_init__: meta device guard for gate/up/down projections
- QEffMixtralModel.__qeff_init__: drop attention_scaling multiply (creates new
  tensor that becomes top-level ONNX initializer; direct reference avoids it)
- QeffMixtralDecoderLayer: add @torch.compiler.nested_compile_region
- QEffMixtralExperts: add to KVCacheTransform so __qeff_init__ is called
- eager_attention_forward: use full_like for causal mask

olmo2/modeling_olmo2.py:
- qeff_apply_rotary_pos_emb: add device/dtype alignment

pytorch_transforms.py:
- Wire QEffMixtralExperts.__qeff_init__ (MixtralExperts→QEffMixtralExperts mapping)
- Wire QEffGraniteMoeAttention.__qeff_init__ (already mapped, __qeff_init__ added)

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
examples/text_generation/weight_free_export_verify.py:
- Single-model smoke-test: weight-free dynamo export → compile → QPC inference
- --no_weight_free: regular from_pretrained + dynamo path
- --layers: override num_hidden_layers for fast testing
- Config loaded without trust_remote_code first (fixes Falcon legacy config)

examples/text_generation/continuous_batching_weightfree.py:
- Single-model weight-free export with continuous batching (full_batch_size)
- Same config fix for legacy models like Falcon

examples/performance/compute_context_length/weight_free_ccl_verify.py:
- Single-model CCL verification: compile with comp_ctx_lengths_prefill/decode
- Prints specializations.json to confirm CCL slots were compiled
- Default: ctx_len=4096, CCL=[1024,2048,4096], weight-free + dynamo

Signed-off-by: amarshar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
@amarquic
amarquic force-pushed the enable_weightfree_for_causallm branch from b5dca6a to 03ce067 Compare July 13, 2026 07:45
amarquic added 5 commits July 13, 2026 13:23
…fter rebase

The rebase auto-merge dropped the if not use_dynamo: guard introduced in
Sharvari's d6f55bf commit. Restore it so TorchScript patches are not
applied on the dynamo export path, and restore use_dynamo in the state
dict so _cleanup_onnx_subfunctions can check it.

Signed-off-by: Amar <amarshar@qti.qualcomm.com>
- Attention softmax: remove .to(query.dtype) — emits SoftMax->Cast in the
  subfunction ONNX which triggers optimizeSoftmax assertion in QAIC compiler
- Router softmax: replace torch.softmax with manual Exp/Sum/Div — with
  num_local_experts=8, top_k_gates has 8 Einsum consumers; optimizeSoftmax
  asserts when a SoftMax node has more than one consumer

Signed-off-by: Amar <amarshar@qti.qualcomm.com>
- cache_utils.py: restore missing return k_out, v_out in QEffHybridCache.update()
  and QEffSlidingWindowCache.update(); remove orphan method blocks that were
  left floating inside wrong classes after rebase
- exporter/__init__.py: use explicit re-export syntax (X as X) for F401
- checkpoint_transforms.py, glm4_moe, grok1, preprocess_moe_checkpoint.py:
  rename ambiguous variable I to ffn_dim / hidden_dim (E741)
- gemma, gemma2, qwen3: remove unused mask_value assignment (F841)
- Run ruff format across all 39 changed files

Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Creates tests/dynamo/ following the same structure as PR quic#1174:
  - conftest.py: markers, dynamo_workdir fixture, xdist-safe result capture,
    HTML/JSON report generation
  - utils/hash_manager.py: blake2b-keyed workdirs (collision-safe under -n auto)
  - utils/hardware_manager.py: file-lock QAIC device pool for HW lanes
  - utils/report_generator.py: HTML + JSON coverage matrix output

Extends DynamoModelSpec in model_registry.py with two new flags:
  weight_free_supported     - True for all validated causal LM architectures
  weight_free_cb_supported  - True for all architectures where CB is also verified
  Both flags False for VLM text-side models (out of scope for this PR)

Adds tests/dynamo/causal_lm/test_weight_free.py with 4 CPU-only lanes:
  1. test_weight_free_export        - meta-device export produces ONNX + weight_spec.json
  2. test_weight_free_no_embedded_weights - ONNX has no large embedded initializers
  3. test_weight_free_spec_coverage - all spec keys resolve in prepared_checkpoint/
  4. test_weight_free_cb_export     - continuous_batching=True export produces artifacts

All lanes are @pytest.mark.dynamo_export (CPU, no QAIC hardware) and
@pytest.mark.regular. Run with:
  pytest tests/dynamo/causal_lm/test_weight_free.py -m 'dynamo_export' -n auto

Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…+ cleanup

- exporter/weight_free/transforms.py: add MoEFusedExpertSplitCheckpointTransform
  for MoE checkpoints that already store experts in fused/stacked form
  (gate_up_proj [E,2I,H] + down_proj [E,H,I]). Splits them into the derived
  layout that QEff ONNX initializers expect (gate_proj/up_proj/down_proj_t).
  Registered in pipeline between MoEExpertStackingCheckpointTransform (old
  per-expert format) and DtypeConversionCheckpointTransform (dense fallback).

- modeling_auto.py: register MoEFusedExpertSplitCheckpointTransform in
  QEFFAutoModelForCausalLM._checkpoint_transforms pipeline

- customop: remove backward() stubs from CtxScatter/Gather — not needed
  for dynamo weight-free export (verified by test)

- exporter/weight_free/core.py: fix sys.path for scripts/memory_profiling
  (extra .parent for new subdirectory depth)

- docs/source/weight_free_export.md: add weight-free export documentation

- examples/text_generation/weight_free/: update docstring paths and add
  __init__.py

Signed-off-by: Amar <amarshar@qti.qualcomm.com>
@smedhe
smedhe force-pushed the enable_dynamo_for_causallm branch 3 times, most recently from b24dc14 to f4e951b Compare July 16, 2026 13:42
amarquic added 2 commits July 17, 2026 16:35
Qwen3-MoE stores the router weight as mlp.router.weight in the checkpoint
but the QEff wrapper accesses it as mlp.gate.weight. Add lookup strategy 6
to _find_checkpoint_key to handle this rename in both directions so the
weight is correctly promoted to an external reference instead of remaining
as an embedded meta tensor that causes SerdeError.

Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…pipeline

- core.py: use config.dtype (not hardcoded float32) for meta model cast and
  checkpoint conversion; getattr fallback handles dtype=None configs
- core.py: propagate enable_proxy flag through _build_meta_qeff_model
- cache_utils.py: use v_out.dtype instead of torch.float32 for invalid-mask
  zeros_like in 5 KV cache update paths — avoids fp32↔bf16 dtype mismatch
- modeling_llama.py: use attn_weights.dtype for masked_fill and softmax
  upcast instead of hardcoded float32
- export_compile_infer.py: add --dtype flag (default float16) to control
  model weight dtype; add enable_proxy=False
- modeling_qeff.py: skip SplitTensorsTransform for weight-free export

Signed-off-by: Amar <amarshar@qti.qualcomm.com>
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.

1 participant