feat: load ModelOpt static-FP8 checkpoints via TorchAO - #157
Open
pengcuo wants to merge 7 commits into
Open
Conversation
pengcuo
marked this pull request as ready for review
August 6, 2026 02:35
Port of the release-synced half of imaginaire4!10639 (i4 main 39845b6510). `cosmos_framework/utils/generator/quantization.py` is mapped from `projects/cosmos3/cosmos3/utils/quantization.py` by the cosmos-framework release pipeline, so this file is byte-identical to what the next scheduled release will emit. Drop this commit if the release lands first. Adds: - `is_modelopt_fp8_checkpoint`: detect `hf_quant_config.json` with quant_method=modelopt / quant_algo=FP8, rejecting malformed or unsupported quantization configs. - `apply_modelopt_fp8_checkpoint_inplace`: stream the exported E4M3 weights and their static per-tensor weight/input scales out of the safetensors shards and install them directly as TorchAO `PrototypeFloat8Tensor` weights — no dequantization, calibration, or re-quantization. The whole conversion is validated (shapes, duplicate targets, missing scales, already-quantized targets) before any module is replaced. - `_ModelOptFloat8Linear`: works around two TorchAO 0.16 static-FP8 limits — zero-row inputs (0 // 0 on the block size) and the rank mismatch between a >2D input and its (1, 1) activation scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: pengcuo <pzeren@nvidia.com>
Port of the CF-owned half of imaginaire4!10639. Unlike quantization.py, `cosmos_framework/inference/model.py` is not in the release mapping (see .file_mapping.json), so it has to be maintained here directly. `_DiffusersLoadPlanner` gains `defer_modelopt_fp8_weights_loading`: when a ModelOpt FP8 checkpoint is detected, the E4M3 weight tensors are dropped from the DCP load plan (and excluded from the missing-key check) so no temporary BF16 copy is ever materialized. `from_pretrained_dcp` then installs them as TorchAO weights once the rest of the checkpoint is in. Guards: ModelOpt FP8 checkpoints are rejected for DP-sharded models (the weights become tensor subclasses, which requires plain-tensor params), when runtime PTQ is also requested (the checkpoint is already quantized), and when the checkpoint is not in diffusers layout (the FP8 loader follows the diffusers weight map). Note the loader-path difference from i4: this method does not apply runtime PTQ — that lives in cosmos_framework/utils/generator/model_loader.py — so ModelOpt FP8 checkpoints are supported on the diffusers path only. Tests cover the planner skip, the load/install ordering, both guards, and the quantization-layer conversion (CPU) plus a TorchAO dispatch check on GPU. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: pengcuo <pzeren@nvidia.com>
pengcuo
force-pushed
the
feat/modelopt-fp8-checkpoint-loading
branch
from
August 6, 2026 02:40
5bf886b to
5f6ed58
Compare
Lifts the data_parallel_shard_degree > 1 guard added in #157 by teaching TorchAO's PrototypeFloat8Tensor the ops and hooks FSDP2 needs, and by swapping ModelOpt FP8 linears before the network is parallelized so peak memory follows FP8 shapes rather than bf16. - utils/generator/quantization.py: * install_torchao_float8_fsdp_support() registers 8 aten ops (view/reshape/split/slice/as_strided/new_zeros/empty_like/ detach/clone/_to_copy/copy_) and fsdp_pre_all_gather / fsdp_post_all_gather on PrototypeFloat8Tensor. Only the E4M3 qdata travels; the PerTensor scale is identical on every rank. * plan_modelopt_fp8_targets() derives the target FQN list from the checkpoint index alone, so build_net can consume it without reaching back into the loader. * swap_modelopt_fp8_linears_on_meta() replaces target linears with _ModelOptFloat8Linear + PrototypeFloat8Tensor placeholders on meta before FSDP wrap -- FSDP registers the pre-all-gather extension at wrap time, so a post-hoc swap wouldn't be picked up. * apply_modelopt_fp8_checkpoint_inplace() now handles the DTensor- sharded (per-rank slice via distribute_tensor) and pre-swapped meta paths in addition to the legacy replicated path. - configs/base/defaults/quantization.py: adds modelopt_fp8_checkpoint_path and modelopt_fp8_target_fqns so the meta swap is driven from config. - inference/common/config.py: undo_config_replacements normalizes checkpoints that ship un-rewritten internal-tree paths (e.g. cosmos3-super-i2v-fp8-14072026) by running the forward table first. - inference/model.py: drops the DP-shard reject; pre-computes the target FQN list and threads it into config.quantization. - model/generator/omni_mot_model.py: swaps ModelOpt FP8 linears in build_net before parallelize_vfm_network, gated on config.quantization.modelopt_fp8_checkpoint_path. - model/generator/reasoner/qwen3_vl/qwen3_vl.py: _init_weights early- returns on _ModelOptFloat8Linear (E4M3 has no normal_ kernel; random init is pointless for a checkpoint-filled weight). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ild_net The FSDP FP8 commit reads self.config.quantization from OmniMoTModel.build_net, but OmniMoTModelConfig had no quantization field, so every path that went through build_net -- including plain bf16 training and convert_model_to_dcp -- crashed with "Key 'quantization' not in 'OmniMoTModelConfig'". Add quantization: QuantizationConfig on OmniMoTModelConfig (default is the disabled-quantization instance, so bf16 flows through unchanged). This mirrors how parallelism and compile are already threaded from the outer Cosmos3OmniConfig property setter to the inner model schema. Also update the tests: - test_from_pretrained_dcp_installs_modelopt_fp8_after_load: mock the new plan_modelopt_fp8_targets call so the fake checkpoint fixture isn't asked to open real safetensors shards. - test_from_pretrained_dcp_rejects_modelopt_fp8_with_dp_sharding: removed, DP sharding is now supported by the FSDP FP8 commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ults
The FSDP FP8 commit added modelopt_fp8_checkpoint_path and
modelopt_fp8_target_fqns to QuantizationConfig, but the
convert_model_to_diffusers validator compared the incoming
quantization dict against a hardcoded {exclude_regex, include_regex,
method}. After the previous commit mirrored quantization onto
OmniMoTModelConfig, the exported config now carries the full
quantization sub-config, so even a fully-default QuantizationConfig()
tripped the "non-default internal quantization" ValueError and broke
convert_model_to_diffusers for every trained bf16 model.
Add both fields (None / [] defaults) to disabled_quantization so a
disabled config still matches. Non-default values (an actual
modelopt_fp8_checkpoint_path or a populated target_fqns list) still
raise, which is what we want: exported diffusers checkpoints should
not carry a rank-local ModelOpt path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collaborator
|
We need to add fp8 inference case also into the smoketest? |
… smoke
The FP8 checkpoint path had unit coverage only. Add two end-to-end cases to the
existing Nano inference smoke test, one per parallelism layout, running the real
CLI against the ModelOpt static-FP8 Nano checkpoint:
* replicated (latency, cp=8) — the layout that worked before FSDP support.
* sharded (throughput, dp_shard=8) — FSDP2 all-gathers the FP8 weights
through the TorchAO tensor-subclass hooks. Without them the run dies on the
first all-gather, so completing it is the assertion; the video-content check
then catches a numerically broken run that still completes.
The swap count is parsed out of the log rather than string-matched: a checkpoint
whose FP8 targets failed to resolve would swap zero linears, silently run in
bf16, and pass every output assertion.
The checkpoint is not published under its own repository yet, so it cannot be a
--checkpoint-path registry name; the test downloads the one subdirectory from
the access-controlled nvidia/Cosmos3-Experimental repo at a pinned revision and
passes the local path. Missing access skips (a fork PR has no runner secret); a
deleted revision or broken download still fails loudly.
Generation is downscaled to 480p / 29 frames / 10 steps — per-tensor weight
quantization is independent of resolution and step count — matching what the
transfer case in the same file already does. Both cases run in 8m22s on 8xH200-
class hardware with a warm cache, so generator-inference-smoke goes to 90
minutes to absorb the 20 GB cold-cache download.
Verified on 8xH100: both cases pass, 504 linears swapped in each.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports ModelOpt static-FP8 checkpoint loading from imaginaire4 (
!10639, merged to i4 main as39845b6510).Loads ModelOpt's HF-exported FP8 checkpoints directly: the already-quantized E4M3 weights and their static per-tensor scales are installed as TorchAO tensor subclasses with no dequantization, calibration, or re-quantization. Detection is automatic from the checkpoint's
hf_quant_config.json— there is no new flag.Commits
0ac5e15—cosmos_framework/utils/generator/quantization.py(+236, pure addition)is_modelopt_fp8_checkpoint()— detectsquant_method=modelopt/quant_algo=FP8; rejects malformed or unsupported quantization configs rather than silently loading them.apply_modelopt_fp8_checkpoint_inplace()— streams the E4M3 weights and theirweight_scale/input_scaleout of the safetensors shards and buildsPrototypeFloat8Tensorweights directly. Follows the root index rather than assuming scale/weight locality (ModelOpt's export can place them in different shards). The full conversion plan is validated — shapes, duplicate targets, missing scales, already-quantized targets — before any module is replaced._ModelOptFloat8Linear— works around two TorchAO 0.16 static-FP8 limits: zero-row inputs (0 // 0on the block size) and the rank mismatch between a >2D input and its(1, 1)activation scale.This file is mapped from
projects/cosmos3/cosmos3/utils/quantization.pyby the release pipeline, so its content is byte-identical to what the next release will emit. It is included becauseorigin/maindoes not have it yet (last release2bd01bcpredates i439845b6510); if a release lands first, this commit can be dropped during rebase.5bf886b—cosmos_framework/inference/model.py(+87) + tests_DiffusersLoadPlannergainsdefer_modelopt_fp8_weights_loading: the E4M3 weight tensors are dropped from the DCP load plan (and excluded from the missing-key check) so no temporary BF16 copy is ever materialized.from_pretrained_dcpinstalls them as TorchAO weights once the rest of the checkpoint is in.Unlike
quantization.py, this file is not in the release mapping (see.file_mapping.json) —cosmos_framework/inference/is maintained here directly, so it is hand-ported.Divergence from i4 worth reviewing
_load_modelstatic method; CF keeps it inline infrom_pretrained_dcp(CF hasno_dist=,_local_checkpoint_dirthreading, and_raise_on_missing_vision_keysthat i4 does not). The port follows CF's structure rather than importing i4's refactor.from_pretrained_dcpdoes not apply runtime PTQ — that lives incosmos_framework/utils/generator/model_loader.py. ModelOpt FP8 checkpoints are therefore supported on the diffusers path only. Wiring the generator path would be a separate change._diffusers_weight_mapwithFileNotFoundError.Behaviour change for non-FP8 checkpoints
is_modelopt_fp8_checkpoint()now runs on every load. A checkpoint carrying anhf_quant_config.jsonthat is not modelopt/FP8 is now rejected withValueErrorinstead of loading. This is intentional (i4ef8d577f0a) — an explicit error beats silently loading quantized weights as BF16 — but it is a new failure point.Everything else on the non-FP8 path is a no-op: the deferral branch is skipped,
skipped_target_keysis empty, and the added set lookup never hits.Guards
ModelOpt FP8 checkpoints are rejected when
data_parallel_shard_degree > 1(weights become tensor subclasses, which requires plain-tensor params, not DTensor shards), when runtime quantization is also requested, and when the layout is not diffusers.Verification
Run against
nvidia/Cosmos3-Experimental@f0cdb8eacosmos3-nano-fp8-14072026(21 GB, ModelOpt 0.44.0 export), single H100:Loaded 504 calibrated ModelOpt FP8 weights into TorchAO— matches the 504weight_scale/input_scalepairs in the checkpoint index exactly. The 7 remaining plainnn.Linearare precisely the modules in the checkpoint'signorelist (proj_in,proj_out,time_embedder*,lm_head,visual*).aten::_scaled_mmdispatching tonvjet_qqtst_128x128_128x6_1x2_h_bz_TNT, with noaten::mm— real FP8 GEMM, not a dequantize-and-BF16-matmul fallback. Activation quantization is visible asaten::div→aten::clamp→_RoundToFloat8using the exportedinput_scale.inputs/omni/t2i.jsongenerates correctly; after the first compiled step, sampling holds 13.7 it/s (so the_ModelOptFloat8Linearreshape does not breaktorch.compile).Non-FP8 regression check on Cosmos3-Nano: loading the same checkpoint on this branch and on
mainproduces an identical SHA256 over all 814 parameters (15.17B elements), and an end-to-end t2i run on this branch is byte-identical tomain's output. (Note the sampling pipeline is not reproducible run-to-run, so image hashes alone are not a valid A/B signal — the weight hash is.)Tests: 18 new (
quantization_test.pycovers config detection, tensor preservation, ignored/absent targets, and a GPU TorchAO dispatch check;model_test.pycovers the planner skip, load/install ordering, and both guards).pyrefly checkclean on all four files.Note:
quantization_test.pyis new here and is not in the release mapping'sinclude_files. Adding it upstream would be a one-line change tomapping_config.toml; until then a future release adding it would trip the drift gate's collision check once.