Skip to content

NVFP4 support - #1

Open
sirish-gambhira wants to merge 14 commits into
mainfrom
users/sgambhira/nvfp4
Open

NVFP4 support#1
sirish-gambhira wants to merge 14 commits into
mainfrom
users/sgambhira/nvfp4

Conversation

@sirish-gambhira

Copy link
Copy Markdown
Collaborator

Summary

Describe the bug, fix, or feature clearly and briefly.

What Changed

  • List the main code changes.
  • List any API or behavior changes.
  • List any follow-up work that is intentionally out of scope.

Tests

Every working PR must include at least one new simple, fast, targeted unit test when the change affects behavior, a bug fix, or a regression path.

  • I added a new simple/fast unit test for this change, or documented why that is not applicable.
  • I ran the new targeted test locally before opening this PR.
  • I ran any other directly relevant local tests.

Paste the exact test commands and results here:

Review Requirements

AI-assisted code is welcome.

Every changed file must still be properly reviewed by a human before the PR is opened as ready for review.

We will not accept PRs that are effectively unreviewed AI output. Non-human-reviewed changes often introduce obscure structure, mismatched APIs, project-inconsistent code patterns, or unnecessary monkeypatching instead of a correct fix or clean feature expansion.

  • I personally reviewed every file in this diff.
  • I checked that the code matches existing project structure, APIs, and conventions.
  • I avoided unnecessary monkeypatching and used the project's normal extension points where possible.

Notes

Add any migration notes, risks, compatibility concerns, or reviewer guidance here.

Sirish Gambhira and others added 4 commits August 5, 2026 11:47
LazyTurtle copied checkpoint tensors into shell params with a plain dtype
cast. For an mxfp8 checkpoint the stored tensor is only the F8_E4M3 mantissa
payload; the magnitude lives in a sibling weight_scale_inv holding E8M0
exponent bytes, one per 32-element block. Casting to bf16 silently dropped
that scale, so every quantized weight was wrong by 2^(e-127).

The scale was never even requested: t_params comes from the shell module's
named_parameters(), and a plain nn.Linear has no weight_scale_inv attribute,
so _resolve_checkpoint_tensor_source was never asked for it.

Measured on Minimax-M3-0602, layers.3.self_attn.q_proj: the materialized
tensor was bit-identical to the raw fp8 payload (absmax 448, std 148.6)
instead of the scaled weight (absmax 0.2812, std 0.0381). Downstream, MoE
routing collapsed onto 6.8 of 128 experts per layer with one expert taking
every token in 55 of 57 layers; with scales applied it reaches 127.9 of 128
with no expert above a 17.5% share.

dequantize_fp8() could not be reused: _expand_scale treats scale_inv as a
float multiplier and broadcasts by rank, so E8M0 bytes would multiply by
120.0 rather than 2^-7, and a [8192,192] scale will not broadcast against a
[8192,6144] weight. Adds dequantize_block_fp8 / block_scale_multiplier,
which decode E8M0 and expand each dim by target_dim // scale_dim. Verified
bit-for-bit (maxdiff 0) against modelopt's MXFP8QTensor.dequantize on
attention, MoE expert, and shared-expert weights.

Routes every materialization read through _read_checkpoint_tensor, covering
both _copy_checkpoint_tensors_into_submodule and the parallel
_materialize_direct_meta_tensors path (param/buffer, single and concat).
Layers in ignored_layers carry no scale tensor, so the lookup misses and
they keep the plain-cast path -- which is why the F32 routers loaded
correctly even while the bug was live.

Not covered: _load_checkpoint_tensors_for_module_path still returns raw
payloads. It has 8 callers in models/base.py and returns the _scale_inv
entries alongside the weights, so dequantizing there could double-apply;
those call sites need review separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GPTQ minimizes the layer output error. For weight-only quantization inference
computes Ŵ·X, so H = 2·X·Xᵀ is the right curvature. Under w4a4 the kernel
computes Ŵ·Q(X), and a Hessian built from X solves for inputs that never
occur. At 4-bit activations that gap is large; at 16-bit it does not exist,
which is why this has not mattered until now.

Activations reach the solve through exactly one path -- process_batch ->
compute_hessian_xtx -> self.H -- so quantizing there also propagates to
dead-column detection, act-order, act-group-aware grouping, and the damped
Cholesky inverse.

Adds GPTQConfig.act_format / act_group_size. act_format=None keeps today's
weight-only behavior exactly: no activation quantizer is constructed and
process_batch is unchanged.

The quantizer is applied explicitly rather than captured by a hook. This is
deliberate: a forward_pre_hook fires before a module's forward body and so
observes activations *before* any input quantizer, which would silently
accumulate a clean Hessian while every config flag looked correct. The
regression test asserts H actually differs (~4.8% relative on random input)
precisely to catch that failure mode.

Ordering in process_batch matters twice: the quantizer runs before the TP
zero-padding, so pad columns never enter a 16-wide block's amax, and after any
input-scale transform already baked into `inp`, since that is what the kernel
quantizes.

fake_quantize_nvfp4 wraps torchao's nvfp4_quantize + NVFP4Tensor.dequantize
rather than reimplementing the grid, so it cannot drift from the weight path.
Following that path, per_tensor_scale is omitted -- block scales only.

Scope: this is tier A of docs/w4a4_hessian.md, min ‖(W−Ŵ)·Q(X)‖², matching
Model-Optimizer's GPTQHelper. It is an approximation: the exact objective
min ‖W·X − Ŵ·Q(X)‖² carries a W·(X−Q(X)) bias no Ŵ can cancel, which needs a
second accumulator (tier B, cf. Quark GPTAQ's ∆(XXᵀ)).

Known gap: NVFP4 activations use a static per-tensor global scale that is
calibrated, plus dynamic per-block scales. Nothing populates the global scale
yet -- Model-Optimizer gets it from a separate max_calibrate stage that
GPTQModel has no equivalent of. Until that lands, the calibration grid is
close to but not identical to a static-global-scale serving kernel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
NVFP4 activations are scaled twice: a static per-tensor global scale frozen at
calibration, and dynamic per-16-block E4M3 scales. Only the block scales can be
derived while accumulating the Hessian, so w4a4 calibration was quantizing
against a block-only grid -- close to, but not the same grid a static-global
kernel consumes.

The global scale cannot be computed here by construction: it is a property of
the whole calibration set, and we are inside the pass that would produce it.
Both reference pipelines resolve this by ordering rather than by a two-pass
GPTQ -- Model-Optimizer freezes it in an earlier max_calibrate stage, and
tore-quant emits it from Phase 1 (`ptq.py --emit-amax-sidecar`) for Phase 2 to
consume. This follows the latter: read the sidecar the pipeline already
produces instead of adding a calibration stage.

Scale formula mirrors tore-quant's amax_to_scale exactly:

    global_scale = amax / 6.0 / 448.0        # amax / F4_E2M1_MAX / F8_E4M3_MAX

The amax is a plain max in that pipeline, not percentile- or MSE-clipped.

act_amax_key_rules exists because the scale is keyed by *input site*, not by
module. w1 and w3 consume the same MoE input and therefore share one entry
(`layer{N}.moe_input`) while w2 sees the post-SwiGLU activation and gets its
own; q/k/v share their attention input by the same logic. A per-module lookup
would hand siblings different scales than the kernel applies -- the exact
mismatch the sidecar exists to prevent -- so a test pins the sharing.

Resolution is lazy rather than done in __init__: `self.name` is not final at
construction time (the looper names modules afterwards), so an early lookup
reads the wrong key. This was caught by the new tests, and in production would
have surfaced as a wrong scale rather than a crash.

A missing key raises instead of falling back to block-only scales; a silent
fallback would quantize against a different grid than the kernel with no
signal. Non-finite and non-positive amax are rejected.

The sidecar loader is lru_cached so thousands of modules read the file once,
and accepts both a flat {key: amax} mapping and the pipeline's
{"amax": {...}} shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@broly-code-security-scanner

broly-code-security-scanner Bot commented Aug 6, 2026

Copy link
Copy Markdown

Broly Security Scan

Note

Summary

1 actionable finding(s) in this PR
3 total in scan · 0 dismissed false positives

  • 🟡 1 medium

All actionable items are in the table below.

2 finding(s) below the medium reporting threshold are not listed above — see the repository Security tab for the full set.

No finding is at or above high, so this check is not blocking. The findings above are still tracked and reported.

Severity Scanner Issue Location Dismiss Verdict
🟡 MEDIUM SAST Command injection via environment variables
interpolated into Python code strings.
scripts/minimax_m3_nvfp4/run.sh:77 d6 🔺 TRUE_POSITIVE · Confidence: HIGH

Dismiss false positives

Tick a box to dismiss the finding; untick it to bring the finding back. That is the same as replying /broly dismiss d1 and /broly undismiss d1. To record why it is a false positive, reply with /broly dismiss d1: your reason instead — Broly reuses those reasons to triage similar findings across the org.

  • d6 · 🟡 MEDIUM   · scripts/minimax_m3_nvfp4/run.sh:77 · Command injection via environment variables interpolated into Python code str...

Note

Re-scan this PR anytime with /broly scan — useful after /broly undismiss, or to refresh findings without a new push.

Broly — SAST (zai-org/GLM-5.2) · Secrets · SCA · IaC · GH Actions · Base Images · Supply Chain Threats · Exploit Chains · Adversarial Verification

We're continuously improving Broly's accuracy and finding quality — your feedback is valuable. False positives, missed findings, bugs, and feature requests all welcome.

Ask in #security-engineering   Powered by Together AI

Comment thread gptqmodel/quantization/gptq.py Fixed
Sirish Gambhira and others added 4 commits August 5, 2026 18:06
…rics

GPTQModel's NVFP4 export stored only per-block E4M3 scales. ModelOpt, Quark
and tore-quant all use two-level scaling: a per-tensor weight_scale_2 =
amax/(6*448) plus block scales. Without it the block scales carry absolute
magnitudes, and for realistic weight tensors block_amax/6 sits below E4M3's
smallest normal (2^-6) -- measured 100% of blocks clamped at the floor on an
N(0, 0.02) weight -- so every block quantized on a compressed grid. Two-level
round-trip error is 1.36x lower on that tensor (0.096 vs 0.131 relative).

RTN path: quantize_nvfp4_weight computes the canonical greedy global scale
(verified equal to ModelOpt's global_amax/(E2M1_MAX*m_fp8) and tore-quant's
amax_to_scale) and returns a 3-tuple.

GPTQ path: the loop has already fixed each block scale s_i and the packed
codes are encoded against them, so the split must satisfy
block_e4m3_i * ws2 == s_i exactly. A greedy ws2 cannot (the ratio of two E4M3
mantissas is generally unrepresentable); a power-of-two ws2 =
2^ceil(log2(max(s)/448)) only shifts exponents, leaving mantissas intact.
Verified bit-exact recovery; an equality check raises rather than silently
emitting codes that no longer match their scales.

Both read paths apply it: dequantize_weight folds ws2 into the block scales
(one pass, exact in fp32), _native_weight passes it as NVFP4Tensor's
per_tensor_scale. TorchFP4Linear -- which executes directly from checkpoint
weights -- now accepts weight_scale_2 too; it previously ignored the tensor
that ModelOpt-layout checkpoints (e.g. MiniMax-M3-NVFP4) carry on every
quantized module, dequantizing every block off by a constant factor.

Also switch x/s to x * s.reciprocal() in NVFP4Quantizer.quantize and
_pack_with_gptq_scales, matching torchao's nvfp4_quantize ("multiply by
reciprocal instead of dividing to match MSLK triton kernel numerics"). The
1-ULP difference can flip an E2M1 code on rounding boundaries, which breaks
bit-identical packing claims.

An unpacked module defaults weight_scale_2 to 1.0 (identity), so one-level
checkpoints load unchanged. test_direct_pack_matches_torchao updated to
assert the two-level layout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ze driver, run.sh

Three-stage pipeline for quantizing Minimax-M3-0602 routed experts to NVFP4
with w4a4 calibration:

emit_act_amax.py (step 0) -- emits the per-layer activation amax sidecar the
static NVFP4 global scale needs. Two keys per MoE layer, keyed by input site
because modules sharing an input share a scale: layer{N}.moe_input
(gate/up_proj) and layer{N}.w2_input (down_proj, post-SwiGLU). Plain max over
|x| reduced across a layer's experts, matching tore-quant's save_amax_sidecar
peer-max semantics. Captures via down_proj pre-hooks when the loader exposes
per-expert Linears (GPTQModel does: it splits the checkpoint's fused 3D expert
tensors), falling back to wrapping the fused module's _apply_gate -- whose
return value IS the down_proj input -- and refusing to emit a partial sidecar
if neither works. Streams layer windows through one GPU with per-window
resume, the p1b_expert_coverage harness pattern.

quantize_w4a4.py -- GPTQ->NVFP4 driver. Quantizes only routed experts (57
layers x 128 experts x 3 projections); attention, shared experts, dense MLP
0-2, routers, embeddings and the vision tower pass through, matching the
tore-quant recipe format map. RTN-fallback thresholds are per-projection at
each weight's in_features (gate/up 6144, down 3072): H = XtX has
rank <= min(n_tokens, cols), so a module below its own input dim would be
GPTQ-solved on a singular Hessian propped up by damping. The library's 0.5%
default sits below both dims and is scale-invariant besides (RTN iff routing
share < pct/(100*top_k) -- more calibration data cannot lower it). With
ACT_AMAX set, the Hessian is accumulated on Q(X) with the sidecar-derived
static global scale; without it the run is weight-only and says so. Post-check
asserts the saved layout (uint8 weight + e4m3 weight_scale + fp32
weight_scale_2) instead of trusting the log.

run.sh -- preflight (reads file CONTENT, not stat: the /scratch checkpoint
copy passes ls and fails on read), disk/GPU checks, predicted per-projection
RTN rates from the measured coverage matrix, then the driver. All knobs are
env vars; PREFLIGHT_ONLY=1 and LAYERS=N smoke-test modes included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt repair

Config (from_quant_config path): accept the `fp4` format alias when the
method resolves to NVFP4, matching the constructor path's
_normalize_nvfp4_kwargs, and route NVFP4Config targets through the same
kwargs normalization.

Config (GPTQ validation): format=nvfp4 now rejects what cannot land on the
NVFP4 grid -- non-RTN fallback strategies (asymmetric integer grids whose
zero-points the packer cannot represent), fallback smoothing (per-block
factors make the stored scale non-E4M3-exact, so packing stops being
lossless), and mock_quantization (integer rounding, one scale per 128-column
block).

GPTQ NaN-loss path: under format=nvfp4 a NaN loss now goes to the RTN
fallback (which quantizes through NVFP4Quantizer, staying on-grid) instead of
the mock-quantization retry, which is off-grid for the same reasons as above.

Also repairs tests/test_nvfp4_export.py from commit 6466766, where
zero-context partial staging misplaced two lines -- the two-level docstring
landed inside a constructor argument list (SyntaxError: positional argument
follows keyword argument) and a weight_scale_2 assert landed in the wrong
test. The worktree version was always correct (37 tests green); only the
committed copy was broken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Point MODEL at /data/huggingface/Minimax-M3-0602: the /scratch copy returns
  EIO on config.json and every safetensors shard (btrfs read errors) while
  passing `ls`, so the failure only shows up on read.
- WINDOW=5 (was 1): the co-tenant pipeline that forced single-layer windows is
  gone; 5-layer windows amortize materialize/free and the peak (~100 GiB
  transient sdpa masks) fits an idle B200.
- Checkpoint after every window (CKPT_EVERY=1) so a kill resumes at the last
  5-layer boundary; two earlier runs were lost to co-tenant OOM kills.
- OOM retry helper for materialization under memory pressure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread scripts/minimax_m3_nvfp4/emit_act_amax.py Fixed
Comment thread scripts/minimax_m3_nvfp4/run.sh Fixed
Sirish Gambhira and others added 5 commits August 5, 2026 18:41
… dequant

The GPTQ quantize flow does not read weights through the LazyTurtle
materialization path fixed in 3e01e1c. The looper materializes quant targets
with role="quant_source" (module_looper.py:1099) and forward modules with
role="forward" (:1067); both pull raw tensors via
checkpoint_tensors_for_submodule and decode fp8 through
_build_decoder_quant_source_module -> dequantize_fp8. That function treated
scale_inv as a float multiplier: mxfp8's weight_scale_inv is uint8 UE8M0
exponent bytes (~110-130), which exceed 1, so _fast_scale_arg flipped into
divide mode -- dividing by the raw byte value instead of multiplying by
2^(b-127). Measured on Minimax-M3-0602 layers.3 q_proj: absmax 3.94 (divide
by ~120) vs correct 0.2812. Every weight GPTQ would have solved against was
wrong by a block-dependent factor, silently.

A uint8 scale can only be E8M0 exponent bytes -- a real float scale is never
serialized as uint8 -- so dequantize_f8_e4m3 now routes uint8 scale/scale_inv
straight through dequantize_block_fp8 (verified bit-identical to modelopt's
MXFP8QTensor.dequantize). Decoding the bytes in place and continuing was not
enough: the fast CPU path also mishandles 2D block-grid scales on small
tensors (the reference path, taken for large tensors, is fine), which is why
the earlier large-tensor verification passed while a small synthetic failed.
Float scales keep their existing paths untouched.

Fixes the quant_source and forward decoder roles, TorchFP8Linear, and every
other dequantize_fp8 caller in one place. Verified bit-identical to the
reference decode on real M3 attention and expert tensors; regression tests
pin the multiplicative semantics (4.0 * 2^-3 = 0.5, not 4.0/124), uint8-vs-
reference equality, float-path stability, and the real-checkpoint magnitude.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The looper names modules layer-relative -- GPTQ's `self.name` for an M3 expert
is `mlp.experts.1.gate_proj`, with no layer index for the sidecar key rules to
capture. Resolution now uses NamedModule.full_name (the full dotted path,
`...layers.3.mlp.experts.1.gate_proj`), falling back to `self.name` when no
NamedModule wraps the module (bare-module tests, HF-optimum path).

Found by the LAYERS=3 smoke run: every rule missed, the lookup fell through to
the raw name, and the fail-loud KeyError stopped the run before any wrong
scale was applied -- the failure mode the guard exists for. The regression
test reproduces the exact shape: layer-relative name + full_name on the
wrapper, asserting the rule captures the layer index from the latter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…w4a4 pipeline

splice_source_passthrough.py (new, run.sh phase 3, SPLICE=1 default): GPTQModel's
save materializes every module, so passthrough tensors come out dequantized-BF16
under runtime naming -- numerically right but not the serving format map, and not
loadable by a source-arch stack (both checkpoints declare
MiniMaxM3SparseForConditionalGeneration, but gptqmodel writes
model.language_model...mlp.experts.N.gate_proj where the source and serving
loaders expect language_model.model...block_sparse_moe.experts.N.w1). The splice
walks the SOURCE tensor list and swaps in the quantized experts under source
naming: passthrough stays byte-identical MXFP8/BF16/F32, quantized experts gain
{weight U8, weight_scale E4M3, weight_scale_2 F32, input_scale F32}, with
input_scale = amax/6/448 from the same sidecar the Hessian was calibrated on --
so the serving activation scale and the calibration scale agree by construction.
Verified on the LAYERS=3 smoke checkpoint: index keyset exact (46,606 keys),
17/17 sampled passthrough tensors byte-identical, 18/18 sampled expert quads
bit-equal with exact f32 input_scale.

quantize_w4a4.py: post-check now probes inside the quantized layer set -- the
layer-blind probe hit passthrough layer 10 first and failed a good checkpoint.

gptq.py: RTN-fallback WARN logs full_name -- self.name is layer-relative and
repeats across all 57 layers, which would make multi-layer fallback rosters
collide silently.

run.sh: COLUMNS=360 so headless logbar stops truncating the per-module loss
column to `...`; emits <OUT>/quant_roster.json mapping every RTN module with
observed tokens + threshold (GPTQ-solved = the complement); phase 3 wiring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al scale

The weight_scale_2 fix (6466766) covered the pack stage and the no-scales RTN
path, but pack deliberately preserves the loop's scales -- and the loop's
NVFP4Quantizer.find_params still derived them single-level:
clamp(block_amax/6, 2^-6, 448). For realistic weight magnitudes those absolute
values sit below E4M3's smallest normal, so blocks pin at the floor and
quantize on a compressed grid. Measured on the LAYERS=3 smoke checkpoint
against MiniMax-M3-NVFP4 (ModelOpt RTN of the same weights): 96.8% of block
scales at the floor, reconstruction error 1.4-1.8x the donor's (0.12-0.15 vs
0.082 relative) -- on our RTN-fallback modules too, which should have matched
the donor's RTN nearly exactly. cos(ours, donor) was 0.993+, so every layout
and losslessness check passed while quality was silently off.

find_params only ever sees one 16-wide group, so the per-tensor scale must be
fixed from the full weight before the loop: prime_global_scale(W), called in
the main quantize path and in _fallback_quantize. Primed, find_params derives
block scales as E4M3 ratios against it (torchao's two-level path, verified
bit-equal); unprimed behaviour is unchanged single-level.

The global scale is snapped to a power of two, 2^ceil(log2(amax/(6*448)))
(tore-quant's lossless-conversion snap), so the loop's effective scales split
exactly at pack time with no plumbing changes: _pack_with_gptq_scales
re-derives gs -- or gs/2 when the max block ratio E4M3-rounds down to exactly
224; either decomposition reproduces the identical product, and the in-pack
equality guard holds. Cost vs the donor's greedy per-tensor scale is at most
one bit of E4M3 range headroom.

Tests: primed find_params bit-equal to nvfp4_quantize(per_tensor_scale=gs);
floor-clamp fraction must drop below 50% on N(0, 0.02) weights (was ~100%);
pack split exact with a power-of-two recovered scale in {gs, gs/2}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…() baseline

base.py passes quantize_config.fallback into module_looper.loop(); it threads
through run_layer_stage -> preprocess -> clone_gptq_config_for_module, which
applied it AFTER the dynamic-override block -- so every per-module dynamic
`fallback` was silently clobbered by the global value in the standard
quantize() path. Found via the LAYERS=3 smoke roster: all three expert
projections fell back at threshold=6144 even though the per-projection rank
thresholds set down_proj to 3072 (its in_features), needlessly RTN'ing 13
down_proj experts whose Hessians were full-rank.

The caller-supplied fallback is now the baseline, applied before the dynamic
block, so a dynamic override merges onto it and wins. Regression test pins
both directions: an overridden module resolves to its dynamic threshold, a
non-overridden module keeps the baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread scripts/minimax_m3_nvfp4/emit_act_amax.py Fixed
…arams raises

The unprimed branch of NVFP4Quantizer.find_params derived absolute amax/6
block scales, which floor-clamp at E4M3's 2^-6 normal minimum on realistic
weight magnitudes (measured 96.8% of blocks on Minimax-M3 experts, 1.4x
reconstruction error). Keeping it as a silent fallback meant any caller that
forgot prime_global_scale would reproduce that bug undetectably -- it cost a
full smoke-run cycle plus a donor-checkpoint comparison to find the first
time. An unprimed caller is a bug, not a mode: find_params now raises with
the priming instruction.

Audit of every path that can hold an NVFP4Quantizer, as the precondition for
making this fatal:

- GPTQ.quantize: primed before the first full-weight find_params; every
  other site (static_groups precompute, act-order branches, CPU-fallback
  re-runs, per-group loop) is downstream in the same call.
- GPTQ._fallback_quantize: primed before the RTN per-block loop.
- GPTAQ / FOEM: subclass GPTQ and inherit the NVFP4 quantizer but run their
  own quantize() with full-weight find_params -- these were UNPRIMED and
  would have produced floor-clamped NVFP4 silently; now primed.
- rtn.py constructs a plain Quantizer (never NVFP4); the weight-only NVFP4
  lifecycle packs via quantize_nvfp4_weight with no quantizer object.

The old unprimed-vs-torchao-single-level parity test is repurposed as the
raise regression; the lossless-pack test primes as production does; primed
two-level parity was already covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

if [[ -n "${ACT_AMAX}" ]]; then
[[ -f "${ACT_AMAX}" ]] || die "ACT_AMAX sidecar not found: ${ACT_AMAX}"
"${PY}" -c "import json,sys; d=json.load(open('${ACT_AMAX}')); d=d.get('amax',d); print(f' act amax ${ACT_AMAX} ({len(d)} entries)')" \
the NVFP4 conversion pipeline emits.
"""

with open(path, "r") as handle:

start_layer = 0
if os.path.exists(CKPT):
state = torch.load(CKPT, map_location="cpu", weights_only=False)
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