Skip to content

feat: load ModelOpt static-FP8 checkpoints via TorchAO - #157

Open
pengcuo wants to merge 7 commits into
mainfrom
feat/modelopt-fp8-checkpoint-loading
Open

feat: load ModelOpt static-FP8 checkpoints via TorchAO#157
pengcuo wants to merge 7 commits into
mainfrom
feat/modelopt-fp8-checkpoint-loading

Conversation

@pengcuo

@pengcuo pengcuo commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Ports ModelOpt static-FP8 checkpoint loading from imaginaire4 (!10639, merged to i4 main as 39845b6510).

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

0ac5e15cosmos_framework/utils/generator/quantization.py (+236, pure addition)

  • is_modelopt_fp8_checkpoint() — detects quant_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 their weight_scale / input_scale out of the safetensors shards and builds PrototypeFloat8Tensor weights 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 // 0 on 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.py by the release pipeline, so its content is byte-identical to what the next release will emit. It is included because origin/main does not have it yet (last release 2bd01bc predates i4 39845b6510); if a release lands first, this commit can be dropped during rebase.

5bf886bcosmos_framework/inference/model.py (+87) + tests

_DiffusersLoadPlanner gains defer_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_dcp installs 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

  • i4 factored the load out into a _load_model static method; CF keeps it inline in from_pretrained_dcp (CF has no_dist=, _local_checkpoint_dir threading, and _raise_on_missing_vision_keys that i4 does not). The port follows CF's structure rather than importing i4's refactor.
  • CF's from_pretrained_dcp does not apply runtime PTQ — that lives in cosmos_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.
  • Added a guard i4 does not have: a ModelOpt checkpoint that is not in diffusers layout is rejected with a clear message instead of failing deep inside _diffusers_weight_map with FileNotFoundError.

Behaviour change for non-FP8 checkpoints

is_modelopt_fp8_checkpoint() now runs on every load. A checkpoint carrying an hf_quant_config.json that is not modelopt/FP8 is now rejected with ValueError instead of loading. This is intentional (i4 ef8d577f0a) — 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_keys is 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@f0cdb8ea cosmos3-nano-fp8-14072026 (21 GB, ModelOpt 0.44.0 export), single H100:

  • Loaded 504 calibrated ModelOpt FP8 weights into TorchAO — matches the 504 weight_scale / input_scale pairs in the checkpoint index exactly. The 7 remaining plain nn.Linear are precisely the modules in the checkpoint's ignore list (proj_in, proj_out, time_embedder*, lm_head, visual*).
  • Profiler on a converted linear shows aten::_scaled_mm dispatching to nvjet_qqtst_128x128_128x6_1x2_h_bz_TNT, with no aten::mm — real FP8 GEMM, not a dequantize-and-BF16-matmul fallback. Activation quantization is visible as aten::divaten::clamp_RoundToFloat8 using the exported input_scale.
  • Quantized weights occupy 12.94 GiB vs 25.88 GiB BF16-equivalent.
  • inputs/omni/t2i.json generates correctly; after the first compiled step, sampling holds 13.7 it/s (so the _ModelOptFloat8Linear reshape does not break torch.compile).

Non-FP8 regression check on Cosmos3-Nano: loading the same checkpoint on this branch and on main produces an identical SHA256 over all 814 parameters (15.17B elements), and an end-to-end t2i run on this branch is byte-identical to main'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.py covers config detection, tensor preservation, ignored/absent targets, and a GPU TorchAO dispatch check; model_test.py covers the planner skip, load/install ordering, and both guards). pyrefly check clean on all four files.

Note: quantization_test.py is new here and is not in the release mapping's include_files. Adding it upstream would be a one-line change to mapping_config.toml; until then a future release adding it would trip the drift gate's collision check once.

@pengcuo
pengcuo marked this pull request as ready for review August 6, 2026 02:35
pengcuo and others added 2 commits August 5, 2026 19:38
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
pengcuo force-pushed the feat/modelopt-fp8-checkpoint-loading branch from 5bf886b to 5f6ed58 Compare August 6, 2026 02:40
pengcuo and others added 4 commits August 6, 2026 23:27
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>
@lfengad

lfengad commented Aug 7, 2026

Copy link
Copy Markdown
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>
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.

2 participants