[diffusion] docs: add tuning guide for h3 on consumer-level gpu (#35816)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mick
2026-08-22 23:48:40 +08:00
committed by GitHub
co-authored by Claude Opus 5
parent b98d472158
commit 7d22b7a875
4 changed files with 389 additions and 27 deletions
@@ -1197,6 +1197,187 @@ peak per GPU.
| prefetch 2, resident 20 | 43.37 s | 78.06 s | 27.5 GiB | No measurable gain | | prefetch 2, resident 20 | 43.37 s | 78.06 s | 27.5 GiB | No measurable gain |
| Ulysses2, prefetch 2, resident 10 | Did not reach warmup | — | — | Rejected | | Ulysses2, prefetch 2, resident 10 | Did not reach warmup | — | — | Rejected |
### Consumer GPU tuning
On consumer hardware the binding question is not which card you have but how much
host RAM sits behind it. H3's weights are about 108 GB — 61.73 GB of DiT and
46.18 GB of text encoder — so no consumer configuration holds them all, and where
the shortfall lands decides the throughput.
**The command** — most consumer machines need exactly one flag beyond the model:
```bash consumer single GPU, lossless
sglang serve --model-path MiniMaxAI/MiniMax-H3 --model-variant fl2va \
--layerwise-offload-components dit,text_encoder,vae
```
With 16 GB of VRAM or more, add `--layerwise-resident-layers video_vae=36` for
the 13 s decode; with ~96 GB of host RAM and 16 GB+ of VRAM, add
`--dit-layerwise-resident-layers 4` for the 6 s step. That is the whole flag
surface. The [builder at the top of this page](#1-quick-start) has consumer
cards and a Host RAM selector: pick your budget and it emits this command with
your tier's measured expectations attached as comments. The table below is the
same data in one view.
**Two budgets, and what each one buys**
| | 12 GB VRAM + 32 GB host | host free, VRAM 16 GB |
| --- | ---: | ---: |
| Recipe | A | B |
| Peak VRAM | ≤ 12 GiB | ≤ 16 GiB (OOMs at 12) |
| Host anonymous (must fit) | 24.5 GiB | 116.7 GB pinned |
| Denoise, 864×480 / 124 frames / 20 NFE | 16.8 - 18.7 s/it | **6.01 s/it** |
| Runs at all | yes | yes |
The left column is one configuration measured twice, at 318.94 s and 356.37 s;
the 12% spread tracked host load on a shared machine, so treat smaller
differences than that as unresolved. The right column is 120.92 s at a 16 GiB
allocator cap. Four resident DiT layers is what Recipe B buys its speed with,
and it is also why 12 GiB is not enough for it.
Read the host row carefully, because the two numbers are not the same kind of
memory. *Anonymous* host memory — pinned buffers and pageable copies — has to fit,
and the kernel cannot reclaim it. Page cache backing a file mapping is
*droppable*, so it does not count against the budget even though it shows up in
`VmRSS`; use `RssAnon` from `/proc/<pid>/status` when checking. Likewise measure
VRAM with `torch.cuda.set_per_process_memory_fraction` and let the allocator fail,
rather than reading `nvidia-smi`, which reports the caching allocator's reserved
pool and overstates the requirement.
Inside 32 GB the weights cannot be pinned, so each denoise step copies about
60 GiB from the checkpoint mapping, and a mapped source is synchronous however
the copy is requested: the driver stages it through its own buffer, so the
transfer neither overlaps compute nor runs at pinned bandwidth. That is where
the step goes, and giving the host room to pin the weights instead is what takes
it to 6.01 s.
Two caveats on the constrained number, both from instrumenting the run rather
than from arithmetic. The machine it was measured on has 2 TB of host memory, so
the kernel kept all 107.7 GiB of mapped checkpoint pages resident: major faults
across a whole request were 6, and `read_bytes` was zero. Nothing was read from
disk. A real 32 GB host cannot cache 107.7 GiB, so it will fault and re-read,
and should be expected to be slower than the figures here rather than equal to
them — an NVMe is a requirement, not a recommendation. Measure your own machine
with major faults (`/proc/<pid>/stat`) on the worker process, not on the
launcher, which holds no weights.
**Recipe A — fits 12 GB VRAM + 32 GB host**
```bash 12 GB + 32 GB, lossless
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
sglang serve --model-path MiniMaxAI/MiniMax-H3 --model-variant fl2va \
--performance-mode memory \
--layerwise-offload-components dit,text_encoder,vae \
--layerwise-resident-layers video_vae=36
```
**Recipe B — host memory is free (the fast path)**
```bash unconstrained host, lossless
sglang serve --model-path MiniMaxAI/MiniMax-H3 --model-variant fl2va \
--performance-mode memory \
--layerwise-offload-components dit,text_encoder,vae \
--dit-layerwise-resident-layers 4 \
--layerwise-resident-layers video_vae=36
```
Recipe B pins ~112 GB of host memory (DiT 61.56 GB, text encoder 46.18 GB, VAE
~4.5 GB in its decode dtype). Do not reach for it on a 32 GB machine.
**What not to change, and why**
- `video_vae=36` holds every decoder block for the decode only — residency
arms at the decoder's first block and releases when it finishes, so the
denoise still runs on an empty card. It fits 12 GB because decoder weights
are held in their decode compute dtype (fp16) from load, which halves them
to ~4.9 GiB; the rounding was already part of every output (the decode
computes in fp16 autocast), so the result is bit-identical, and the decode
drops from 60 s streamed (or 209 s on a busy host) to ~10 s. The
`expandable_segments` line stays: the decode sits close enough to the cap
that fragmentation otherwise tips it over.
- Leave `--enable-torch-compile` off, as elsewhere on this page. Layerwise offload
rebinds `param.data` on every layer, so compiled graphs do not get the benefit
they would on resident weights.
- Recipe A's flags are what the automatic policy should choose on its own. Until
the model declares its own placement, `--performance-mode memory` plus the
explicit component list is what makes it happen; pass them.
**Reading the startup log**
The server prints the memory decisions it made; checking three lines against
your budget catches a mis-set machine in the first minute instead of the first
request.
- `Layerwise offload: host memory available: N GiB` — what the runtime sees
after loading, not your DIMM size. On a 32 GB host expect single digits here;
a much larger number means another process's memory accounting (or a
container limit) is in play.
- `leaving N GiB of weights on the checkpoint mapping` — the expected line on a
32 GB host: the DiT streams from the checkpoint file. If instead the log
reports pinned weights, the runtime decided your host has room — which is
faster, and means the 32 GB figures above do not apply to you.
- `Loaded video_vae: ... host mmap` vs `host pageable` — where the VAE landed
(decoder weights are ~4.9 GiB once held in their decode dtype).
`Loaded <component>` lines carry the same buckets for every component.
If a request dies after the denoise finishes, it is the decode colliding with
the cap: keep the `expandable_segments` line, and if it persists drop to
`video_vae=24` and take the partially streamed decode.
**Against ComfyUI, on the same weights**
Same unpruned bf16 checkpoints, same card, same sampler settings (cfg 1.0,
euler_ancestral, sigma shift 12.0/3.0, seed 1101), 864×480 / 124 frames / 20 NFE:
When host memory is free, the engines are close and sglang is ahead:
| | denoise | host anonymous | peak VRAM |
| --- | ---: | ---: | ---: |
| sglang, Recipe B | **6.01 s/it** | 116.7 GB pinned | ≤ 16 GiB |
| ComfyUI KSampler | 6.58–6.59 s/it | 116.5 GiB | 13048 MiB |
Inside 12 GB, both engines run these weights, and one measurement convention
matters on each side. ComfyUI's memory manager reads system RAM and adapts, so
the rows below patch `psutil` to a pretend host size — the same convention the
sglang rows use. Its `--reserve-vram` is also soft: told to keep 12 GiB free it
still peaked at 13.5 GiB, a figure a real 12 GB card cannot give it, so both
engines here run under the same hard allocator cap
(`set_per_process_memory_fraction`), where its peak stays at 12.1–12.3 GiB.
Under that cap, Recipe A wins the whole request at every host size:
| 12 GB VRAM, both engines hard-capped | sglang Recipe A (TE + denoise + decode) | ComfyUI, bf16 (warm) |
| --- | ---: | ---: |
| 32 GB host | 12.4 + 212.8 + 9.4 ≈ **235 s** | 276–302 s |
| 48 GB host | 15.8 + 192.1 + 10.0 ≈ **218 s** | 246–267 s |
| 64 GB host | 7.5 + 162.4 + 10.3 ≈ **180 s** | 194–195 s |
Same GPU, same load window, unpruned bf16 checkpoints, outputs verified. The
VRAM axis holds too: capped at 16 GiB the same recipe wins ~250 vs 292–301 s,
and at 24 GiB (with `--dit-layerwise-resident-layers 10`, which only a 24 GB
card has headroom for) ~230 vs 249–260 s. Four changes carry it: the VAE staying on its checkpoint mapping (#35862, root fix
#35946), per-layer pinning with net-cost accounting (#35867), the courier
thread that ships still-mapped layers through pinned slots (#35882), and
decoder weights held in their decode dtype from load (#35967) — which is what
lets `video_vae=36` fit and turns the decode from the slowest stage (54–96 s
streamed) into the fastest (~10 s, faster than ComfyUI's own 15–25 s). Output
equivalence is bit-level: the fp16-held decode reproduced the fp32-store run's
video byte for byte, and the audio stream is bit-identical.
Stage by stage under the cap: text encoding is even (both stream the same
48 GB Qwen3VL), the denoise leads at 32–48 GB hosts and sits within
run-to-run variance of ComfyUI at 64 GB (162 vs 159 s), and the decode leads
everywhere. Two ComfyUI notes that still matter: `--fast-disk` measured no
faster than its default here, and stacking
`--novram --cache-none --disable-pinned-memory` made things strictly worse
(69.1 GiB anonymous, 750 s requests) — the adaptive default is the right
configuration on a small host.
The path ComfyUI ships for 12 GB cards uses
`minimax_h3_fl2va_pruned_int8_convrot` and `qwen3vl_32b_minimax_h3_nvfp4_awq`,
i.e. an int8 DiT and an NVFP4 text encoder, and its pruned bf16 file is 40.2 GB
against the unpruned 66.3 GB. Those are different weights, so it is not a
like-for-like comparison with the recipes above.
### RTX 4090 24 GB single-GPU run ### RTX 4090 24 GB single-GPU run
One RTX 4090 D 24 GB completed the 1344×768, 107-frame, 20-NFE T2VA One RTX 4090 D 24 GB completed the 1344×768, 107-frame, 20-NFE T2VA
+9
View File
@@ -272,8 +272,17 @@ for (const path of walk(CONFIGS)) {
if (!Array.isArray(errors) || errors.length) { if (!Array.isArray(errors) || errors.length) {
fail(where, `verifiedRecipes[${index}] fails topology validation: ${(errors || []).join("; ")}`); fail(where, `verifiedRecipes[${index}] fails topology validation: ${(errors || []).join("; ")}`);
} }
// A recipe may carry `unverified: true`: it supplies the card's default
// shape without claiming a verification run, and must resolve that way.
if (recipe.unverified) {
const resolved = validateResolved(selection, `verifiedRecipes[${index}]`);
if (resolved && resolved.builder.verification?.serve === "verified") {
fail(where, `verifiedRecipes[${index}] is declared unverified but resolves as verified`);
}
} else {
validateResolved(selection, `verifiedRecipes[${index}]`, true); validateResolved(selection, `verifiedRecipes[${index}]`, true);
} }
}
// H3's architectural contract is important enough to pin directly: exact // H3's architectural contract is important enough to pin directly: exact
// platform recipes, legal custom admission, and each invalidity family. // platform recipes, legal custom admission, and each invalidity family.
+13 -3
View File
@@ -1504,6 +1504,16 @@ export const Deployment = ({ config, benchmarks }) => {
ring_degree: resourcesFollowPlatformDefault ring_degree: resourcesFollowPlatformDefault
? (nextRecipe?.ring_degree ?? 1) ? (nextRecipe?.ring_degree ?? 1)
: next.ring_degree, : next.ring_degree,
// Placement and encoder are per-hardware recipe facts just like
// the resource shape: keeping the previous card's picks produces
// a command the new card cannot run (e.g. a resident 61.7 GB DiT
// on a single consumer GPU) shown as "unverified".
placement: resourcesFollowPlatformDefault
? (nextRecipe?.placement || "auto")
: next.placement,
encoder: resourcesFollowPlatformDefault
? (nextRecipe?.encoder || "auto")
: next.encoder,
}; };
} }
return reseatHiddenPicks(normalizeBuilderSelection(next)); return reseatHiddenPicks(normalizeBuilderSelection(next));
@@ -1829,7 +1839,7 @@ export const Deployment = ({ config, benchmarks }) => {
{/* This is the verified operating point, not sizing advice — a {/* This is the verified operating point, not sizing advice — a
hardware whose validation ran on 8 GPUs is not "recommending" hardware whose validation ran on 8 GPUs is not "recommending"
8 over a smaller deployment. */} 8 over a smaller deployment. */}
<span>Verified recipe · {sel.hw.toUpperCase()}</span> <span>{recommendedRecipe.unverified ? "Derived recipe" : "Verified recipe"} · {sel.hw.toUpperCase()}</span>
<strong> <strong>
{[ {[
`${recommendedRecipe.nodes * recommendedRecipe.gpus_per_node} GPUs`, `${recommendedRecipe.nodes * recommendedRecipe.gpus_per_node} GPUs`,
@@ -1841,10 +1851,10 @@ export const Deployment = ({ config, benchmarks }) => {
</strong> </strong>
</div> </div>
<div> <div>
{renderStatus("verified")} {renderStatus(recommendedRecipe.unverified ? "unverified" : "verified")}
{recommendedInUse {recommendedInUse
? <small>In use</small> ? <small>In use</small>
: <button type="button" className="sgd-builder-text-action" onClick={restoreRecommendedRecipe}>Use verified recipe</button>} : <button type="button" className="sgd-builder-text-action" onClick={restoreRecommendedRecipe}>{recommendedRecipe.unverified ? "Use derived recipe" : "Use verified recipe"}</button>}
</div> </div>
</section> </section>
)} )}
@@ -6,7 +6,111 @@
// deployment command engine. // deployment command engine.
export const config = { // Single-GPU consumer cards run H3 lossless through layerwise offload. The
// flags carry only what differs from the defaults; what changes with the
// machine is the expectation, which the hints spell out per budget. Measured
// on one RTX 4090 (denoise medians across interleaved runs, outputs verified
// end to end); 48-64 GB hosts sit between the measured points.
export const config = (() => {
// One recipe per VRAM tier, measured under a hard allocator cap of that
// size: the figures were taken at 12/16/24 GiB caps, so every card of a
// tier shares them. 30-series cards run the same recipe; their step times
// land above the measured 40/50-series figures.
const CONSUMER_12G = ["rtx4070", "rtx5070", "rtx3060"];
const CONSUMER_16G = ["rtx4080", "rtx5080", "rtx5070ti", "rtx4060ti"];
const CONSUMER_24G = ["rtx4090", "rtx3090"];
// Workstation cards a home builder can actually buy. No hard-cap anchor was
// measured for these sizes (the lab card is 24 GB and caps only shrink), so
// their recipes are derived from the tier logic, not verified runs.
const WORKSTATION_48G = ["rtx6000ada"];
const WORKSTATION_96G = ["rtxpro6000"];
const CONSUMER_SINGLE = [
...CONSUMER_12G,
...CONSUMER_16G,
...CONSUMER_24G,
...WORKSTATION_48G,
...WORKSTATION_96G,
];
const CONSUMER_VRAM_16_PLUS = [...CONSUMER_16G, ...CONSUMER_24G];
const CONSUMER_AMPERE = ["rtx3060", "rtx3090"];
function consumerFlags(s) {
if (WORKSTATION_96G.includes(s.hw)) return workstation96Flags();
// The whole video decoder held for the decode only: residency arms at the
// decoder's first block and releases when it finishes, so the denoise still
// runs on an empty card. All 36 blocks fit 12 GB because decoder weights are
// held in their decode compute dtype (fp16, ~4.9 GiB) from load -- the
// rounding was already in every output, so the result is bit-identical --
// and the decode drops from 60 s streamed to ~10 s.
const flags = [
"--performance-mode memory",
"--layerwise-offload-components dit,text_encoder,vae",
"--layerwise-resident-layers video_vae=36",
];
if (CONSUMER_VRAM_16_PLUS.includes(s.hw) && s.host_ram === "ram96") {
flags.push("--dit-layerwise-resident-layers 4");
}
// A 24 GB card on a 32 GB host has allocator headroom to keep ten DiT
// layers resident (measured 10.4 vs 11.6 s/step); a 16 GB card does not --
// there even four resident layers measured slower than none, so it keeps
// the plain recipe.
if (CONSUMER_24G.includes(s.hw) && s.host_ram === "ram32") {
flags.push("--dit-layerwise-resident-layers 10");
}
if (WORKSTATION_48G.includes(s.hw)) {
flags.push("--dit-layerwise-resident-layers 40");
}
return flags;
}
function workstation96Flags() {
// 96 GB holds the whole 61.7 GB DiT; only the encoders and VAEs step aside.
return [
"--performance-mode memory",
"--layerwise-offload-components text_encoder,vae",
"--layerwise-resident-layers video_vae=36",
];
}
function consumerHints(s) {
const hints = [];
const bigHost = s.host_ram === "ram96";
const midHost = s.host_ram === "ram64";
if (bigHost) {
if (CONSUMER_VRAM_16_PLUS.includes(s.hw)) {
hints.push("verified end to end: ~6 s per denoise step, 13 s decode");
} else {
hints.push("~6 s per step once the host pins the DiT; the decode holds all 36 blocks in their fp16 decode dtype and takes ~10 s");
}
return hints;
}
if (CONSUMER_24G.includes(s.hw)) {
hints.push("measured at 32 GB host: ~10.4 s per denoise step with ten resident layers, ~9.6 s decode, ~230 s per request -- ahead of ComfyUI (249-260 s) under the same hard 24 GiB cap");
} else if (CONSUMER_16G.includes(s.hw)) {
hints.push("measured at 32 GB host: ~11.9 s per denoise step, ~11 s decode, ~250 s per request -- ahead of ComfyUI (292-301 s) under the same hard 16 GiB cap");
} else {
hints.push("measured at 32 GB host: ~10.6 s per denoise step, ~9.4 s decode, ~235 s per request -- ahead of ComfyUI (276-302 s) on the same weights under the same hard 12 GiB cap, output bit-identical");
}
if (CONSUMER_AMPERE.includes(s.hw)) {
hints.push("the recipe and its memory behavior are tier-exact for this card; the step times above were measured on 40-series compute, and Ampere lands above them");
}
if (WORKSTATION_96G.includes(s.hw)) {
hints.push("derived recipe, not yet verified: 96 GB holds the whole 61.7 GB DiT resident, so only the text encoder and VAEs stream -- expect near-datacenter step times rather than the offload figures above");
}
if (WORKSTATION_48G.includes(s.hw)) {
hints.push("derived recipe, not yet verified: 48 GB holds forty of the fifty DiT layers; the figures above are the 24 GB tier's and this card should land well under them");
}
hints.push("run with PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True -- the decode sits close enough to the cap that fragmentation otherwise tips it over");
if (midHost) {
hints.push("measured on a 12 GB card at a 48 GB host: ~9.6 s/step, ~218 s per request (ComfyUI 246-267 s); at 64 GB: ~8.1 s/step, ~180 s (ComfyUI 194-195 s); larger cards land at or below these");
} else {
hints.push("a 32 GB host cannot cache the 108 GB checkpoint: NVMe is required, and real runs land above the quoted step time");
}
hints.push('the startup log should say "leaving ... GiB of weights on the checkpoint mapping" -- if it does not, the host is not the constraint you set');
return hints;
}
return {
modelName: "MiniMax-H3", modelName: "MiniMax-H3",
supportedHardware: [ supportedHardware: [
@@ -16,16 +120,51 @@ export const config = {
"h100", "h100",
"mi300x", "mi300x",
"mi355x", "mi355x",
"rtxpro6000",
"rtx6000ada",
"rtx5090", "rtx5090",
"rtx4090",
"rtx3090",
"rtx5080",
"rtx5070ti",
"rtx4080",
"rtx4060ti",
"rtx5070",
"rtx4070",
"rtx3060",
], ],
hardware: [ hardware: [
{ id: "rtxpro6000", label: "RTX PRO 6000", vram: "96GB", vendor: "consumer" },
{ id: "rtx6000ada", label: "RTX 6000 Ada", vram: "48GB", vendor: "consumer" },
{ id: "rtx5090", label: "RTX 5090", vram: "32GB", vendor: "consumer" }, { id: "rtx5090", label: "RTX 5090", vram: "32GB", vendor: "consumer" },
{ id: "rtx4090", label: "RTX 4090", vram: "24GB", vendor: "consumer" },
{ id: "rtx3090", label: "RTX 3090", vram: "24GB", vendor: "consumer" },
{ id: "rtx5080", label: "RTX 5080", vram: "16GB", vendor: "consumer" },
{ id: "rtx5070ti", label: "RTX 5070 Ti", vram: "16GB", vendor: "consumer" },
{ id: "rtx4080", label: "RTX 4080", vram: "16GB", vendor: "consumer" },
{ id: "rtx4060ti", label: "RTX 4060 Ti", vram: "16GB", vendor: "consumer" },
{ id: "rtx5070", label: "RTX 5070", vram: "12GB", vendor: "consumer" },
{ id: "rtx4070", label: "RTX 4070", vram: "12GB", vendor: "consumer" },
{ id: "rtx3060", label: "RTX 3060", vram: "12GB", vendor: "consumer" },
], ],
groupHardware: false, groupHardware: false,
matchDims: [], matchDims: [],
overlayDims: [ overlayDims: [
{
id: "host_ram",
title: "Host RAM",
scope: "serve",
description: "System memory decides where the DiT weights wait between steps: pinned when they fit, on the checkpoint mapping when they do not.",
default: "ram32",
showWhen: (s) => CONSUMER_SINGLE.includes(s.hw),
options: [
{ id: "ram32", label: "32 GB" },
{ id: "ram64", label: "48-64 GB" },
{ id: "ram96", label: "96 GB+" },
],
},
{ {
id: "weights", id: "weights",
title: "Checkpoint Weights", title: "Checkpoint Weights",
@@ -120,6 +259,7 @@ export const config = {
id: "auto", id: "auto",
label: "Auto", label: "Auto",
flags: (s) => { flags: (s) => {
if (CONSUMER_SINGLE.includes(s.hw)) return consumerFlags(s);
const recipe = config.commandBuilder.resource.verifiedRecipes.find((entry) => const recipe = config.commandBuilder.resource.verifiedRecipes.find((entry) =>
entry.hw === s.hw && entry.nodes === Number(s.nodes) entry.hw === s.hw && entry.nodes === Number(s.nodes)
&& entry.gpus_per_node === Number(s.gpus_per_node)); && entry.gpus_per_node === Number(s.gpus_per_node));
@@ -128,18 +268,19 @@ export const config = {
return placement === "offload" ? [ return placement === "offload" ? [
"--performance-mode memory", "--performance-mode memory",
"--layerwise-offload-components dit,text_encoder,vae", "--layerwise-offload-components dit,text_encoder,vae",
"--dit-offload-prefetch-size 1",
"--dit-layerwise-resident-layers 20", "--dit-layerwise-resident-layers 20",
"--enable-torch-compile false",
] : ["--performance-mode speed"]; ] : ["--performance-mode speed"];
}, },
hints: (s) => (CONSUMER_SINGLE.includes(s.hw) ? consumerHints(s) : []),
description: "Use the recommended placement for the selected hardware and resource shape.", description: "Use the recommended placement for the selected hardware and resource shape.",
}, },
{ {
id: "resident", id: "resident",
label: "Resident", label: "Resident",
flags: ["--performance-mode speed"], flags: ["--performance-mode speed"],
recommendedWhen: (s) => s.hw !== "rtx5090", disabled: (s) => CONSUMER_SINGLE.includes(s.hw),
disableReason: "The 61.7 GB DiT cannot be resident on a single consumer card.",
recommendedWhen: (s) => s.hw !== "rtx5090" && !CONSUMER_SINGLE.includes(s.hw),
description: "Lowest-latency path when the full pipeline fits in aggregate GPU memory.", description: "Lowest-latency path when the full pipeline fits in aggregate GPU memory.",
}, },
{ {
@@ -153,16 +294,18 @@ export const config = {
{ {
id: "offload", id: "offload",
label: "Layerwise offload", label: "Layerwise offload",
flags: [ flags: (s) => {
if (CONSUMER_SINGLE.includes(s.hw)) return consumerFlags(s);
return [
"--performance-mode memory", "--performance-mode memory",
"--layerwise-offload-components dit,text_encoder,vae", "--layerwise-offload-components dit,text_encoder,vae",
"--dit-offload-prefetch-size 1",
"--dit-layerwise-resident-layers 20", "--dit-layerwise-resident-layers 20",
"--enable-torch-compile false", ];
], },
soft: (s) => s.hw !== "rtx5090", hints: (s) => (CONSUMER_SINGLE.includes(s.hw) ? consumerHints(s) : []),
softReason: "Tuned and verified on RTX 5090. It runs on the datacenter GPUs too, where a resident recipe is simply faster.", soft: (s) => s.hw !== "rtx5090" && !CONSUMER_SINGLE.includes(s.hw),
recommendedWhen: (s) => s.hw === "rtx5090", softReason: "Tuned and verified on the consumer cards. It runs on the datacenter GPUs too, where a resident recipe is simply faster.",
recommendedWhen: (s) => s.hw === "rtx5090" || CONSUMER_SINGLE.includes(s.hw),
description: "Capacity-first PCIe path. It is substantially slower than a resident datacenter recipe.", description: "Capacity-first PCIe path. It is substantially slower than a resident datacenter recipe.",
}, },
], ],
@@ -244,7 +387,7 @@ export const config = {
{ {
id: "auto", id: "auto",
label: "Auto", label: "Auto",
flags: (s) => [`--encoder-parallel ${s.nodes > 1 ? "replicate" : "auto"}`], flags: (s) => (s.nodes > 1 ? ["--encoder-parallel replicate"] : []),
recommended: true, recommended: true,
description: "Folds on verified single-host P2P systems and resolves to replicate across nodes.", description: "Folds on verified single-host P2P systems and resolves to replicate across nodes.",
}, },
@@ -382,7 +525,19 @@ export const config = {
{ id: "mi355x-resident-2", hw: "mi355x", nodes: 1, gpus_per_node: 2, placement: "resident", tp_size: 1, ulysses_degree: 2, ring_degree: 1, encoder: "auto" }, { id: "mi355x-resident-2", hw: "mi355x", nodes: 1, gpus_per_node: 2, placement: "resident", tp_size: 1, ulysses_degree: 2, ring_degree: 1, encoder: "auto" },
{ id: "mi355x-resident-4", hw: "mi355x", nodes: 1, gpus_per_node: 4, placement: "resident", tp_size: 1, ulysses_degree: 4, ring_degree: 1, encoder: "auto" }, { id: "mi355x-resident-4", hw: "mi355x", nodes: 1, gpus_per_node: 4, placement: "resident", tp_size: 1, ulysses_degree: 4, ring_degree: 1, encoder: "auto" },
{ id: "mi355x-resident-8", hw: "mi355x", nodes: 1, gpus_per_node: 8, placement: "resident", tp_size: 1, ulysses_degree: 8, ring_degree: 1, encoder: "auto", default: true }, { id: "mi355x-resident-8", hw: "mi355x", nodes: 1, gpus_per_node: 8, placement: "resident", tp_size: 1, ulysses_degree: 8, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx5090-offload-2", hw: "rtx5090", nodes: 1, gpus_per_node: 2, placement: "offload", tp_size: 2, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true }, { id: "rtxpro6000-offload-1", hw: "rtxpro6000", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true, unverified: true },
{ id: "rtx6000ada-offload-1", hw: "rtx6000ada", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true, unverified: true },
{ id: "rtx5090-offload-1", hw: "rtx5090", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx5090-offload-2", hw: "rtx5090", nodes: 1, gpus_per_node: 2, placement: "offload", tp_size: 2, ulysses_degree: 1, ring_degree: 1, encoder: "auto" },
{ id: "rtx4090-offload-1", hw: "rtx4090", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx4080-offload-1", hw: "rtx4080", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx3090-offload-1", hw: "rtx3090", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx5080-offload-1", hw: "rtx5080", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx5070ti-offload-1", hw: "rtx5070ti", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx4060ti-offload-1", hw: "rtx4060ti", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx5070-offload-1", hw: "rtx5070", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx3060-offload-1", hw: "rtx3060", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
{ id: "rtx4070-offload-1", hw: "rtx4070", nodes: 1, gpus_per_node: 1, placement: "offload", tp_size: 1, ulysses_degree: 1, ring_degree: 1, encoder: "auto", default: true },
], ],
autoTopology: (s) => { autoTopology: (s) => {
const recipes = config.commandBuilder.resource.verifiedRecipes; const recipes = config.commandBuilder.resource.verifiedRecipes;
@@ -437,11 +592,16 @@ export const config = {
&& entry.ulysses_degree === topology.ulysses_degree && entry.ulysses_degree === topology.ulysses_degree
&& entry.ring_degree === topology.ring_degree); && entry.ring_degree === topology.ring_degree);
const resolvedPlacement = s.placement === "auto" const resolvedPlacement = s.placement === "auto"
? (automaticRecipe?.placement || (s.hw === "rtx5090" ? "offload" : "resident")) ? (automaticRecipe?.placement
|| (s.hw === "rtx5090" || CONSUMER_SINGLE.includes(s.hw) ? "offload" : "resident"))
: s.placement; : s.placement;
const coverageWarnings = []; const coverageWarnings = [];
if (resolvedPlacement === "offload" && s.hw !== "rtx5090") { if (resolvedPlacement === "offload" && s.hw !== "rtx5090"
coverageWarnings.push("Layerwise offload is tuned and verified on RTX 5090; on this hardware it runs unverified and a resident recipe is faster."); && !CONSUMER_SINGLE.includes(s.hw)) {
coverageWarnings.push("Layerwise offload is tuned and verified on consumer cards; on this hardware it runs unverified and a resident recipe is faster.");
}
if (CONSUMER_SINGLE.includes(s.hw) && s.host_ram === "ram64") {
coverageWarnings.push("48-64 GB hosts sit between the measured 32 GB and 96 GB points and have not been through their own verification round.");
} }
if (resolvedPlacement === "fsdp" && (s.nodes !== 1 || !["b200", "b300", "h200", "h100"].includes(s.hw))) { if (resolvedPlacement === "fsdp" && (s.nodes !== 1 || !["b200", "b300", "h200", "h100"].includes(s.hw))) {
coverageWarnings.push("FSDP outside the single-node NVIDIA recipes runs unverified."); coverageWarnings.push("FSDP outside the single-node NVIDIA recipes runs unverified.");
@@ -465,7 +625,7 @@ export const config = {
&& entry.tp_size === topology.tp_size && entry.tp_size === topology.tp_size
&& entry.ulysses_degree === topology.ulysses_degree && entry.ulysses_degree === topology.ulysses_degree
&& entry.ring_degree === topology.ring_degree); && entry.ring_degree === topology.ring_degree);
const topologyVerified = !!recipe && errors.length === 0; const topologyVerified = !!recipe && !recipe.unverified && errors.length === 0;
const encoderVerified = s.encoder === "auto" const encoderVerified = s.encoder === "auto"
|| s.encoder === recipe?.encoder || s.encoder === recipe?.encoder
|| (s.nodes > 1 && s.encoder === "replicate"); || (s.nodes > 1 && s.encoder === "replicate");
@@ -491,10 +651,11 @@ export const config = {
topologyParts.push(Number(s.nodes) > 1 ? `${s.nodes} nodes` : "Single node"); topologyParts.push(Number(s.nodes) > 1 ? `${s.nodes} nodes` : "Single node");
const world = Number(s.nodes) * Number(s.gpus_per_node); const world = Number(s.nodes) * Number(s.gpus_per_node);
const flags = ["--model-path {{MODEL_NAME}}", `--num-gpus ${world}`]; const flags = ["--model-path {{MODEL_NAME}}"];
if (world > 1) flags.push(`--num-gpus ${world}`);
if (topology.ring_degree > 1) flags.push(`--sp-degree ${world}`); if (topology.ring_degree > 1) flags.push(`--sp-degree ${world}`);
if (topology.tp_size > 1) flags.push(`--tp-size ${topology.tp_size}`); if (topology.tp_size > 1) flags.push(`--tp-size ${topology.tp_size}`);
flags.push(`--ulysses-degree ${topology.ulysses_degree}`); if (topology.ulysses_degree > 1) flags.push(`--ulysses-degree ${topology.ulysses_degree}`);
if (topology.ring_degree > 1) flags.push(`--ring-degree ${topology.ring_degree}`); if (topology.ring_degree > 1) flags.push(`--ring-degree ${topology.ring_degree}`);
flags.push("--host {{HOST_IP}}", "--port {{PORT}}"); flags.push("--host {{HOST_IP}}", "--port {{PORT}}");
@@ -505,7 +666,7 @@ export const config = {
if (resolvedPlacement === "fsdp") { if (resolvedPlacement === "fsdp") {
warnings.push("FSDP lowers resident DiT memory but adds per-block parameter collectives; prefer Resident when the pipeline fits."); warnings.push("FSDP lowers resident DiT memory but adds per-block parameter collectives; prefer Resident when the pipeline fits.");
} }
if (s.hw === "rtx5090") { if (s.hw === "rtx5090" && Number(s.gpus_per_node) === 2) {
warnings.push("The 2× RTX 5090 path requires a 384 GiB-class host and prioritizes capacity over latency."); warnings.push("The 2× RTX 5090 path requires a 384 GiB-class host and prioritizes capacity over latency.");
} }
@@ -781,3 +942,4 @@ export const config = {
cells: [], cells: [],
}; };
})();