Cosmos3-Edge under 10 GB

Quantizing and optimizing NVIDIA's 4B omni world model to run every inference mode in under 10 GiB of GPU memory on an Ampere A10G — with quality validated against a properly calibrated gate, and a torch 2.10 kernel trap found on the way.

July 2026 · A10G 23 GB (sm_86) · torch 2.10 + torchao 0.16 · repo · framework patch · previous phase: SFT in 24 GB

−2.62 GiB
weights saved, every mode (exactly the theoretical int8 saving)
4 / 4
inference modes under 10 GiB allocated
0.036
LPIPS vs bf16, 480p video (189 frames) — visually indistinguishable
≈ 1.0×
speed vs compiled bf16 (and the reasoner got 17% faster)

1 · Why this is not just "run bitsandbytes"

Cosmos3-Edge is not a plain LLM. It is a Mixture-of-Transformers: one 28-layer tower in which every layer carries two disjoint parameter sets — an understanding/reasoner pathway (Nemotron-2B VL) and a generation pathway (a rectified-flow video diffusion expert) — sharing a joint attention. Both pathways run on every diffusion step; there is no way to load just one. Add the Wan2.2 video VAE and a SigLIP2 vision tower and you get ~8.5 GiB of bf16 weights before a single activation.

NVIDIA ships quantization hooks in cosmos-framework — but the two shipped methods (mxfp8, nvfp4) require Blackwell tensor cores. On the Ampere A10G (sm_86: no FP8, no FP4) they cannot run at all. NVIDIA's own Jetson benchmarks for Edge run plain bf16 on 16–128 GB unified-memory devices. As far as we could find, no sub-10 GB Cosmos3-Edge recipe existed anywhere — this write-up is one.

We also found the shipped hooks were never applied on the checkpoint path most people actually use (the HuggingFace/diffusers snapshot): the config was stored into the model dict and silently ignored. Part of the patch is simply making --quantization-method work there.

2 · Where the bytes actually are

We parsed the safetensors headers for exact counts (no estimates):

ComponentParamsbf16 sizeQuantized in this work?
MoT attention (both pathways, 28 layers)0.70 B1.31 GiByes → int8
Reasoner-pathway MLPs1.06 B1.97 GiByes → int8
Diffusion-pathway MLPs (mlp_moe_gen)1.06 B1.97 GiByes → int8
embed_tokens + lm_head (untied, vocab 131k)0.54 B1.00 GiBno — next target (A4)
Wan2.2 VAE (encoder+decoder)0.70 B1.31 GiBno — CPU round-trip flag exists
SigLIP2 vision tower (lazy-loaded)0.49 B0.91 GiBno — reasoner-vision only

Two structural facts drive everything. First, at 480p×121-frame video the diffusion runs in a 16×/4× compressed latent space — only ≈12k tokens per forward — so weights, not activations, dominate the peak: quantize weights and the peak follows. Second, classifier-free guidance runs as two sequential full forwards per step (70 forwards per 35-step video), which is why the eventual distillation track (4-step, CFG-free) is worth ~17× compute.

3 · What we changed

The patch, in four parts (112 insertions across 5 files — branch):

  1. Two new quantization methods in the framework's existing QuantizationConfig plumbing: int8wo (torchao Int8WeightOnlyConfig v2 — int8 storage, bf16 compute) and int8dq (Int8DynamicActivationInt8WeightConfig — W8A8 on Ampere's INT8 tensor cores). The same regex module filter as the shipped methods: all nn.Linears in the 28 MoT layers, both pathways — 2.82 B of 3.37 B params.
  2. Actually applying quantization on the HF checkpoint path: a _apply_quantization hook after all weight loading, at both return points of from_pretrained_dcp — placed after load because quantize_ replaces live params with tensor subclasses that a later weight load would not survive (the lazy vision tower is safe: its module names don't match the filter).
  3. A guard against a torch 2.10 kernel regression (§6): gate the CUDA weight-only-quant inductor pattern back to CPU-only, restoring torch ≤2.9 behavior.
  4. set_inductor_config=False on both configs: torchao's default silently enables TF32/high-precision-matmul globally — the framework deliberately disables TF32, and a quality A/B where the baseline and the quantized run compile under different global flags measures nothing.

Around the patch we built a measurement harness (bench/): phase-separated memory probes (load transient vs inference peak — a distinction that turned out to be gate-critical), a fixed 4-mode battery with every sampler argument pinned, PSNR/LPIPS comparison, and a sampling-trajectory probe for the quality gate below. The design was adversarially reviewed by two independent review passes (one for code-path correctness — every claim verified against file:line — one for experimental methodology) before we trusted the numbers.

4 · Memory results

Peak inference memory, bf16 vs int8wo

GiB allocated (torch.cuda.max_memory_allocated after load-phase reset) — A10G, identical pinned sampler args, seed 0

bf16 int8wo 10 GiB target
Modebf16int8woΔUnder 10 GiB?
text2image 480p / 50 steps8.956.33−2.62yes
image2video 256p / 189 f8.365.74−2.62yes
image2video 480p / 189 f (worst case)12.489.86−2.62yes
reasoner (VLM planning, 1024 tok)8.656.01−2.64yes
steady-state weights after load7.594.97−2.62

The −2.62 GiB delta is identical in every mode and matches the arithmetic (2.82 B params × 1 byte saved, minus scales) to ±0.02 — evidence that int8wo adds no activation overhead: the whole saving survives to the peak. Stacking the existing COSMOS_VAE_CPU_OFFLOAD=1 flag frees another 1.31 GiB at the 480p peak (≈8.6 GiB expected steady-state).

Honest caveat: a hard-capped proof run (allocator clamped to ~10.1 GiB via DEVICE_MEMORY_FRACTION) survived load but OOMed on a decode transient at 9.53 GiB — the last ~0.5 GiB of transient headroom at 480p is open work (§9). The un-capped 480p peak of 9.86 GiB stands.

5 · Evaluation: quality without fooling ourselves

The obvious evaluation — same seed, compare pixels — is wrong for diffusion models, and it's worth being precise about why. A 480p video is 50 sampling steps × 2 CFG forwards = 100 forwards; a 0.05% numeric difference per forward compounds into a different sample — different but equally good. Per-pixel PSNR measures trajectory divergence, not quality loss. Our gate had to separate those.

Tier 0 — the null floor

Two identical bf16 runs, same seed: bit-exact (relative L2 = 0.0). The pipeline is deterministic, so every non-zero difference we measure is real signal, not run-to-run noise.

Tier 1 — direct quantization error, before it compounds

We instrumented the UniPC sampler and captured the step-0 velocity prediction — at step 0 the inputs (noise, timestep, prompt) are bit-identical across runs, so the difference there is the pure single-forward quantization error. The calibration insight: merely toggling torch.compile on the bf16 model — same weights, universally accepted as "the same model" — shifts the step-0 velocity by 10.1% relative L2. That's the numerics delta everyone already accepts. Quantization must be judged against it.

Comparison (t2i 480p, step-0 velocity)relative L2verdict
bf16 vs bf16 (same seed)0.000bit-exact null
bf16 compiled vs bf16 eager0.101the accepted numerics bar
bf16 vs int8wo (eager, kernel-clean)0.053half the accepted bar
bf16 vs int8wo (compiled)0.096under the bar
bf16 vs int8dq (W8A8)0.128above int8wo; borderline

Tier 2 — perceptual, on full generations

Three frames (start, middle, end) of the bf16 480p driving video: coastal mountain road, rockfall, emergency stop
bf16 — frames 0 / 90 / 180 of the 480p image2video battery clip
The same three frames from the int8 weight-only run: visually identical composition, textures and motion
int8wo — same frames, same seed
Full-output comparison vs bf16LPIPS (mean)LPIPS (max)read
image2video 480p, 189 frames0.0360.053visually indistinguishable
image2video 256p, 189 frames0.0400.063visually indistinguishable
text2image (single sample)0.179different-but-equal sample (trajectory divergence)

The reasoner mode decodes greedily; int8 flips occasional near-tie tokens, after which the texts legitimately diverge — both runs produce valid, complete task plans for the same manipulation prompt. (The framework's own docstrings document the same greedy-tie divergence for bf16 rank-to-rank noise.) A teacher-forced token-agreement probe is the planned tightening here.

6 · The torch 2.10 kernel trap

First int8 run: memory perfect, sampling 12× slower. No graph breaks, nothing in the logs. The root cause is worth publishing on its own:

torch 2.10 newly enables an inductor pattern (pytorch#161680) that rewrites the int8 dequant+matmul graph into CUDA aten._weight_int8pack_mm — a kernel designed for batch-1 LLM decoding (thread-per-output GEMV). At diffusion token counts (M≈12k) we measured it at 1125 ms for a matmul that dequant+cuBLAS does in 9.5 ms — ~150× slower — on the exact FFN shape of this model. Because it's an in-graph kernel substitution, not a graph break, it is invisible to normal debugging. torch ≤ 2.9 gated this pattern to CPU-only.

Our fix restores that gate before the first compiled forward (a ~20-line guard in the patch). Two extra traps documented in the repo: the bad kernel persists in the inductor cache after the fix (clear ~/.cache/torch/inductor once), and torchao v1-vs-v2 config versions are a red herring — both emit the identical graph and hit the identical pattern.

7 · Speed results

Sampling throughput, t2i 480p / 50 steps

iterations per second (higher is better) — same window, ~5 GiB co-tenant load on the shared GPU

Configurationit/snote
int8wo, eager3.89fastest — beats compiled bf16
bf16, compiled (baseline)3.78
bf16, eager2.60
int8wo, compiled + our gate1.8114× recovery over ungated
int8dq (W8A8), compiled + gate1.76microbench says 1.45× bf16 per block — e2e pending clean window
int8wo, compiled, ungated (the trap)0.13the §6 kernel substitution

Weight-only int8 being fast is not luck: it halves weight-memory traffic, and these forwards are partly bandwidth-bound even at 12k tokens. The clearest demonstration is the reasoner mode: 51.2 s → 42.4 s (17% faster) under int8 — autoregressive decode is memory-bound, exactly where weight-only quantization pays twice (memory and speed).

8 · The recipe

# apply the patch branch to cosmos-framework, then:
python -m cosmos_framework.scripts.inference \
    -i input.json -o outputs/run \
    --checkpoint-path Cosmos3-Edge \
    --quantization-method int8wo \
    --no-use-torch-compile          # eager int8 ≈ compiled-bf16 speed, no inductor footguns

# stack for the lowest peak (−1.31 GiB more):
COSMOS_VAE_CPU_OFFLOAD=1 python -m cosmos_framework.scripts.inference ...

Every number on this page is reproducible from the repo: bench/run_battery.sh bf16_refbench/run_battery.sh int8wo --quantization-method int8wobench/compare.py bf16_ref int8wo; trajectory gates via bench/trajprobe.py; the kernel regression isolated in bench/int8wo_microbench.py. Raw measurement JSON is in results/.

9 · What's next

TrackWhatExpected
A4Quantize embed/lm_head (last 1.0 GiB bf16 weights) + tame the 480p decode transient; re-run the hard-capped <10 GiB proof480p ≈ 8.5 GiB, capped-run proof
int8dqClean-window speed + video-quality validation of W8A8 (INT8 tensor cores)int8 faster than bf16 compiled, same memory
B<10 GiB training: QLoRA on the generation pathway (int8/NF4 frozen base + LoRA r32–64 + precomputed VAE latents + 8-bit Adam)est. 5.6–8 GiB SFT
CDMD2 step distillation (pipeline exists in-repo) → 4-step CFG-free student~17× fewer forwards; real-time-ish robot-policy rollouts

The full research document — literature review across quantization/attention/offload/ distillation (60+ sources), the verified model anatomy, ranked technique stacks with memory arithmetic, and the review-hardened measurement protocol — is in docs/QUANT_10GB_RESEARCH_PLAN.md.