~/blog/minimax-h3-on-modded-2080ti-22gb

改裝 2080 Ti 22G · part 11

[Just for Fun — Advanced] Wait, a 2080 Ti Can Run MiniMax-H3? 1080p With Audio on a 2018 Card

cat --toc

TL;DR

A modded RTX 2080 Ti 22G from 2018 renders 15 seconds of 1080p video with audio in 23 min 03 s, using MiniMax-H3 — a 33B model whose four files come to 38 GiB on disk. The same clip on a 5090 takes 314s, so this card is 4.4× slower. Slow, not incapable. Three things make it fit: an INT8 diffusion transformer (W4A4 is 18× worse and tears the frame), a W4A4 text encoder (fine there, worth 8.6%), and --reserve-vram 4, which is the sampler's activation budget rather than OS headroom. SageAttention adds 16.8% while logging two fallbacks per run. Caveat: reserve-vram is the one value you keep retuning per clip length.

🔊 Sound on. 15 seconds of 1080p with synchronized audio, generated end to end on a 2018 RTX 2080 Ti modded to 22GB. Render time: 23 min 03 s.

Intro

When you rent a moving truck, everyone compares horsepower, and the number that actually decides how much you move in one trip is cargo volume. Old GPUs get ruled out of new workloads the same way. The problem usually isn't speed — slow is a scheduling problem and you can wait it out. It's that the weights don't fit, and there is no "just leave it running longer" for a tensor with nowhere to live.

MiniMax-H3 is a 33B model that generates video and its audio track in a single forward pass. Its four files come to 38 GiB on disk, half again as much as this card holds. Read the right-hand column — that is why they fit anyway:

fileon diskwhen it is on the card
text encoder Qwen3-VL-32B (W4A4)13.20 GiBruns once at the start, then unloads
diffusion transformer (INT8)19.53 GiBresident for the whole sampling run
video VAE (fp16)4.85 GiBloaded only for the final decode
audio VAE (fp32)0.56 GiBsame
total38.14 GiBnever resident at the same time

So the 38 GiB is not a bill you pay all at once. It is four tenants taking turns in one room.

Residency across the three phases: only the text encoder is on the card while the prompt is read, then the DiT plus activations fill all 22 GiB during sampling, and the two VAEs come last

What decides whether this card works is the middle row: the DiT sits there at 19.53 GiB and the card holds 22. The remaining 2.47 GiB has to absorb every intermediate tensor sampling produces, and that margin explains everything else in this article.

I already had this model running on a 5090 — that write-up covers getting MiniMax-H3 running on a single RTX 5090 and is the prerequisite for this one. Here it runs on a modded RTX 2080 Ti 22G: Turing, sm_75, 22,528 MiB, a card that shipped in 2018 and had its memory chips swapped by someone in Shenzhen.

Config first, then the measured speed and quality, then how it got there.

The config that runs it

These three versions are welded together — upgrade any one and SageAttention stops building:

torch 2.6.0+cu124 · triton 3.2.0 · sageattention 1.0.6

python main.py --listen 127.0.0.1 --port 8188 \
  --reserve-vram 2 --use-sage-attention     # a 15-second clip needs 4
diffusion      minimax_h3_fl2va_pruned_int8_convrot.safetensors    ← INT8, not W4A4
text encoder   qwen3vl_32b_heretic_convrot_w4a4.safetensors        ← W4A4 is fine here
VAEs           minimax_h3_video_vae_fp16 + minimax_h3_audio_vae_fp32
sampler        res_multistep + simple · 20 steps · fps 24
resolution     generate at 960×540 → upscale to 1920×1080
rescale        SCALE = 1.0 (no rescaling needed once INT8 replaced W4A4)

Three of those choices are load-bearing, and each one is a place I lost time.

Why --reserve-vram and not something else

The flag reads like "memory held back for the operating system," and it is that — but on this card it is simultaneously the budget the sampler gets for activations. Both meanings share one number and they pull in opposite directions.

Three values on the same 15-second clip. Only one of the three is a real failure:

--reserve-vramoutcomecost
2torch.OutOfMemoryError after 43.5 s, peak 21,948 / 22,528 MiBfails
4fits1,383.0 s
89.5 GB of weights evicted to streamingover 5 min per step

Reserve too little and the activations have nowhere to go. Reserve too much and the weights stop fitting, so they stream in from host memory every step. Short clips run happily at 2; a 15-second clip needs 4. This is the one number you will keep adjusting, and it scales with clip length rather than resolution.

22.5 GB has to hold weights and activations at once: reserve 2 and activations overflow, reserve 8 and weights get evicted, 4 is where both fit

The transferable part: an OOM does not mean it doesn't fit. It means it doesn't fit under the current allocation policy.

Why the DiT must be INT8 while the text encoder can be W4A4

Same quantization format, two models in the same pipeline, opposite verdicts:

modelat W4A4verdict
diffusion transformerrelative error 0.2005 vs INT8's 0.0110full-frame colour tearing — unusable
text encoder Qwen3-VL-32B24 GiB → 13.2 GiB, worth 8.6% wall clockfree money

The DiT is where error compounds, once per step across 20 steps. The text encoder produces conditioning that is consumed once and never iterated on, so the same error has nowhere near the leverage. "W4A4 is bad" is not a portable conclusion — where the tensor sits in the graph decides it.

Why SageAttention stays on and the log noise stays ignored

With the flag on, every attention call logs this:

[ERROR] Error running sage attention: out of resource: shared memory,
Required: 67584, Hardware limit: 65536 ... using pytorch attention instead

while the startup banner says Using sage attention. It looks broken. The seconds say otherwise:

buildseconds
SageAttention on (2 fallbacks per run)133.37fastest
SDPA (flag off)160.2516.8% slower

💡 A fallback line is not a failure. sm_75 exceeds the 64KB shared-memory limit only on some attention shapes; every other call still takes the fast path. Verify with seconds, never with the presence of a log line.

What it looks like running: 23 minutes, same specs as the 5090

Here is what ffprobe says about the file in the player above, and the render cost that produced it:

output   1920x1080 · 24 fps · 362 frames · AAC 32 kHz stereo · 15.083 s
render   1,383.046 s  (23 min 03 s) · 20 steps · 88.87 s/step

Read the middle column for this card; the right one shows what the same job costs on current silicon:

clipmodded 2080 Ti 22GRTX 5090
15 s · 1080p · audio1,383.0 s314 s
3 s · 960×540 upscaled to 1080p · 73 frames253.8 s

That makes the 2080 Ti 4.4× slower. It is not incapable, just slow, and the two lead to completely different decisions. "This card can't do it" makes you buy a 5090. "This card takes 23 minutes" makes you queue a batch before bed and collect it in the morning.

The surprise is in the upscale stage. RTX Video Super Resolution runs on this card, and it is not the bottleneck:

cardframesupscale timeper frame
RTX 509036211.17 s30.9 ms
modded 2080 Ti 22G733.45 s47.3 ms

1.53× slower per frame, on a card seven years older — a much smaller gap than the 4.4× on generation. I isolated the upscale stage with the cache-separation method from the 5090 speedup article.

The two stages measured separately: 4.4x slower on generation but only 1.53x on upscaling — the old card loses on sampling, not on the whole pipeline

🔊 Sound on. Same seed, same prompt, only the upscale node differs. Left: Real-ESRGAN. Right: RTX VSR. Both rendered on the modded 2080 Ti.

⚠️ One thing I cannot explain: the quality-mode ranking is inverted between the two cards. On the 5090, ULTRA produces the largest file, which is what you would expect when it keeps the most detail. On the 2080 Ti, ULTRA produces the smallest — 643 KB against HIGHBITRATE_ULTRA's 703 KB. Pick the mode by measuring on your own machine rather than copying mine.


Deep dive: how that config got there

Skippable — everything above is the complete working setup. This half is the road taken to get there, including one branch I followed all the way to the end before realising it was the wrong one.

The longest detour: seeing the fallback and trying to fix it

The reasoning went like this:

  1. Every attention call logs a fallback, while the banner claims SageAttention is on
  2. So fix it, and SageAttention runs at full speed
  3. Derive where Required: 67584 comes from, halve BLOCK_M from 128 to 64 — the message really does disappear
  4. Measure: 176.2 s, slower than doing nothing at all

Halving BLOCK_M doubles the block count, and twice the scheduling and boundary handling costs more than the fast path saves. The unpatched build takes the large-block fast path on most calls and pays SDPA's price on a few; the patched build pays a small penalty on every call. Two fallbacks are cheaper than a globally worse tiling.

An earlier judgment went down with it. I had used the patched build to represent "SageAttention", concluded it was useless on Turing, and wrote that down. The untouched build was the fastest of the three the whole time.

buildseconds
SageAttention untouched (2 fallbacks)133.37fastest
SDPA (flag off)160.25baseline
SageAttention, patched kernel (0 fallbacks)176.2slowest

Three builds side by side: untouched 133.37 s is fastest, flag off 160.25 s, patched kernel 176.20 s is slowest

The measurement design is the part worth copying. I ran the three arms interleaved — A B C A B C A B C — in a single window, argv byte-identical between arms except for the flag under test. The control arm's range across three runs was 1.15 s, or 0.7%, so a 16.8% effect is 24× the noise floor and needs no statistical argument. Raw: SDPA 160.88 / 159.73 / 160.13; Sage 133.51 / 133.18 / 133.41. Interleaving matters more than repetition count — it is what absorbs thermal drift, which a sequential AAA BBB design folds straight into the effect.

💡 A fallback line is noise, not a symptom. Chasing it into the kernel trades performance for a tidy log.

The derivation below is sound and, as far as I can find, original. Only its conclusion changed — from "I fixed it" to "I should not have."

Where 67,584 comes from

Hardware limit: 65536 is 64KB, Turing's per-SM shared memory. Required: 67584 overshoots by exactly 2,048 bytes — a 3% overshoot, and the kernel is simply unlaunchable. Three turns pinned it:

Turn one: resolution didn't change the number. Three different resolutions, three identical Required: 67584. A shared-memory requirement independent of input size is not a function of sequence length; it is a compile-time constant.

Turn two: the arithmetic matches exactly. SageAttention 1.0.6's h96 kernel defaults to BLOCK_M=128, BLOCK_N=64. The three tiles it stages come to (128×4 + 64×2 + 64×1) × head_dim, or 704 × head_dim:

BLOCK_M × 96 × 4 (fp32) = 128 × 96 × 4 = 49,152
BLOCK_N × 96 × 2 (fp16) =  64 × 96 × 2 = 12,288
BLOCK_N × 96 × 1 (int8) =  64 × 96 × 1 =  6,144
                                  total = 67,584

Exact to the byte, and 96 is the only value that lands there — 128 gives 90,112 and 64 gives 45,056. So whatever attention call reached this kernel had head_dim 96, which is precisely what core.py:98's if headdim == 96: routes here.

⚠️ That call is not the main DiT. MiniMax-H3's official transformer config says attention_head_dim = 128, so the 96 comes from somewhere else in this pipeline. I have not tracked down where.

Root cause: SageAttention 1.0.6 has no architecture branch. Its block sizes assume far more shared memory than Turing's 64KB (Ampere is 100-164KB depending on the part, Hopper 228KB), and 64KB is never checked for.

The three terms sum to exactly 67,584, which overshoots Turing's per-SM 65,536 by 2,048 bytes, so every call falls back to SDPA

Turn three: half of the error message's own advice is wrong. It suggests "Reducing block sizes or num_stages". I changed num_stages from 4 to 2 and the requirement did not move — not one byte, which follows directly from the arithmetic: num_stages is not in it.

Right parameter, wrong file

I halved BLOCK_M, restarted, and got the identical 67,584. My first read was that the derivation was wrong.

It wasn't. core.py:98 routes the call to the h96 kernel and I had been editing the head_dim 128 one. Then I hit the same wall again: editing only h96 also did nothing. All six kernels under sageattention/ need the change, and it only takes effect after clearing ~/.triton/cache and the __pycache__ directories — Triton caches compiled kernels by source hash, and a stale entry reproduces the old requirement perfectly.

💡 "My change had no effect" has at least two causes — wrong parameter and wrong file — and they present identically. Distinguishing them costs one deliberate act of sabotage: break the file you think you are editing badly enough that it must throw, and see whether it does.

I misjudged RTX VSR too

I wrote off RTX Video Super Resolution on this card before ever running it, on the strength of an old Maxine SDK support table full of A100 and H100-class parts.

Wrong yardstick. The current nvidia-vfx wheel supports Turing through Blackwell. It installed and it ran — the 47.3 ms/frame above is it.

Why W4A4 does not work

Included so nobody has to walk it again. The reasoning went: the INT8 DiT is 19.53 GiB and fills the card almost completely — and Turing has an INT4 path, so W4A4 would halve it again.

  • Straight quantization blew up. The weights overflow INT4's representable range and it will not run at all.
  • So it needs rescaling. SCALE at 1, then 1/16, 1/64, 1/256 — eventually it ran to completion.
  • The video came out, and it was unusable: colour tearing across the whole frame, neon bokeh smeared away.
  • The microbenchmark matches what the eye sees: relative error 0.2005 against INT8's 0.0110, 18× worse.

⚠️ This is as far as I took it. All four SCALE values broke in the identical way and removing the upscaler did not help either, so I stopped there and went back to INT8. That is not the same as proving W4A4 can never work on this DiT — only that none of the paths I tried produced a usable frame. Those weights are deleted from my machine.

FAQ

Can you run MiniMax-H3 on a 22GB GPU?
Yes. A modded RTX 2080 Ti 22G (sm_75, 22,528 MiB) rendered a 15-second 1080p clip with audio in 1,383 seconds — 23 minutes — at 88.87s per step. It needs an INT8 diffusion transformer, a W4A4 text encoder, and ComfyUI started with --reserve-vram 4. The four files are 38 GiB on disk but they take turns; they are never all resident.
What does --reserve-vram actually do in ComfyUI?
It is not only memory held back for the OS — it is simultaneously the budget the sampler has for activations. Set it too low and the activations have nowhere to go, so you get an OOM mid-render; set it too high and model weights get evicted to streaming and every step crawls. At 2 a 15-second clip OOMs; at 4 it works; at 8, 9.5GB of weights gets evicted and every step takes over five minutes.
Is SageAttention worth using on an RTX 2080 Ti (sm_75)?
Yes — 16.8% faster than PyTorch SDPA in an interleaved A/B, 133.37s versus 160.25s, against a control-arm noise floor of 0.7%. It logs a shared-memory fallback on some attention calls and silently takes the fast path on all the others. Judge it by the wall-clock seconds, not by whether the fallback line appears.
Does RTX Video Super Resolution work on Turing GPUs?
Yes. The current nvidia-vfx wheel supports Turing through Blackwell, despite older Maxine SDK support tables that list only datacenter parts. Isolating the upscale stage, a 2080 Ti runs 47.3 ms/frame against a 5090's 30.9 ms/frame — 1.53× slower on a card seven years older. The quality-mode ranking differs between the two cards, so measure on your own machine.
Should I quantize a video diffusion model to W4A4?
Not the diffusion transformer. On MiniMax-H3 the W4A4 DiT had a microbenchmark relative error of 0.2005 against INT8's 0.0110 — 18× worse — and produced full-frame colour tearing that no rescaling factor fixed. The text encoder is a different story: Qwen3-VL-32B at W4A4 drops from about 24GiB to 13.2GiB and cuts wall-clock time by 8.6% with no visible quality cost, because it only produces conditioning.

Read next

  • 2026-07-18
    [Just for Fun — Advanced] Two Identical 2080 Tis, One 3.4× Slower — the Culprit Was a Single dtype Log Line

    The same modded 2080 Ti 22G ran Z-Image 3.4× slower on one machine than another. Not the hardware, not the OS, not a missing package — a Turing bf16→fp32 fallback hiding in one log line. One flag fixed it.

  • 2026-07-10
    [Just for Fun — Advanced] 0xc0000409: When My AI Service Died Silently and the Log Ate the Evidence

    A local brain on a headless Windows box kept dying under real load: the client caught a brief 503, the service quietly restarted itself, and the application log was blank. The worst part wasn't the crash — it was that my own log truncated the one line that explained it on restart. This is the hunt to dig that reason back out from under its own log: why a self-restarting service is the best at burying the reason for its own crash, why you go to the OS-layer Event Log first, and what 0xc0000409 actually means. Full disclosure: I haven't 100% pinned the root cause — this is an open investigation, not a closed case.

  • 2026-07-03
    [Just for Fun — Advanced] I Doubled My Agent's Decode Speed and It Got Slower: TTFT Is the Number You Actually Feel

    I swapped my home agent's brain for one that decodes 30-40 tok/s instead of 14, and it felt slower. The number I'd stared at for a year — tok/s — only measures how fast tokens come out, not how long before they start. On a hybrid model, a single cache miss re-prefills the entire prompt: same box, same brain, 2.6s warm vs 216s cold. Here's the live log.

  • 2026-07-02
    [Just for Fun — Advanced] Progressive Streaming on a Slow Model Got My Bot Rate-Limited by Telegram

    To ease the wait on a pokey local agent, I turned on Telegram streaming — which, the way this bot did it, means rewriting the same message every fraction of a second. On a 14 tok/s brain, a single 175-second reply works out to an estimated couple hundred edit requests, which slammed into Telegram's flood control and got the whole bot benched for four minutes — final answer included. The short, ugly lesson: slow models should not fake streaming with edits. Send the finished answer once. Live logs inside.

Don't miss the next one

Subscribe, and you won't.

One-click unsubscribe anytime.