diff --git a/docs/assets/cookbook-recipes.json b/docs/assets/cookbook-recipes.json index 11784e087..37aa205e7 100644 --- a/docs/assets/cookbook-recipes.json +++ b/docs/assets/cookbook-recipes.json @@ -1,5 +1,5 @@ { - "version": 7, + "version": 8, "recipes": [ { "id": "fastwan21-t2v", @@ -513,6 +513,71 @@ "The MLX path supports T2VA, optional temporal --fast, optional spatial --fast-spatial, and opt-in VSA on --include-vsa checkpoints. FL2VA, Ref2VA, and two-pass refinement are not wired." ] }, + { + "id": "fasth3-preview-spark", + "group": "fasth3-preview", + "group_label": "FastH3 Preview", + "group_task": "4-step text to video + audio", + "family": "minimax_h3", + "stage": "inference", + "task": "Few-step text to video (with audio)", + "label": "FastH3 Preview on one DGX Spark", + "summary": "Run FastH3 Preview on one GB10 with Triton VSA, FA4 off, and lazy module load. Height, width, frames, and steps in the YAML are examples.", + "model": "FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree", + "source": "examples/inference/basic/basic_fasth3_spark.yaml", + "serving": { + "source": "examples/serving/openai_fasth3_spark.yaml", + "install": "UV_TORCH_BACKEND=cu130 uv pip install -e .", + "env": "FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3" + }, + "command": "FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 FASTVIDEO_STAGE_LOGGING=1 fastvideo generate --config examples/inference/basic/basic_fasth3_spark.yaml", + "gpu_types": ["NVIDIA"], + "hardware": { + "platform": "cuda", + "device": "spark", + "gpu_count": 1, + "evidence": "source-configured" + }, + "evidence": "Source-backed", + "expected_artifact": "MP4 under outputs/fasth3_spark/", + "modes": ["T2VA", "1-Spark"], + "limitations": [ + "Install from the DGX Spark guide, not the generic CUDA extra. GB10 has no FA4 / sm_100a VSA kernel; keep FASTVIDEO_FA4=0 and FASTVIDEO_VSA_SM100A=0.", + "Legal num_frames values are 17n+5, capped at 345 (15 s). Native 16:9 sizes include 832x480 and 1344x768.", + "Lazy module load reloads Qwen3-VL and the DiT between phases of each request. Do not pass --no-lazy-module-load on this box.", + "A 345-frame request on one Spark can OOM. Prefer 124 or 243 frames, TAEH3 decode, or two Sparks over QSFP." + ] + }, + { + "id": "fasth3-spark-pair", + "group": "fasth3-preview", + "group_label": "FastH3 Preview", + "group_task": "4-step text to video + audio", + "family": "minimax_h3", + "stage": "inference", + "task": "Few-step text to video (with audio)", + "label": "FastH3 Preview on two DGX Sparks", + "summary": "Run one FastH3 clip across two GB10s with Ray sequence parallel over QSFP RoCE. Sequential load and lazy module load stay on because SP replicates the DiT on each node.", + "model": "FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree", + "source": "examples/inference/basic/basic_fasth3_spark_pair.yaml", + "command": "source examples/inference/optimizations/spark_pair_env.sh && FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 FASTVIDEO_VAE_PARALLEL_DECODE=1 fastvideo generate --config examples/inference/basic/basic_fasth3_spark_pair.yaml", + "gpu_types": ["NVIDIA"], + "hardware": { + "platform": "cuda", + "device": "spark", + "gpu_count": 2, + "accelerator": "NVIDIA GB10 (DGX Spark pair)", + "evidence": "validated", + "evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/1803" + }, + "evidence": "Verified", + "expected_artifact": "MP4 under outputs/fasth3_spark_pair/", + "modes": ["T2VA", "2-Spark SP"], + "limitations": [ + "Requires a two-node Ray cluster on the QSFP interconnect. There is no cookbook server for this path; use Python / generate.", + "Height, width, frames, and steps in the YAML are examples. Edit them or pass CLI flags. See docs/getting_started/installation/spark_pair.md." + ] + }, { "id": "minimax-h3-fl2va", "family": "minimax_h3", @@ -665,29 +730,6 @@ "evidence": "Source-backed", "limitations": ["The upstream checkpoint must be converted to Diffusers layout via scripts/checkpoint_conversion/convert_mmaudio_to_diffusers.py unless loaded from the FastVideo converted repo as done here."] }, - { - "id": "fasth3-spark-pair", - "family": "minimax_h3", - "stage": "inference", - "task": "Few-step text to video (with audio)", - "label": "FastH3 on two DGX Sparks (sequence parallel)", - "summary": "Run one FastH3 clip across two GB10s with Ray sequence parallel over QSFP RoCE. Sequential load and lazy module load stay on because SP replicates the DiT on each node.", - "model": "FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree", - "source": "examples/inference/basic/basic_fasth3_spark_pair.yaml", - "command": "source examples/inference/optimizations/spark_pair_env.sh && FASTVIDEO_VSA_SM100A=0 FASTVIDEO_FA4=0 FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 FASTVIDEO_VAE_PARALLEL_DECODE=1 fastvideo generate --config examples/inference/basic/basic_fasth3_spark_pair.yaml", - "gpu_types": ["NVIDIA"], - "hardware": { - "platform": "cuda", - "gpu_count": 2, - "accelerator": "NVIDIA GB10 (DGX Spark pair)", - "evidence": "validated", - "evidence_url": "https://github.com/hao-ai-lab/FastVideo/pull/1803" - }, - "evidence": "Verified", - "expected_artifact": "MP4 under outputs/fasth3_spark_pair/", - "modes": ["T2VA", "2-Spark SP"], - "limitations": ["Requires a two-node Ray cluster on the QSFP interconnect. Height, width, frames, and steps in the YAML are examples. Edit them or pass CLI flags. See docs/getting_started/installation/spark_pair.md."] - }, { "id": "matrix-game-2", "family": "matrixgame", diff --git a/docs/assets/cookbook.js b/docs/assets/cookbook.js index 65e80f95f..00c11e595 100644 --- a/docs/assets/cookbook.js +++ b/docs/assets/cookbook.js @@ -89,8 +89,10 @@ const groupIdFor = (recipe) => recipe.group || recipe.id; - const gpuCountLabel = (hardware) => { + const gpuCountLabel = (hardware, runtimeId) => { const count = hardware?.gpu_count; + if (count == null) return ""; + if (runtimeId === "spark") return `${count} Spark${count === 1 ? "" : "s"}`; return `${count} GPU${count === 1 ? "" : "s"}`; }; @@ -139,12 +141,19 @@ }; } const hardware = recipe.hardware || {}; + if (hardware.device === "spark") { + return { + id: "spark", + label: "NVIDIA DGX Spark", + hint: "GB10 · 128 GB unified memory", + }; + } return { id: "cuda", label: "NVIDIA CUDA", hint: hardware.accelerator - ? `${hardware.accelerator} · ${gpuCountLabel(hardware)}` - : `${gpuCountLabel(hardware)} configured · GPU model not recorded`, + ? `${hardware.accelerator} · ${gpuCountLabel(hardware, "cuda")}` + : `${gpuCountLabel(hardware, "cuda")} configured · GPU model not recorded`, }; }; @@ -161,8 +170,11 @@ .filter(Boolean) .join(" · "); } - if (hardware.accelerator) return [hardware.accelerator, gpuCountLabel(hardware)].join(" · "); - return `NVIDIA CUDA · ${gpuCountLabel(hardware)} configured · GPU model and VRAM not recorded`; + if (runtime.id === "spark") { + return [hardware.accelerator || "NVIDIA GB10", gpuCountLabel(hardware, "spark")].filter(Boolean).join(" · "); + } + if (hardware.accelerator) return [hardware.accelerator, gpuCountLabel(hardware, runtime.id)].join(" · "); + return `NVIDIA CUDA · ${gpuCountLabel(hardware, "cuda")} configured · GPU model and VRAM not recorded`; }; const renderHardwareEvidence = (container, badge, recipe) => { @@ -182,15 +194,17 @@ if (isValidated) { const recorded = [ hardware.accelerator, - runtime.id === "cuda" ? gpuCountLabel(hardware) : hardware.system_memory, + runtime.id === "mlx" || runtime.id === "mps" ? hardware.system_memory : gpuCountLabel(hardware, runtime.id), ].filter(Boolean); const statements = [`${recorded.join(" · ")}.`]; if (hardware.minimum_memory) statements.push(`Documented minimum: ${hardware.minimum_memory}.`); if (hardware.peak_memory) statements.push(`Measured: ${hardware.peak_memory}.`); if (!hardware.minimum_memory) statements.push("This recorded device is not a minimum requirement."); details.textContent = ` ${statements.join(" ")}`; + } else if (runtime.id === "spark") { + details.textContent = ` NVIDIA DGX Spark · ${gpuCountLabel(hardware, "spark")}. GB10 has no FA4 / sm_100a VSA kernel; keep Triton VSA and FA4 off.`; } else if (runtime.id === "cuda") { - details.textContent = ` NVIDIA CUDA · ${gpuCountLabel(hardware)}. The source does not record the GPU model or VRAM.`; + details.textContent = ` NVIDIA CUDA · ${gpuCountLabel(hardware, "cuda")}. The source does not record the GPU model or VRAM.`; } else { details.textContent = ` ${runtime.label}. The source does not record a device or memory requirement.`; } @@ -249,6 +263,9 @@ const usage = root.querySelector("[data-cookbook-usage]"); const servingAvailability = root.querySelector("[data-cookbook-serving-availability]"); const knobsContainer = root.querySelector("[data-cookbook-knobs]"); + const deviceRow = root.querySelector("[data-cookbook-device-row]"); + const deviceOptions = root.querySelector("[data-cookbook-device-options]"); + const deviceCaption = root.querySelector("[data-cookbook-device-caption]"); modelOptions.setAttribute("aria-label", "Recipe"); hardwareOptions.setAttribute("aria-label", "Runtime"); @@ -372,16 +389,39 @@ }); }; + const recipesForRuntime = (runtimeId, groupRecipes) => + groupRecipes.filter((item) => runtimeFor(item).id === runtimeId); + + const uniqueRuntimeIds = (groupRecipes) => { + const ids = []; + groupRecipes.forEach((item) => { + const id = runtimeFor(item).id; + if (!ids.includes(id)) ids.push(id); + }); + return ids; + }; + + const pickRecipeForRuntime = (runtimeId, preferredCount, groupRecipes) => { + const siblings = recipesForRuntime(runtimeId, groupRecipes); + if (!siblings.length) return null; + if (preferredCount != null) { + const match = siblings.find((item) => item.hardware?.gpu_count === preferredCount); + if (match) return match; + } + return siblings.find((item) => item.hardware?.gpu_count === 1) || siblings[0]; + }; + const renderRuntimeOptions = () => { const groupRecipes = groups.get(selectedGroupId) || []; - const renderedIds = [...hardwareOptions.querySelectorAll("[data-recipe-id]")].map((option) => option.dataset.recipeId); - if (renderedIds.join(",") === groupRecipes.map((recipe) => recipe.id).join(",")) return; + const runtimeIds = uniqueRuntimeIds(groupRecipes); + const renderedIds = [...hardwareOptions.querySelectorAll("[data-runtime-id]")].map((option) => option.dataset.runtimeId); + if (renderedIds.join(",") === runtimeIds.join(",")) return; hardwareOptions.replaceChildren(); - groupRecipes.forEach((recipe) => { - const runtime = runtimeFor(recipe); + runtimeIds.forEach((runtimeId) => { + const representative = pickRecipeForRuntime(runtimeId, 1, groupRecipes); + const runtime = runtimeFor(representative); const option = document.createElement("button"); option.type = "button"; - option.dataset.recipeId = recipe.id; option.dataset.runtimeId = runtime.id; option.setAttribute("aria-pressed", "false"); const optionLabel = document.createElement("strong"); @@ -393,6 +433,49 @@ }); }; + const renderDeviceOptions = (recipe) => { + if (!deviceRow || !deviceOptions) return; + const groupRecipes = groups.get(selectedGroupId) || []; + const runtime = runtimeFor(recipe); + const siblings = recipesForRuntime(runtime.id, groupRecipes) + .slice() + .sort((left, right) => (left.hardware?.gpu_count || 0) - (right.hardware?.gpu_count || 0)); + const show = siblings.length > 1; + deviceRow.hidden = !show; + if (deviceCaption) { + deviceCaption.textContent = runtime.id === "spark" ? "1 Spark or a QSFP pair" : "GPU count for this runtime"; + } + if (!show) { + deviceOptions.replaceChildren(); + return; + } + const renderedIds = [...deviceOptions.querySelectorAll("[data-recipe-id]")].map((option) => option.dataset.recipeId); + if (renderedIds.join(",") !== siblings.map((item) => item.id).join(",")) { + deviceOptions.replaceChildren(); + siblings.forEach((candidate) => { + const option = document.createElement("button"); + option.type = "button"; + option.dataset.recipeId = candidate.id; + option.setAttribute("aria-pressed", "false"); + const optionLabel = document.createElement("strong"); + optionLabel.textContent = gpuCountLabel(candidate.hardware, runtime.id); + const optionHint = document.createElement("span"); + optionHint.textContent = runtime.id === "spark" && candidate.hardware?.gpu_count === 2 + ? "Ray sequence parallel over QSFP" + : runtime.id === "spark" + ? "One GB10, local process" + : `${gpuCountLabel(candidate.hardware, runtime.id)} configured`; + option.append(optionLabel, optionHint); + deviceOptions.append(option); + }); + } + deviceOptions.querySelectorAll("button").forEach((option) => { + const selected = option.dataset.recipeId === recipe.id; + option.classList.toggle("cookbook-option--selected", selected); + option.setAttribute("aria-pressed", String(selected)); + }); + }; + let notes = root.querySelector("[data-cookbook-notes]"); if (!notes) { notes = document.createElement("aside"); @@ -408,8 +491,9 @@ let recipe = byId.get(selectedRecipeId); if (groupChanged || groupIdFor(recipe) !== selectedGroupId) { const currentRuntime = runtimeFor(recipe).id; + const currentCount = recipe.hardware?.gpu_count; const groupRecipes = groups.get(selectedGroupId) || []; - recipe = groupRecipes.find((candidate) => runtimeFor(candidate).id === currentRuntime) || groupRecipes[0]; + recipe = pickRecipeForRuntime(currentRuntime, currentCount, groupRecipes) || groupRecipes[0]; selectedRecipeId = recipe.id; } @@ -418,6 +502,7 @@ renderRuntimeOptions(); renderedRuntimeGroup = selectedGroupId; } + renderDeviceOptions(recipe); const runtime = runtimeFor(recipe); const profile = servingPanel && servingProfiles[recipe.id]; const useServer = Boolean(profile && usagePreference === "server"); @@ -437,7 +522,7 @@ ? "The playground and API clients share one server process. Both workflows can run on your own machine." : servingLoadFailed ? "Server examples could not be loaded. Open the H3 server guide below, or use Python directly." - : "This recipe uses Python directly. For the playground and API clients, choose FastH3 Preview with CUDA or MLX."; + : "This recipe uses Python directly. For the playground and API clients, choose FastH3 Preview with CUDA, MLX, or one Spark."; servingPanel.hidden = !useServer; commandBlock.hidden = useServer; root.querySelector("[data-cookbook-python-note]").hidden = useServer; @@ -445,11 +530,17 @@ if (useServer) { const isMLX = profile.runtime === "mlx"; + const isSpark = runtime.id === "spark"; servingPanel.querySelector("[data-cookbook-server-lifetime]").textContent = isMLX ? "Start once, then change prompts in the playground or your app. MLX reuses its pipeline and prompt cache, but loads and releases model components between phases to limit unified-memory use. It does not keep all weights resident." - : "Start once, then change prompts in the playground or your app. CUDA requests reuse the loaded model. The Python SDK can also reuse a generator within one process."; + : isSpark + ? "Start once, then change prompts in the playground or your app. On a DGX Spark, lazy module load still reloads Qwen3-VL and the DiT between phases of each request, so later prompts are not a free hot cache." + : "Start once, then change prompts in the playground or your app. CUDA requests reuse the loaded model. The Python SDK can also reuse a generator within one process."; servingPanel.querySelector("[data-cookbook-install-guide]").href = isMLX - ? "../../getting_started/installation/mps/#run-fasth3-preview" : "../../getting_started/installation/gpu/"; + ? "../../getting_started/installation/mps/#run-fasth3-preview" + : isSpark + ? "../../getting_started/installation/spark/" + : "../../getting_started/installation/gpu/"; servingPanel.querySelector("[data-cookbook-prepare]").hidden = !profile.prepare; servingPanel.querySelector("[data-cookbook-server-prepare]").textContent = profile.prepare; servingPanel.querySelector("[data-cookbook-server-install]").textContent = profile.install; @@ -477,18 +568,13 @@ option.setAttribute("aria-pressed", String(selected)); }); hardwareOptions.querySelectorAll("button").forEach((option) => { - const selected = option.dataset.recipeId === recipe.id; + const selected = option.dataset.runtimeId === runtime.id; option.classList.toggle("cookbook-option--selected", selected); option.setAttribute("aria-pressed", String(selected)); - const candidate = byId.get(option.dataset.recipeId); - const candidateProfile = servingProfiles[candidate.id]; - const candidateRuntime = runtimeFor(candidateProfile && usagePreference === "server" - ? { ...candidate, hardware: candidateProfile.hardware } : candidate); - option.querySelector("span").textContent = candidateRuntime.hint; }); description.textContent = useServer - ? `FastH3 Preview generates video with audio. This server profile uses the checked-in ${profile.runtime.toUpperCase()} configuration.` + ? `FastH3 Preview generates video with audio. This server profile uses the checked-in ${runtime.label} configuration.` : recipe.summary; label.textContent = useServer ? `${recipe.group_label || recipe.label} · Server` : recipe.label; model.textContent = recipe.model; @@ -514,7 +600,9 @@ const limitations = [...(useServer ? [ profile.runtime === "mlx" ? "This MLX server config has no recorded hardware run. Measurements from the Python recipe are not server memory requirements. Only text-to-video/audio is wired; reference inputs and fast modes are not exposed here." - : "This server config has no recorded serving benchmark. Compilation is disabled, unlike the measured Python performance profile.", + : runtime.id === "spark" + ? "This Spark server config has no recorded serving benchmark. Lazy module load reloads Qwen3-VL and the DiT between phases of each request. Compilation of the DiT is disabled." + : "This server config has no recorded serving benchmark. Compilation is disabled, unlike the measured Python performance profile.", `${profile.sampling.width} × ${profile.sampling.height} · ${profile.sampling.num_frames} frames · ${profile.sampling.fps} fps. The server supplies these defaults; the client sends the model and prompt.`, "Generation is serialized. Job metadata is held in memory and is lost when the server restarts.", ] : recipe.limitations || []), ...knobCaveats]; @@ -535,7 +623,12 @@ const nextQuery = new URLSearchParams(window.location.search); nextQuery.set("recipe", recipe.id); nextQuery.set("runtime", runtime.id); - nextQuery.delete("gpus"); + const deviceSiblings = recipesForRuntime(runtime.id, groups.get(selectedGroupId) || []); + if (deviceSiblings.length > 1 && recipe.hardware?.gpu_count != null) { + nextQuery.set("gpus", String(recipe.hardware.gpu_count)); + } else { + nextQuery.delete("gpus"); + } knobDefs.forEach((knob, key) => { if (knobs.some((activeKnob) => activeKnob.key === key)) nextQuery.set(key, String(knobValues[key])); else nextQuery.delete(key); @@ -559,6 +652,17 @@ render({ groupChanged: true, historyMode: "push" }); }); hardwareOptions.addEventListener("click", (event) => { + const option = event.target.closest("button[data-runtime-id]"); + if (!option) return; + const groupRecipes = groups.get(selectedGroupId) || []; + const currentCount = byId.get(selectedRecipeId)?.hardware?.gpu_count; + const nextRecipe = pickRecipeForRuntime(option.dataset.runtimeId, currentCount, groupRecipes); + if (!nextRecipe) return; + selectedRecipeId = nextRecipe.id; + selectedGroupId = groupIdFor(nextRecipe); + render({ historyMode: "push" }); + }); + deviceOptions?.addEventListener("click", (event) => { const option = event.target.closest("button[data-recipe-id]"); if (!option) return; selectedRecipeId = option.dataset.recipeId; diff --git a/docs/assets/custom.css b/docs/assets/custom.css index 66dd7cbbf..03a6eee7a 100644 --- a/docs/assets/custom.css +++ b/docs/assets/custom.css @@ -564,7 +564,7 @@ img { } .cookbook-option-grid--hardware { - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(6.4rem, 1fr)); } .cookbook-option-grid button { @@ -1251,7 +1251,7 @@ img { } .cookbook-option-grid--hardware { - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(6.4rem, 1fr)); } .cookbook-option-grid button { diff --git a/docs/cookbook/cosmos.md b/docs/cookbook/cosmos.md index cb00bd510..603613676 100644 --- a/docs/cookbook/cosmos.md +++ b/docs/cookbook/cosmos.md @@ -5,7 +5,7 @@ hide: # Cosmos recipes -
Primary focus · Inference
Generate video and audio with H3. Run a server on CUDA or Apple Silicon MLX to iterate on prompts, or call the pipeline directly from Python.
- 7 maintained recipes +Generate video and audio with H3. Run a server on CUDA, one DGX Spark, or Apple Silicon MLX to iterate on prompts, or call the pipeline directly from Python.
+ 8 maintained recipes
CUDA covers T2VA, FL2VA, and Ref2VA on the full checkpoint, plus FastH3
- Preview, FastH3 LoRA, and a two-node FastH3 Preview recipe over Ray
- sequence parallel. MLX is T2VA only. Temporal --fast,
+ Preview and FastH3 LoRA. FastH3 Preview also has a DGX Spark runtime with
+ a 1-Spark or 2-Spark device row. MLX is T2VA only. Temporal --fast,
spatial --fast-spatial, and opt-in VSA are flags on the same
MLX script, not extra recipes.
sp_size=2) over QSFP RoCEsp_size=2) over QSFP RoCE. Select NVIDIA DGX Spark, then 1 Spark or 2 Sparks.Choose the result you want, then use a maintained CUDA or MLX path. +
Choose the result you want, then use a maintained CUDA, DGX Spark, or MLX path. Device claims stay tied to checked-in sources and recorded runs.
Loading recipe details...
@@ -243,9 +252,11 @@ cd FastVideoApple Silicon
uv pip install -e ".[mlx]"
Follow the Apple Silicon guide for the download, conversion, and storage requirements.
-Two DGX Sparks
+NVIDIA DGX Spark
+UV_TORCH_BACKEND=cu130 uv pip install -e .
+ Follow the DGX Spark install guide for ARM64 CUDA 13. One Spark is a local process. Two Sparks need Ray on the QSFP link:
uv pip install ray
- The 2-Spark recipe needs two DGX Sparks on an active QSFP link, the same FastH3 snapshot on both NVMes, and a Ray cluster on top. Follow pairing two Sparks before running it.
+Bring up the cluster from pairing two Sparks before selecting 2 Sparks in the builder.
--fast, optional spatial --fast-spatial, and opt-in VSA on --include-vsa checkpoints. FL2VA, Ref2VA, and two-pass refinement are not wired.FASTVIDEO_FA4=0 and FASTVIDEO_VSA_SM100A=0. Legal num_frames values are 17n+5, capped at 345 (15 s). A 345-frame request on one Spark can OOM.huggingface-cli login and confirm you accepted the model's license on Hugging Face.