Enabling Dissag Infra for weightfree + TODO addressed - #1345
quic-amitraj wants to merge 15 commits into
Conversation
| del initializers[name] | ||
|
|
||
|
|
||
| def _prepared_checkpoint_hash( |
There was a problem hiding this comment.
Include moe_prefill_num_pipeline_stages and
moe_prefill_num_parallelized_experts here. Two expert_parallel
exports of the same model and dtype but different P/E values
produce different packed tensor shapes, but this key sends
both to the same prepared directory and manifest, so the
second export can silently reuse incompatible weights.
| _ID_MAP = { | ||
| "moe_expert_stacking_v1": MoEExpertStackingCheckpointTransform, | ||
| "gptoss_mxfp4_dequant_v1": GptOssMxfp4ExpertDequantSplitCheckpointTransform, | ||
| "fused_expert_split_v1": FusedExpertSplitCheckpointTransform, | ||
| "moe_fused_expert_split_v1": FusedExpertSplitCheckpointTransform, | ||
| "granite_moe_fused_split_v1": FusedExpertSplitCheckpointTransform, | ||
| "dtype_conversion_v1": DtypeConversionCheckpointTransform, | ||
| } |
There was a problem hiding this comment.
Add the expert-parallel transform IDs here, even if
they resolve to the unconfigured base classes for key lookup.
The manifest can now record moe_expert_parallel_stacking_v1 or
gptoss_mxfp4_dequant_expert_parallel_v1, but this lookup
cannot resolve either, so
promote_initializers_and_build_spec() loses the active
transform and per-expert .mlp. ONNX names no longer map
to .block_sparse_moe. checkpoint keys.
| if len(scatter_nodes) != 2: | ||
| logger.debug( | ||
| "PreserveNestedCacheRetainedStateTransform: function '%s' has %d scatter node(s), expected 2 — skipping.", | ||
| node.op_type, | ||
| len(scatter_nodes), | ||
| ) | ||
| continue |
There was a problem hiding this comment.
This condition doesn't make sense for prefill moe if you see this hardcoded the number of scatter node to 2 but for prefill with expert parallel, we have more than 2 operator ctx_nodes then it will not apply this onnx transform and which raise compiler error. Now even if we remove this condition below
scatter_nodes.sort(key=cls._scatter_sort_key) # Only the first two scatter outputs map to key / value respectively. scatter_outputs = [n.output[0] for n in scatter_nodes[:2]]
we sort and select top 2 which is required for the retain_state renaming which work perfectly fine. So the condition was redundant previously.
There was a problem hiding this comment.
there are some models that might have single KV instead of two like deepseek there we want only first scatter output to map to KV cache for MLA. We may need to handle that when we enable deepseek models. Can you put this as a TODO here.
| # NOTE: expand_as(...) instead of torch.full_like(...) is the compiler-preferred | ||
| # workaround for ConstantOfShape(INT32_MAX); both produce identical traced Ctx ops. | ||
| matched_idx = int32_max_scalar.expand_as(token_idx) | ||
| matched_idx = int32_max_scalar |
There was a problem hiding this comment.
I think this was intentional, did you check the NOTE? why remove this?
There was a problem hiding this comment.
revert the changes. I was changing the torch.tensor() to torch.full_like() that was the follow up changes. I reverted this .
ochougul
left a comment
There was a problem hiding this comment.
PR 1345 Checkpoint Pipeline Refactor Plan
Goal
Replace the current checkpoint-transform selector/consumed-key model with a real planning and execution pipeline for
weight-free checkpoint preparation.
The design must support:
- multiple transforms applied to the same logical tensor
- transforms that require a group of keys before they can run
- one source read path and one final write path where possible
- bounded RAM usage while still parallelizing independent work
- centralized checkpoint finalization: sidecars, index, manifest, sentinel
Current Problem
PR 1345 changes the old selector into a staged flow, but it still uses a consumed set:
for transform in transforms_to_run:
remaining = {k: v for k, v in weight_map.items() if k not in consumed}
result = transform.apply(src, out, target_dtype=target_dtype, weight_map=remaining)
consumed.update(transform.get_consumed_keys(weight_map))This means each raw key is effectively owned by one transform. That cannot model cases where the same logical tensor
needs multiple stages:
raw expert keys -> stack/dequant/split -> expert_parallel pack -> dtype/final output
The current workaround is to create special combined transform subclasses, but that does not scale and does not fully
address the pipeline TODO.
Target Model
Use a two-phase design:
-
Planning phase
- inspect
weight_map, model config,hash_params, and target dtype - build explicit checkpoint tasks
- identify dependencies between tasks
- estimate peak RAM per task
- derive the prepared-checkpoint cache key from the full plan
- inspect
-
Execution phase
- run ready tasks through a shared thread pool
- submit tasks only when estimated RAM budget allows
- load required tensors once for the task
- run all compatible transform stages in memory
- write final tensors once
- release tensors immediately
- collect final
weight_map
Core Types
Add small internal planning types, probably in QEfficient/base/checkpoint_transforms.py or a nearby internal module.
from dataclasses import dataclass
@dataclass(frozen=True)
class TensorRef:
key: str
stage: str
@dataclass(frozen=True)
class TaskParams:
transform_id: str
values: tuple[tuple[str, object], ...]
@dataclass
class CheckpointTask:
task_id: str
input_refs: tuple[TensorRef, ...]
output_refs: tuple[TensorRef, ...]
source_files: tuple[str, ...]
output_file: str
estimated_peak_bytes: int
params: TaskParams
def run(self, src: Path, out: Path, target_dtype: torch.dtype) -> dict[str, str]:
...TensorRef.stage is important. It distinguishes:
("model.layers.0.moe_weights.gate", "stacked")
("model.layers.0.moe_weights.gate", "packed")
("model.layers.0.moe_weights.gate", "final")
Without staged tensor refs, a plain key-to-transform map cannot represent multiple transforms on one logical key.
Transform Contract
Replace or supplement the current apply() / get_consumed_keys() contract with planning:
class BaseCheckpointTransform:
TRANSFORM_ID: str = ""
@classmethod
def plan_tasks(
cls,
weight_map: dict[str, str],
config,
hash_params: dict,
target_dtype: torch.dtype,
) -> list[CheckpointTask]:
...Transforms should declare task groups. The pipeline should own scheduling, loading, writing, and finalization.
Task Shapes
Dense / Base Dtype Conversion
Use independent shard-level or key-group tasks:
input: raw base keys from one shard
output: final same keys
run: load -> dtype convert if needed -> write base shard
These tasks can run immediately and in parallel, subject to RAM budget.
Per-Expert MoE Stacking
Use one grouped task per layer:
input: all expert projection keys for that layer
output: moe_weights.gate, moe_weights.up, moe_weights.down
run: load all required expert tensors -> stack -> optional pack -> dtype/final -> write one layer shard
The task must validate that all required experts/projections are present before execution.
GPT-OSS MXFP4 Dequant + Split
Use one grouped task per layer:
input: gate_up/down blocks, scales, optional biases
output: canonical moe_weights.*
run: load blocks/scales/biases -> dequant -> split -> optional pack -> dtype/final -> write one layer shard
Fused Expert Split
Use one grouped task per layer or per fused tensor prefix:
input: gate_up_proj, down_proj, optional biases
output: canonical moe_weights.*
run: load fused tensors -> split -> optional pack -> dtype/final -> write final shard
Expert-Parallel Packing
Do not write this as a separate intermediate disk stage.
When hash_params["moe_prefill_flavour"] == "expert_parallel", fold packing into the grouped task execution after
stack/dequant/split and before final write:
raw group -> canonical moe_weights -> expert_parallel packed moe_weights -> final write
This avoids:
- writing temporary stacked shards
- reading those shards again for packing
- growing disk usage
- creating cleanup/staleness problems
Memory-Bounded Execution
Add a scheduler that accepts planned tasks and a memory budget.
Default budget:
max_ram_bytes = int(available_ram_gb() * 0.8 * 1024**3)Allow an internal override later if needed, but keep the first implementation simple.
Scheduler behavior:
pending = list(tasks)
running = {}
available_refs = set(raw_refs)
active_bytes = 0
while pending or running:
for task in ready_tasks(pending, available_refs):
if active_bytes + task.estimated_peak_bytes <= max_ram_bytes:
future = executor.submit(task.run, src, out, target_dtype)
running[future] = task
active_bytes += task.estimated_peak_bytes
pending.remove(task)
completed_future = wait_for_one_future(running)
task = running.pop(completed_future)
active_bytes -= task.estimated_peak_bytes
final_weight_map.update(completed_future.result())
available_refs.update(task.output_refs)If one task exceeds the cap, fail early:
raise ValueError(
f"Checkpoint transform task {task.task_id} requires at least {required_gb:.2f} GB, "
f"but max checkpoint transform RAM is {limit_gb:.2f} GB."
)The cap must be allowed to be smaller than total model size, but not smaller than the largest indivisible grouped task.
Intermediate Tensor Policy
Do not write intermediate tensors by default.
Within a task:
load raw tensors
run all applicable transform stages in memory
write final output shard
release tensors
Only introduce spill-to-disk later if a real model needs it and the failure mode is understood.
Cache Key
The prepared checkpoint hash should be derived from the plan, not only from loose top-level fields.
Include at least:
- source model ref or resolved source path
- source checkpoint fingerprint
- target dtype
- task transform IDs
- task parameters
- input keys
- output keys
- output file naming
- expert-parallel parameters:
moe_prefill_flavourmoe_prefill_num_pipeline_stagesmoe_prefill_num_parallelized_expertsmoe_prefill_expert_parallel_chunk_sizeif it affects layout/output
This fixes the incomplete TODO-6 behavior where two expert_parallel layouts can collide in the same prepared directory.
Pipeline Responsibilities
CheckpointTransformPipeline.apply() should own:
- source format validation
- reading the original
weight_map - building the plan
- checking cache manifest
- clearing stale output
- invoking the memory-bounded executor
- writing final
model.safetensors.index.json - copying sidecars
- writing manifest
- touching sentinel
Transform classes should not independently write index, manifest, sidecars, or sentinel.
Manifest
Bump or extend the manifest to include:
- manifest version
- source path
- target dtype
- source file fingerprint
- plan fingerprint
- active transform IDs
- transform parameters
- final output files
The manifest should be strict enough that a changed packing layout, transform parameter, or source file invalidates the
prepared directory.
Migration Steps
- Add
TensorRef,CheckpointTask, and planning helpers. - Add a memory-bounded task executor.
- Implement dense dtype conversion as planned base shard tasks.
- Convert per-expert MoE stacking to layer-level grouped tasks.
- Convert GPT-OSS MXFP4 dequant/split to layer-level grouped tasks.
- Convert fused expert split to grouped tasks using canonical key mapping.
- Fold expert-parallel packing into the relevant grouped tasks instead of using separate combined subclasses.
- Move all finalization to
CheckpointTransformPipeline.apply(). - Derive prepared checkpoint hash from the plan fingerprint.
- Remove or update the stale selector TODO once the new pipeline supports composable stages.
Validation
Use existing weight-free tests where possible. Add focused cases under existing test files rather than creating broad new
test files.
Minimum tests:
- dense checkpoint still performs dtype conversion and writes a valid index
- per-expert MoE stacking writes canonical final keys
- GPT-OSS MXFP4 dequant/split writes canonical final keys
- fused expert split handles Mixtral-style and GraniteMoE-style keys
- expert_parallel with different P/E values produces different prepared hashes
- one logical key can pass through multiple stages before final write
- grouped task waits until all required keys are present
- missing required grouped key fails before writing a partial prepared checkpoint
- memory budget limits concurrent grouped tasks
- one task above memory cap raises a clear error
Acceptance Criteria
- No transform owns global finalization.
- No intermediate checkpoint files are written for normal stack/dequant/split/pack flows.
- Final output tensors are written once.
- Final
weight_mapcontains only valid final tensor keys and existing shard files. - Cache identity changes when transform layout or expert_parallel packing parameters change.
- The pipeline can represent multiple transforms on one logical key without hardcoded combined subclasses.
read_weight_map() (checkpoint_utils.py):
- Remove shard-scan fallback that opened every shard file just to
reconstruct an index already available in model.safetensors.index.json.
- Single-file model (exactly 1 shard, no index): open once and return.
- Zero safetensors: FileNotFoundError with clear message.
- Multiple shards, no index: FileNotFoundError identifying malformed
checkpoint — callers no longer silently scan on broken inputs.
CheckpointTransformPipeline.apply() (base/checkpoint_transforms.py):
- Drop silent .bin → safetensors rewrite which doubled I/O and disk
usage with no user visibility.
- Raise ValueError immediately when .bin files are present so callers
know exactly what to fix.
- Remove now-unused convert_bin_to_safetensors import.
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…(TODO-6)
base/checkpoint_transforms.py:
- Add detect_group_transform_id(config, weight_map, hash_params) -> str.
Uses config.num_experts as the MoE gate and inspects weight_map key
patterns to identify the checkpoint layout without opening any shard
files. Returns stable TRANSFORM_ID strings that will map 1:1 to the
group transform classes introduced in the pipeline redesign.
- Simplify _checkpoint_manifest(): remove original_source field (source_dir
always equals src after Stage ①), add active_group field, bump version to 2.
- Simplify _checkpoint_file_fingerprint(): drop label parameter.
- Simplify _clear_stale_prepared_dir(): drop source_dir parameter.
- Read weight_map once at the top of apply() so detection and transform
selection share the same dict.
export.py:
- Add _prepared_checkpoint_hash(model_ref, dtype, active_group_id,
moe_flavour) -> 12-char sha256. Different model flavours (dense vs MoE,
decode vs expert_parallel, different quantizations) now hash to different
prepared directories and never silently overwrite each other.
- _prepare_checkpoint_for_weight_free_export() detects the active group
transform upfront, computes the hash, and uses it in prepared_name instead
of the plain dtype suffix.
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…DO-5)
- Pipeline passes weight_map to transform.apply() so transforms never
re-read the index themselves.
- MoEExpertStackingCheckpointTransform: replace Phase 1 ThreadPoolExecutor
shard scan with O(N) dict iteration on the passed weight_map.
- GptOssMxfp4ExpertDequantSplitCheckpointTransform: same.
- MoEFusedExpertSplitCheckpointTransform: replace inline index re-read
duplication (TODO-5) with read_weight_map() fallback.
- GraniteMoeFusedExpertSplitCheckpointTransform: same pattern.
All four transforms accept weight_map as an explicit Optional parameter
and fall back to read_weight_map(src) only when called without it.
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…n (TODO-1)
detect_group_transform() returns the active layout transform class directly
(not a string ID), eliminating the is_applicable() selector loop.
Pipeline execute logic:
transforms_to_run = [active_layout_transform, DtypeConversionCheckpointTransform]
for each: apply(remaining_keys), collect result, shrink consumed set
Each layout transform (MoEExpertStacking, GptOssMxfp4Dequant,
MoEFusedExpertSplit) gains:
+ TRANSFORM_ID: stable cache hash identifier
+ get_consumed_keys(): declares which keys it owns
- Phase 3 removed: base key dtype conversion deleted from layout transforms
- sentinel.touch(), write_index(), copy_checkpoint_aux_files() removed
- apply() now returns Dict[str, str] instead of bool
DtypeConversionCheckpointTransform gains TRANSFORM_ID + get_consumed_keys()
and always runs on remaining keys after any layout transform.
Stage ⑤ (finalise): sidecar copy, write_index, manifest, sentinel now
handled once in the pipeline after all transforms complete.
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
…roach
Replaces MoEFusedExpertSplitCheckpointTransform and the now-deleted
GraniteMoeFusedExpertSplitCheckpointTransform with one class:
FusedExpertSplitCheckpointTransform:
_get_key_remap(weight_map): detects architecture from index.json keys.
input_linear.weight present → GraniteMoE remap
otherwise → Mixtral fused (already canonical, no remap)
build_canonical_maps(weight_map, remap) produces:
canonical_index: {canonical_key → shard_file} WHERE to find tensor
key_translation: {canonical_key → actual_key} WHAT to ask shard for
Single pass — loads by actual key, writes by canonical output key.
No shape reads — split dim determined from bias key presence in canonical_index.
_checkpoint_transforms now has 4 entries; no architecture-specific subclasses.
detect_group_transform() delegates pre-stacked detection to is_applicable().
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Each layout transform now declares resolve_onnx_key(onnx_key, index):
MoEExpertStackingCheckpointTransform: direct + .mlp.→.block_sparse_moe.
FusedExpertSplitCheckpointTransform: direct + .mlp.→.block_sparse_moe.
GptOssMxfp4ExpertDequantSplit: direct match only
find_checkpoint_key() updated:
1. Universal HF prefix rules (unchanged)
2. active_transform.resolve_onnx_key() — explicit per-architecture
3. Legacy MoE alias fallback (kept for old checkpoints)
Hardcoded .mlp./.gate.weight/.router.weight rules removed from resolver.
promote_initializers_and_build_spec() detects active_transform from
qeff_model and passes it to find_checkpoint_key().
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Each layout transform now declares resolve_onnx_key(onnx_key, index):
MoEExpertStackingCheckpointTransform: direct + .mlp.→.block_sparse_moe.
FusedExpertSplitCheckpointTransform: direct + .mlp.→.block_sparse_moe.
GptOssMxfp4ExpertDequantSplit: direct match only
find_checkpoint_key() updated:
1. Universal HF prefix rules (unchanged)
2. active_transform.resolve_onnx_key() — explicit per-architecture
3. Legacy MoE alias fallback (kept for old checkpoints)
Hardcoded .mlp./.gate.weight/.router.weight rules removed from resolver.
promote_initializers_and_build_spec() detects active_transform from
qeff_model and passes it to find_checkpoint_key().
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Bug 1: Old MoEFusedExpertSplitCheckpointTransform and GraniteMoeFusedExpertSplitCheckpointTransform class definitions were never removed after Stage ⑤ aliased them to FusedExpertSplitCheckpoint Transform. The redefinitions overrode the aliases, restored the old bool return type, and broke the Stage ④ pipeline contract. Deleted. Bug 2: FusedExpertSplitCheckpointTransform.get_consumed_keys() returned canonical keys (experts.gate_up_proj) but the pipeline's remaining computation filters against original weight_map keys (input_linear.weight for GraniteMoE). Mismatch meant DtypeConversion would re-process expert keys. Fixed to iterate original keys and map to canonical for matching. Signed-off-by: Amar <amarshar@qti.qualcomm.com>
- find_checkpoint_key(): add lm_head.weight → wte.weight fallback for
GPT-family models where lm_head is not stored in the checkpoint.
- checkpoint_key_resolver: read active_group TRANSFORM_ID from prepared
checkpoint manifest (.checkpoint_prepared.json) instead of re-running
detect_group_transform() on the prepared weight_map (which has output
keys, not the original expert keys).
- GptOssMxfp4ExpertDequantExpertParallelCheckpointTransform: new transform
for GptOss MXFP4 with expert_parallel prefill — dequant + repack
[E,H,I] → [E/P,P,H,I] via pack_moe_weights_for_expert_parallel().
- detect_group_transform(): use config.model_type == 'gpt_oss' to route
GptOss (cleaner than _blocks key scan), with expert_parallel routing.
- _load_prepared_tensors: scan shards when index.json is absent (needed
for unit tests that call transforms directly without the pipeline).
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
4aacb94 to
f4c3e22
Compare
|
CI-Ready |
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
1. _find_transform_by_id(): add expert-parallel transform IDs so promote_initializers_and_build_spec() can recover the active_transform from the manifest and apply resolve_onnx_key() (.mlp.→.block_sparse_moe.). - moe_expert_parallel_stacking_v1 → MoEExpertParallelStackingCheckpointTransform - gptoss_mxfp4_dequant_expert_parallel_v1 → GptOssMxfp4ExpertDequantExpertParallelCheckpointTransform 2. _prepared_checkpoint_hash(): include moe_prefill_num_pipeline_stages and moe_prefill_num_parallelized_experts so two expert_parallel exports with different P/E values produce different prepared directories. 3. flavours.py: restore expand_as for matched_idx; torch.full_like on int32_max_scalar was an accidental change — the original compiler-preferred workaround uses expand_as on a scalar to avoid ConstantOfShape(INT32_MAX). Signed-off-by: Amar <amarshar@qti.qualcomm.com>
Signed-off-by: Amar <amarshar@qti.qualcomm.com>
f4c3e22 to
5b685cf
Compare
|
@quic-amitraj please add proper description |
|
CI-Ready |
| from QEfficient.transformers.moe.weights import _pack_expert_parallel_tensor # noqa: PLC0415 | ||
|
|
||
| # Step 1: standard per-expert stacking → moe_weights.gate/up/down [E, H, I] | ||
| new_weight_map = super().apply(src, out, target_dtype=target_dtype, weight_map=weight_map, **kwargs) |
There was a problem hiding this comment.
This still writes stacked experts-layer-* shards and then reopens/overwrites them for expert-parallel packing. The requested shape was raw group -> stack/dequant/split -> pack -> final write inside
one task, with no normal intermediate checkpoint stage.
|
|
||
| new_weight_map: Dict[str, str] = {} | ||
| consumed: set = set() | ||
| for transform in transforms_to_run: |
There was a problem hiding this comment.
This is still the selector/consumed-key model from the previous review. A raw key can only be owned by one transform, so multi-stage flows still have to be encoded as combined transform classes
instead of planned tasks with staged refs. Please move this to the planned task pipeline or stop claiming the pipeline TODO is addressed.
No description provided.