From 089a5a55a000db41b53fb64bd5f65206de727aba Mon Sep 17 00:00:00 2001 From: Aryan Kumar <156166681+aryan5v@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:10:29 -0700 Subject: [PATCH] [feat] Cookbook serving, hardware catalog, and landing page (#1999) Co-authored-by: Aryan Kumar --- .../components/cookbook/HardwarePicker.tsx | 445 +++++-- .../components/ui/card-curtain-reveal.tsx | 163 +++ .../cookbook/minimax-h3/fasth3-4step.mdx | 7 + .../cookbook/minimax-h3/fasth3-omniref.mdx | 7 + .../cookbook/minimax-h3/fasth3-preview.mdx | 7 + docs-site/islands/Cookbook.tsx | 526 ++++++-- docs-site/islands/SourceRecipe.tsx | 56 +- docs-site/lib/cookbook-commands.ts | 15 +- docs-site/lib/cookbook-pages.ts | 59 +- docs-site/lib/fast-models.ts | 16 +- docs-site/lib/hardware.ts | 352 +++++ docs-site/navigation.json | 18 +- docs-site/package-lock.json | 77 ++ docs-site/package.json | 2 + docs-site/playwright.config.ts | 17 +- docs-site/scripts/transfer-docs.mjs | 5 +- docs-site/tests/commands.test.mjs | 12 + docs-site/tests/cookbook.spec.ts | 229 +++- docs-site/tests/documentation.spec.ts | 13 +- .../cookbook-hardware-catalog/README.md | 17 + .../after/directory-m4-max.png | Bin 0 -> 152097 bytes .../after/directory-task.png | Bin 0 -> 113769 bytes .../after/model-unified.png | Bin 0 -> 139142 bytes .../after/narrow-lookup.png | Bin 0 -> 111582 bytes .../after/no-results.png | Bin 0 -> 138846 bytes docs-site/tests/fixtures/hardware.json | 214 +++ docs-site/theme.css | 472 +++++-- docs/assets/cookbook-recipes.json | 61 + docs/contributing/capacity_estimation.md | 122 +- docs/contributing/cookbook_configuration.md | 79 +- .../serving_documentation_contract.md | 123 ++ docs/cookbook/capacity-api-contract.review.md | 261 ---- docs/cookbook/capacity/hardware-coverage.md | 88 ++ docs/cookbook/capacity/hardware.yaml | 998 +++++++++++++- docs/cookbook/design.md | 28 +- docs/cookbook/options.yaml | 30 + docs/cookbook/recipes/fasth3-4step.yaml | 47 + docs/cookbook/recipes/fasth3-8step.yaml | 5 + docs/cookbook/recipes/fasth3-omniref.yaml | 141 ++ docs/cookbook/recipes/fasth3-preview.yaml | 64 + docs/cookbook/recipes/fasth3-trim.yaml | 8 + docs/cookbook/recipes/hunyuan15-i2v-480p.yaml | 30 + docs/cookbook/recipes/hunyuan15-t2v-480p.yaml | 26 + docs/cookbook/recipes/kandinsky5-t2v.yaml | 26 + .../recipes/kandinsky6-ti2va-distilled.yaml | 26 + docs/cookbook/recipes/kandinsky6-ti2va.yaml | 26 + docs/cookbook/recipes/ltx2-distilled.yaml | 27 +- docs/cookbook/recipes/wan21-t2v-14b.yaml | 26 + docs/cookbook/recipes/wan22-i2v-a14b.yaml | 30 + docs/cookbook/serving-coverage.md | 134 ++ .../inference_schema_parity_inventory.yaml | 3 + docs/inference/fasth3_omniref.md | 34 + docs/js/build-cookbook-capacity.mjs | 163 ++- docs/js/build-cookbook-config.mjs | 6 +- docs/js/cookbook-capacity-schema.mjs | 5 +- docs/js/cookbook-capacity.mjs | 72 +- docs/js/cookbook-config.mjs | 23 +- docs/js/cookbook-memory.mjs | 1162 +++++++++++++++-- docs/js/cookbook-workloads.mjs | 45 + docs/node_modules | 1 + docs/tests/cookbook-capacity.test.mjs | 114 +- docs/tests/cookbook-config.test.mjs | 29 + docs/tests/cookbook-memory.test.mjs | 929 ++++++++++--- examples/serving/README.md | 84 +- examples/serving/clients/audio.mjs | 48 + examples/serving/clients/audio.py | 66 + examples/serving/clients/audio.sh | 54 + .../openai_fasth3_omniref_rtx5090.yaml | 78 ++ .../openai_fasth3_omniref_rtx_pro6000.yaml | 74 ++ .../serving/openai_fasth3_omniref_spark.yaml | 79 ++ examples/serving/openai_flux1_dev.yaml | 26 + examples/serving/openai_flux2_dev.yaml | 26 + examples/serving/openai_flux2_klein.yaml | 24 + .../serving/openai_hunyuan15_i2v_480p.yaml | 36 + .../serving/openai_hunyuan15_t2v_480p.yaml | 35 + examples/serving/openai_kandinsky5_t2v.yaml | 39 + examples/serving/openai_kandinsky6_ti2va.yaml | 35 + .../openai_kandinsky6_ti2va_distilled.yaml | 35 + examples/serving/openai_ltx2_distilled.yaml | 36 + examples/serving/openai_mmaudio.yaml | 37 + examples/serving/openai_sd35.yaml | 24 + examples/serving/openai_stable_audio.yaml | 38 + examples/serving/openai_wan21_t2v_14b.yaml | 43 + examples/serving/openai_wan22_i2v_a14b.yaml | 33 + fastvideo/api/compat.py | 9 +- fastvideo/api/schema.py | 15 + fastvideo/entrypoints/cli/serve.py | 6 + fastvideo/entrypoints/openai/api_server.py | 15 +- fastvideo/entrypoints/openai/media_policy.py | 113 ++ fastvideo/entrypoints/openai/protocol.py | 4 +- .../entrypoints/openai/request_adapter.py | 193 ++- fastvideo/entrypoints/openai/state.py | 23 + fastvideo/entrypoints/openai/utils.py | 47 +- fastvideo/entrypoints/openai/video_api.py | 145 +- fastvideo/fastvideo_args.py | 3 + fastvideo/models/loader/component_loader.py | 26 +- fastvideo/registry.py | 9 +- fastvideo/tests/api/test_cli_translation.py | 23 +- fastvideo/tests/api/test_configs.py | 3 + fastvideo/tests/api/test_parser.py | 2 + .../tests/entrypoints/test_media_policy.py | 151 +++ .../entrypoints/test_openai_serving_engine.py | 158 ++- .../test_serving_family_baselines.py | 293 +++++ .../vaes/test_minimax_h3_nvfp4_decoder.py | 19 +- .../test_cookbook_native_contract.py | 12 +- 105 files changed, 8787 insertions(+), 1153 deletions(-) create mode 100644 docs-site/components/ui/card-curtain-reveal.tsx create mode 100644 docs-site/content/cookbook/minimax-h3/fasth3-4step.mdx create mode 100644 docs-site/content/cookbook/minimax-h3/fasth3-omniref.mdx create mode 100644 docs-site/content/cookbook/minimax-h3/fasth3-preview.mdx create mode 100644 docs-site/lib/hardware.ts create mode 100644 docs-site/tests/evidence/cookbook-hardware-catalog/README.md create mode 100644 docs-site/tests/evidence/cookbook-hardware-catalog/after/directory-m4-max.png create mode 100644 docs-site/tests/evidence/cookbook-hardware-catalog/after/directory-task.png create mode 100644 docs-site/tests/evidence/cookbook-hardware-catalog/after/model-unified.png create mode 100644 docs-site/tests/evidence/cookbook-hardware-catalog/after/narrow-lookup.png create mode 100644 docs-site/tests/evidence/cookbook-hardware-catalog/after/no-results.png create mode 100644 docs-site/tests/fixtures/hardware.json create mode 100644 docs/contributing/serving_documentation_contract.md delete mode 100644 docs/cookbook/capacity-api-contract.review.md create mode 100644 docs/cookbook/capacity/hardware-coverage.md create mode 100644 docs/cookbook/recipes/fasth3-4step.yaml create mode 100644 docs/cookbook/recipes/fasth3-omniref.yaml create mode 100644 docs/cookbook/recipes/fasth3-preview.yaml create mode 100644 docs/cookbook/recipes/hunyuan15-i2v-480p.yaml create mode 100644 docs/cookbook/recipes/hunyuan15-t2v-480p.yaml create mode 100644 docs/cookbook/recipes/kandinsky5-t2v.yaml create mode 100644 docs/cookbook/recipes/kandinsky6-ti2va-distilled.yaml create mode 100644 docs/cookbook/recipes/kandinsky6-ti2va.yaml create mode 100644 docs/cookbook/recipes/wan21-t2v-14b.yaml create mode 100644 docs/cookbook/recipes/wan22-i2v-a14b.yaml create mode 100644 docs/cookbook/serving-coverage.md create mode 100644 docs/js/cookbook-workloads.mjs create mode 120000 docs/node_modules create mode 100644 examples/serving/clients/audio.mjs create mode 100644 examples/serving/clients/audio.py create mode 100755 examples/serving/clients/audio.sh create mode 100644 examples/serving/openai_fasth3_omniref_rtx5090.yaml create mode 100644 examples/serving/openai_fasth3_omniref_rtx_pro6000.yaml create mode 100644 examples/serving/openai_fasth3_omniref_spark.yaml create mode 100644 examples/serving/openai_flux1_dev.yaml create mode 100644 examples/serving/openai_flux2_dev.yaml create mode 100644 examples/serving/openai_flux2_klein.yaml create mode 100644 examples/serving/openai_hunyuan15_i2v_480p.yaml create mode 100644 examples/serving/openai_hunyuan15_t2v_480p.yaml create mode 100644 examples/serving/openai_kandinsky5_t2v.yaml create mode 100644 examples/serving/openai_kandinsky6_ti2va.yaml create mode 100644 examples/serving/openai_kandinsky6_ti2va_distilled.yaml create mode 100644 examples/serving/openai_ltx2_distilled.yaml create mode 100644 examples/serving/openai_mmaudio.yaml create mode 100644 examples/serving/openai_sd35.yaml create mode 100644 examples/serving/openai_stable_audio.yaml create mode 100644 examples/serving/openai_wan21_t2v_14b.yaml create mode 100644 examples/serving/openai_wan22_i2v_a14b.yaml create mode 100644 fastvideo/entrypoints/openai/media_policy.py create mode 100644 fastvideo/tests/entrypoints/test_media_policy.py create mode 100644 fastvideo/tests/entrypoints/test_serving_family_baselines.py diff --git a/docs-site/components/cookbook/HardwarePicker.tsx b/docs-site/components/cookbook/HardwarePicker.tsx index 6495ab5c6..972c2e32b 100644 --- a/docs-site/components/cookbook/HardwarePicker.tsx +++ b/docs-site/components/cookbook/HardwarePicker.tsx @@ -1,121 +1,378 @@ -import { useEffect, useId, useState } from "react"; +import { useEffect, useId, useRef, useState } from "react"; import { Input } from "@/components/ui/input"; +import { + backendLabel, + gpuScopeNote, + memoryText, + resolveHardware, + searchHardware, + type CustomHardware, + type HardwareCatalogState, + type HardwareRecord, + type HardwareSelection, + type MemoryType, + type MemoryUnit, +} from "@/lib/hardware"; -type Purpose = "target" | "filter"; +interface Draft { + name: string; + backend: string; + memory_type: MemoryType; + memory: string; + memory_unit: MemoryUnit; + budget: string; + ram: string; +} + +function draftFrom(custom: CustomHardware | undefined, backend: string): Draft { + return { + name: custom?.name ?? "", + backend: custom?.backend ?? (backend === "mlx" ? "mlx" : "cuda"), + memory_type: custom?.memory_type ?? (backend === "mlx" ? "unified" : "dedicated"), + memory: custom?.memory_gb ? String(custom.memory_gb) : "", + memory_unit: custom?.memory_unit ?? "GB", + budget: custom?.usable_budget_gb ? String(custom.usable_budget_gb) : "", + ram: custom?.host_ram_gb ? String(custom.host_ram_gb) : "", + }; +} + +function amount(value: string): number | undefined { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 && parsed <= 4096 ? parsed : undefined; +} export default function HardwarePicker({ backend, - options, + catalog, + status, + error, value, onChange, + gpuCount = 1, purpose = "target", + reload, }: { backend: string; - options: string[][]; - value: string; - onChange: (value: string) => void; - purpose?: Purpose; + catalog: HardwareRecord[]; + status: HardwareCatalogState["status"]; + error?: string; + value: HardwareSelection; + onChange: (selection: HardwareSelection) => void; + gpuCount?: number; + purpose?: "target" | "filter"; + reload?: () => void; }) { - const [search, setSearch] = useState(""); - const [adding, setAdding] = useState(false); - const [custom, setCustom] = useState(""); - useEffect(() => { - const requested = new URL(window.location.href).searchParams.get("hardware"); - setCustom(requested?.startsWith("custom:") ? requested.slice(7) : ""); - }, []); const id = useId(); - const known = options.filter((option) => option.join(" ").toLowerCase().includes(search.toLowerCase())); - const selected = value.startsWith("custom:") ? value.slice(7) : options.find(([key]) => key === value)?.[1]; - const savedUnknown = !!value && !selected; - function add() { - const name = search.trim(); - if (!name) return; - setCustom(name); - onChange(`custom:${name}`); - setSearch(""); + const panelId = `${id}-lookup`; + const triggerRef = useRef(null); + const searchRef = useRef(null); + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const [adding, setAdding] = useState(false); + const [draft, setDraft] = useState(() => draftFrom(value.custom, backend)); + const record = resolveHardware(value, catalog); + const unknown = !!value.id && value.id !== "custom" && !record; + const candidates = + purpose === "filter" ? catalog : catalog.filter((item) => item.backend === (backend === "mlx" ? "mlx" : "cuda")); + const matches = searchHardware(candidates, query).slice(0, 20); + const draftMemory = amount(draft.memory); + const draftBudget = amount(draft.budget); + const budgetTooLarge = draftMemory !== undefined && draftBudget !== undefined && draftBudget > draftMemory; + const validDraft = !!draft.name.trim() && !!draftMemory && !budgetTooLarge; + const scope = gpuScopeNote(record, gpuCount); + + useEffect(() => { + if (value.custom) setDraft(draftFrom(value.custom, backend)); + }, [backend, value.custom]); + + useEffect(() => { + if (status === "unavailable") setAdding(true); + }, [status]); + + useEffect(() => { + if (open && status === "ready" && !adding) searchRef.current?.focus(); + }, [adding, open, status]); + + function close() { + setOpen(false); + triggerRef.current?.focus(); + } + + function choose(recordId: string) { + onChange({ id: recordId }); + setOpen(false); + setQuery(""); + } + + function save(event: React.FormEvent) { + event.preventDefault(); + const memory = amount(draft.memory); + if (!validDraft || !memory) return; + onChange({ + id: "custom", + custom: { + name: draft.name.trim(), + backend: draft.backend, + memory_type: draft.memory_type, + memory_gb: memory, + memory_unit: draft.memory_unit, + usable_budget_gb: amount(draft.budget), + host_ram_gb: draft.memory_type === "unified" ? undefined : amount(draft.ram), + }, + }); + setOpen(false); setAdding(false); } - function clear() { - setCustom(""); - setSearch(""); - onChange(""); - } + return ( -
-
- Hardware {purpose} - {known.map(([key, title, detail]) => ( -
-
- - {(selected || savedUnknown) && ( -
+ - )} -
- {!selected && ( + + ) : value.id === "custom" ? (

- {savedUnknown ? "Saved hardware target is not in this list." : `No hardware ${purpose} selected.`} + Add this device's memory to finish selecting it. +

+ ) : unknown ? ( +

+ Saved hardware target ({value.id}) is not in this catalog. +

+ ) : ( +

+ No hardware {purpose} selected.

)} - {adding && ( -
{ - event.preventDefault(); - add(); + {record ? ( +

+ {record.memory_type === "unified" + ? "Shared with the CPU; not dedicated VRAM." + : record.memory_type === "dedicated" + ? "Per device, not a combined pool." + : "Host memory, not GPU VRAM."} +

+ ) : null} + {scope ?

{scope}

: null} +
+ {!record && value.id ? ( + + ) : null} + +
+ {record || open ? ( +

+ {purpose === "filter" + ? "Filters by runtime. Memory fit is not estimated." + : "Single-GPU devices set GPU count to 1. Nothing else in the configuration changes."} +

+ ) : null} + {open ? ( +
event.key === "Escape" && close()}> + {status === "loading" ?

Loading hardware catalog…

: null} + {status === "unavailable" ? ( +
+

Hardware catalog unavailable. Add your device manually.

+ {error ? {error} : null} + {reload ? ( + + ) : null} +
+ ) : null} + {status === "ready" ? ( + <> + setQuery(event.target.value)} + /> +
+ {matches.map((item) => ( + + ))} +
+ {!matches.length ?

No matching devices.

: null} + + ) : null} + - - )} + {adding ? ( +
+

Add a device

+
+ + + + + + + {draft.memory_type !== "unified" ? ( + + ) : null} +
+ {budgetTooLarge ? ( +

The usable budget cannot exceed the nominal memory.

+ ) : null} +

+ Nominal memory is device capacity, not what FastVideo can use. A budget is your own limit and is not + verified. Unified memory is shared with the CPU; it is not dedicated VRAM. +

+
+ + +
+
+ ) : null} +
+ ) : null} ); } diff --git a/docs-site/components/ui/card-curtain-reveal.tsx b/docs-site/components/ui/card-curtain-reveal.tsx new file mode 100644 index 000000000..4303f3ad5 --- /dev/null +++ b/docs-site/components/ui/card-curtain-reveal.tsx @@ -0,0 +1,163 @@ +"use client"; + +import * as React from "react"; +import { type HTMLMotionProps, type Variants, motion } from "motion/react"; +import { cn } from "cn"; + +const curtainVariants: Variants = { + visible: { + clipPath: "polygon(0 0,100% 0,100% 100%,0 100%)", + transition: { duration: 0.4, ease: [0.25, 1.5, 0.5, 1] }, + }, + hidden: { + clipPath: "polygon(50% 0,50% 0,50% 100%,50% 100%)", + transition: { duration: 0.3, ease: [0.25, 1.5, 0.5, 1] }, + }, +}; + +interface CardCurtainRevealContextValue { + revealed: boolean; + /** False on touch-only devices: details stay visible and the curtain never draws. */ + canHover: boolean; +} +const CardCurtainRevealContext = React.createContext(undefined); +function useCardCurtainRevealContext() { + const context = React.useContext(CardCurtainRevealContext); + if (!context) throw new Error("useCardCurtainRevealContext must be used within a CardCurtainReveal component"); + return context; +} + +function useCanHover() { + const [canHover, setCanHover] = React.useState(true); + React.useEffect(() => { + const query = window.matchMedia("(hover: hover) and (pointer: fine)"); + const sync = () => setCanHover(query.matches); + sync(); + query.addEventListener("change", sync); + return () => query.removeEventListener("change", sync); + }, []); + return canHover; +} + +interface CardCurtainRevealProps extends React.HTMLAttributes { + /** Reveals the card without a pointer, for example while its link has keyboard focus. */ + active?: boolean; +} + +const CardCurtainReveal = React.forwardRef( + ({ children, className, active = false, ...props }, ref) => { + const [isMouseIn, setIsMouseIn] = React.useState(false); + const canHover = useCanHover(); + const handleMouseEnter = React.useCallback(() => setIsMouseIn(true), []); + const handleMouseLeave = React.useCallback(() => setIsMouseIn(false), []); + const value = React.useMemo( + () => ({ revealed: !canHover || isMouseIn || active, canHover }), + [active, canHover, isMouseIn], + ); + return ( + +
+ {children} +
+
+ ); + }, +); +CardCurtainReveal.displayName = "CardCurtainReveal"; + +const CardCurtainRevealFooter = React.forwardRef>( + ({ className, ...props }, ref) => { + const { revealed } = useCardCurtainRevealContext(); + return ( + + ); + }, +); +CardCurtainRevealFooter.displayName = "CardCurtainRevealFooter"; + +const CardCurtainRevealBody = React.forwardRef>( + ({ className, ...props }, ref) =>
, +); +CardCurtainRevealBody.displayName = "CardCurtainRevealBody"; + +interface CardCurtainRevealTitleProps extends HTMLMotionProps<"h2"> { + /** Distance in pixels the title rests below its revealed position. */ + hiddenOffset?: number; + as?: "h2" | "h3" | "h4"; +} + +const CardCurtainRevealTitle = React.forwardRef( + ({ className, hiddenOffset = 170, as = "h2", ...props }, ref) => { + const { revealed } = useCardCurtainRevealContext(); + const Heading = motion[as]; + return ( + + ); + }, +); +CardCurtainRevealTitle.displayName = "CardCurtainRevealTitle"; + +const CardCurtain = React.forwardRef>(({ className, ...props }, ref) => { + const { revealed, canHover } = useCardCurtainRevealContext(); + if (!canHover) return null; + return ( +
Resources @@ -501,7 +551,7 @@ function Editor({ href={sitePath( mlx ? "/getting_started/installation/mlx" - : hardware === "spark" + : hardwareRecord?.name.toLowerCase().includes("dgx spark") ? "/getting_started/installation/spark" : "/getting_started/installation/gpu", )} @@ -510,13 +560,6 @@ function Editor({
- {hardwareLabel ? ( -

- {mlx - ? `${hardwareLabel} is saved as your target. Apple Silicon uses unified memory; dedicated GPU memory does not apply.` - : `${hardwareLabel} is saved as your target. It does not change the generated YAML; use GPU count and parallelism for the run.`} -

- ) : null} {catalog.workload === "i2v" && runMode !== "serving" && ( @@ -543,7 +586,7 @@ function Editor({ )} - {model.family === "FastH3" && ( + {model.family === "MiniMax H3" && ( {model.id === "fasth3-trim" && (
@@ -557,7 +600,7 @@ function Editor({
)}

- Components come from the maintained recipe. They are not individually selectable in the cookbook yet. + Transformer weights come from the maintained recipe. Swap the text encoder and video decoder below.

)} @@ -692,8 +735,20 @@ export default function Cookbook({ modelId }: { modelId?: string }) { setError(""); const url = new URL(window.location.href); url.searchParams.set("deployment", value); - window.history.replaceState(null, "", url); + window.history.pushState(null, "", url); } + useEffect(() => { + const restore = () => { + const requested = new URL(window.location.href).searchParams.get("deployment"); + const available = index?.models.find((item) => item.id === modelId)?.deployments ?? []; + const next = available.find((deployment) => deployment.id === requested)?.id ?? available[0]?.id; + if (!next) return; + // The deployment loader clears and reloads the catalog only when the deployment changes. + setSelected(next); + }; + window.addEventListener("popstate", restore); + return () => window.removeEventListener("popstate", restore); + }, [index, modelId]); const model = index?.models.find((model) => model.id === modelId); if (!modelId) return setRetry(retry + 1)} />; const directoryModel = modelDirectory.find((model) => model.id === modelId); @@ -739,7 +794,7 @@ export default function Cookbook({ modelId }: { modelId?: string }) { aria-pressed={selected === deployment.id} onClick={() => select(deployment.id)} > - {deployment.runtime.includes("mlx") ? "MLX" : "CUDA"} + {deploymentLabel(deployment, model?.deployments ?? [])} ))} @@ -750,104 +805,307 @@ export default function Cookbook({ modelId }: { modelId?: string }) { ); } -function RecipeDirectory({ index, error, retry }: { index?: Index; error: string; retry: () => void }) { - const [query, setQuery] = useState(""); - const [category, setCategory] = useState("All models"); - const [family, setFamily] = useState("All families"); - const visible = modelDirectory.filter( - (model) => - (category === "All models" || model.category === category) && - (family === "All families" || model.family === family) && - `${model.title} ${model.family} ${model.tags.join(" ")}`.toLowerCase().includes(query.trim().toLowerCase()), - ); +const GPU_COUNT_PATH = "generator.engine.num_gpus"; +const SINGLE_GPU_FORM_FACTOR = /desktop|laptop|workstation|system|soc/; + +/** Desktop and workstation cards, DGX Spark, and Macs are one-GPU machines; datacenter parts keep the recipe default. */ +function singleGpuDevice(record: { form_factor?: string }) { + return SINGLE_GPU_FORM_FACTOR.test(record.form_factor ?? ""); +} + +/** Returns new edits where every fixed parallel size that no longer divides `gpus` becomes automatic. */ +function fitParallelism(fields: Field[], edits: Record, gpus: number): Record { + const stale = fields.filter((field) => { + if (!/\.parallelism\.[a-z_]+_size$/.test(field.path)) return false; + const size = edits[field.path] ?? field.value; + return typeof size === "number" && size > 0 && (size > gpus || gpus % size !== 0); + }); + return stale.length ? { ...edits, ...Object.fromEntries(stale.map((field) => [field.path, -1])) } : edits; +} + +type DeploymentChoice = { runtime: string; label: string }; + +/** Backend name when it is unique; the deployment's hardware label when several share a backend. */ +function deploymentLabel(deployment: DeploymentChoice, deployments: DeploymentChoice[]) { + const backend = (item: DeploymentChoice) => (item.runtime.includes("mlx") ? "MLX" : "CUDA"); + if (deployments.filter((item) => backend(item) === backend(deployment)).length < 2) return backend(deployment); + return deployment.label.replace(/\s*REST$/i, ""); +} + +/** Mirrors the editor: MLX and WebSocket deployments offer serving only. */ +function configurableModes(index: Index | undefined, modelId: string): string[] | undefined { + const deployments = index?.models.find((model) => model.id === modelId)?.deployments; + if (!deployments?.length) return undefined; + const servingOnly = (runtime: string) => runtime.includes("mlx") || runtime.includes("streaming"); + return deployments.some((deployment) => !servingOnly(deployment.runtime)) + ? ["Python SDK", "CLI", "Serving"] + : ["Serving"]; +} + +/** Mirrors the source recipe page: Python SDK and CLI always, serving when a recipe declares it. */ +function exampleModes(model: DirectoryModel): string[] { + return ["Python SDK", "CLI", ...(model.recipes.some((recipe) => recipe.serving) ? ["Serving"] : [])]; +} + +/** Rest state shows identity only; hover or keyboard focus opens a curtain over the details. */ +function RecipeCard({ model, href, modes }: { model: DirectoryModel; href: string; modes?: string[] }) { + const [focused, setFocused] = useState(false); + const details = useRef(null); + const [restOffset, setRestOffset] = useState(72); + useEffect(() => { + const element = details.current; + if (!element) return; + const measure = () => setRestOffset(element.offsetHeight + 8); + measure(); + const observer = new ResizeObserver(measure); + observer.observe(element); + return () => observer.disconnect(); + }, []); return ( -
-
-
- {["All models", "Video", "Image", "Audio", "World"].map((value) => ( - - ))} -
- setQuery(event.target.value)} - /> -
-
-

{visible.length} models

- -
- {error && ( -
-

{error}

- -
- )} -
- {visible.map((model) => ( - -
- - {model.provider} - {model.featured && Featured} -
-

{model.title}

+
setFocused(true)} onBlur={() => setFocused(false)}> + + +
+ + {model.provider} + {model.featured ? Featured : null} +
+ + {model.title} + + +

{model.description}

{model.category} {model.tags.slice(0, 2).map((tag) => ( {tag} ))}
-
- - {model.platforms.map((platform) => (platform === "Apple Silicon" ? "MLX" : platform)).join(" · ")} - - {model.configurable ? "Configure →" : "View recipe →"} -
-
- ))} + {modes ?

{modes.join(" · ")}

: null} + + + + + + {model.platforms.map((platform) => (platform === "Apple Silicon" ? "MLX" : platform)).join(" · ")} + + + {model.configurable ? "Configure" : "View recipe"} + + + + ); +} + +function RecipeDirectory({ index, error, retry }: { index?: Index; error: string; retry: () => void }) { + const [initial] = useState(() => + typeof window === "undefined" ? new URLSearchParams() : new URL(window.location.href).searchParams, + ); + const [query, setQuery] = useState(initial.get("q") ?? ""); + const [category, setCategory] = useState(initial.get("category") ?? "All models"); + const [family, setFamily] = useState(initial.get("family") ?? "All families"); + const [runtime, setRuntime] = useState(initial.get("runtime") ?? "all"); + const [task, setTask] = useState(initial.get("task") ?? "All tasks"); + useEffect(() => { + const url = new URL(window.location.href); + const search = url.searchParams; + const set = (key: string, value: string | undefined) => { + if (value) search.set(key, value); + else search.delete(key); + }; + set("q", query.trim() || undefined); + set("category", category === "All models" ? undefined : category); + set("family", family === "All families" ? undefined : family); + set("runtime", runtime === "all" ? undefined : runtime); + set("task", task === "All tasks" ? undefined : task); + window.history.replaceState(null, "", url); + }, [category, family, query, runtime, task]); + const runtimeMatches = (platforms: string[]) => { + if (runtime === "cuda") return platforms.includes("NVIDIA"); + if (runtime === "mlx") return platforms.includes("Apple Silicon"); + return true; + }; + const visible = modelDirectory.filter( + (model) => + (category === "All models" || model.category === category) && + (family === "All families" || model.family === family) && + (task === "All tasks" || model.task === task) && + runtimeMatches(model.platforms) && + `${model.title} ${model.family} ${model.tags.join(" ")} ${model.description}` + .toLowerCase() + .includes(query.trim().toLowerCase()), + ); + function clear() { + setQuery(""); + setCategory("All models"); + setFamily("All families"); + setRuntime("all"); + setTask("All tasks"); + } + const configurable = visible.filter((model) => model.configurable); + const examples = visible.filter((model) => !model.configurable); + const exampleFamilies = [...new Set(examples.map((model) => model.family))]; + const filtered = + !!query.trim() || + category !== "All models" || + family !== "All families" || + runtime !== "all" || + task !== "All tasks"; + const categoryCount = (value: string) => + modelDirectory.filter((model) => value === "All models" || model.category === value).length; + return ( + +
+
+
+ {["All models", "Video", "Image", "Audio", "World"].map((value) => ( + + ))} +
+ setQuery(event.target.value)} + /> +
+
+

+ {visible.length} {visible.length === 1 ? "model" : "models"} + {configurable.length ? ` · ${configurable.length} configurable` : ""} + {runtime !== "all" ? ` · ${runtime === "mlx" ? "MLX" : "CUDA"} only` : ""} + {task !== "All tasks" ? ` · ${task}` : ""} + {filtered && visible.length ? ( + + ) : null} +

+
+ + + +
+
+ {error ? ( +
+

{error}

+ +
+ ) : null} + {configurable.length ? ( +
+
+

Configurable recipes

+

Edit settings, then copy one config for the Python SDK, the CLI, or an OpenAI-compatible server.

+
+
+ {configurable.map((model) => ( + + ))} +
+
+ ) : null} + {examples.length ? ( +
+
+

Example recipes

+

+ Maintained example scripts, grouped by model family. Adjust their documented settings and run them as + written. +

+
+ {exampleFamilies.map((name) => { + const members = examples.filter((model) => model.family === name); + return ( +
+
+

{name}

+ + {members.length} {members.length === 1 ? "recipe" : "recipes"} + + Family guide → +
+
+ {members.map((model) => ( + + ))} +
+
+ ); + })} +
+ ) : null} + {!visible.length ? ( +
+

No matching recipes

+

Clear a filter or choose a different runtime.

+ +
+ ) : null}
- {!visible.length && ( -
-

No matching recipes

- -
- )} -
+ ); } diff --git a/docs-site/islands/SourceRecipe.tsx b/docs-site/islands/SourceRecipe.tsx index f100ed74c..054360cc4 100644 --- a/docs-site/islands/SourceRecipe.tsx +++ b/docs-site/islands/SourceRecipe.tsx @@ -9,6 +9,14 @@ import RecipeFacts from "@/components/cookbook/RecipeFacts"; import { configAndLaunch, fileAndLaunch, sdkCommand, pythonSource } from "@/lib/cookbook-commands"; import { sitePath, type DirectoryModel } from "@/lib/cookbook-pages"; import { api } from "@/lib/cookbook"; +import { + gpuCountFrom, + readHardwareSelection, + resolveHardware, + useHardwareCatalog, + writeHardwareSelection, + type HardwareSelection, +} from "@/lib/hardware"; interface Control { name: string; @@ -51,14 +59,10 @@ export default function SourceRecipe({ model }: { model: DirectoryModel }) { const [outputTab, setOutputTab] = useState("commands"); const [error, setError] = useState(""); const [edits, setEdits] = useState>({}); - const [hardware, setHardware] = useState(() => { - const requested = new URL(window.location.href).searchParams.get("hardware"); - return requested && requested !== "unspecified" ? requested : ""; - }); - useEffect(() => { - const requested = new URL(window.location.href).searchParams.get("hardware"); - setHardware(requested && requested !== "unspecified" ? requested : ""); - }, []); + const [hardware, setHardware] = useState(() => + readHardwareSelection(new URL(window.location.href).searchParams), + ); + const hardwareCatalog = useHardwareCatalog(); const recipe = model.recipes.find((recipe) => recipe.id === selected)!; const yamlSource = mode === "serving" ? recipe.serving?.source : /\.ya?ml$/.test(recipe.source) ? recipe.source : undefined; @@ -212,23 +216,12 @@ export default function SourceRecipe({ model }: { model: DirectoryModel }) { .map((match) => match[1]) .join("\n"); const mlx = recipe.gpu_types.includes("Apple Silicon"); - const devices = mlx - ? [["apple", "Apple Silicon", "Unified memory"]] - : [ - ["b200", "B200", "Data center"], - ["h100", "H100", "Data center"], - ["rtx5090", "RTX 5090", "GeForce"], - ["rtx", "RTX PRO 6000", "Workstation"], - ["spark", "DGX Spark", "GB10"], - ]; - const hardwareLabel = hardware.startsWith("custom:") - ? hardware.slice(7) - : devices.find(([id]) => id === hardware)?.[1]; - const chooseHardware = (value: string) => { - setHardware(value); + const hardwareRecord = resolveHardware(hardware, hardwareCatalog.records); + const gpuCount = config ? gpuCountFrom(config) : (recipe.hardware?.gpu_count ?? 1); + const chooseHardware = (selection: HardwareSelection) => { + setHardware(selection); const url = new URL(window.location.href); - if (value) url.searchParams.set("hardware", value); - else url.searchParams.delete("hardware"); + writeHardwareSelection(url, selection); window.history.replaceState(null, "", url); }; const control = (field: Control, index: number) => { @@ -468,10 +461,14 @@ export default function SourceRecipe({ model }: { model: DirectoryModel }) {
Resources @@ -484,13 +481,6 @@ export default function SourceRecipe({ model }: { model: DirectoryModel }) {
- {hardwareLabel ? ( -

- {mlx - ? `${hardwareLabel} is saved as your target. Apple Silicon uses unified memory; dedicated GPU memory does not apply.` - : `${hardwareLabel} is saved as your target. It does not change the example; use GPU count in the source settings.`} -

- ) : null} ; const pipeline = generator.pipeline as Record | undefined; const inputs = { ...((defaults.inputs ?? {}) as Record) }; - if (imagePath !== undefined) inputs.image_path = imagePath.trim(); - const images = Array.isArray(inputs.image_path) ? inputs.image_path : [inputs.image_path]; + const components = pipeline?.components as Record | undefined; + // Ref2VA pipelines read ordered references, not a first-frame image. + const ref2va = /ref2va/i.test(String(components?.override_pipeline_cls_name ?? "")); + if (imagePath !== undefined && imagePath.trim()) { + if (ref2va) inputs.references = [{ source: imagePath.trim(), media_type: "image" }]; + else inputs.image_path = imagePath.trim(); + } else if (imagePath !== undefined && !ref2va) inputs.image_path = imagePath.trim(); + const references = Array.isArray(inputs.references) ? (inputs.references as Record[]) : []; + const images = ref2va + ? references.map((reference) => reference.source) + : Array.isArray(inputs.image_path) + ? inputs.image_path + : [inputs.image_path]; if (pipeline?.workload_type === "i2v" && !images.some((image) => typeof image === "string" && image.trim())) { throw new Error("Enter a source image path before copying a CLI or Python SDK example."); } diff --git a/docs-site/lib/cookbook-pages.ts b/docs-site/lib/cookbook-pages.ts index 041ea9761..b44799a35 100644 --- a/docs-site/lib/cookbook-pages.ts +++ b/docs-site/lib/cookbook-pages.ts @@ -4,23 +4,50 @@ import sourceCatalog from "../../docs/assets/cookbook-recipes.json" with { type: export const recipePages = [ { id: "fasth3-8step", - family: "FastH3", + family: "MiniMax H3", title: "FastH3 8-Step V2", mark: "H3", path: "/cookbook/minimax-h3/fasth3-8step", description: "Distilled MiniMax-H3 video generation with synchronized audio.", tags: ["Text to video", "Audio", "8-step"], - featured: true, }, { id: "fasth3-trim", - family: "FastH3", + family: "MiniMax H3", title: "FastH3 Trim", mark: "H3", path: "/cookbook/minimax-h3/fasth3-trim", description: "The smaller FastH3 model for eight-step video generation with synchronized audio.", tags: ["Text to video", "Audio", "NVFP4", "8-step"], }, + { + id: "fasth3-preview", + family: "MiniMax H3", + title: "FastH3 Preview V0.2", + mark: "H3", + path: "/cookbook/minimax-h3/fasth3-preview", + description: "The few-step FastH3 preview checkpoint on CUDA or native Apple Silicon MLX.", + tags: ["Text to video", "Audio", "4-step"], + }, + { + id: "fasth3-4step", + family: "MiniMax H3", + title: "FastH3 4-Step V1", + mark: "H3", + path: "/cookbook/minimax-h3/fasth3-4step", + description: "FastH3 V1 on one DGX Spark with offloaded encoders and lazy module loading.", + tags: ["Text to video", "Audio", "DGX Spark"], + }, + { + id: "fasth3-omniref", + family: "MiniMax H3", + title: "FastH3 OmniRef", + mark: "H3", + path: "/cookbook/minimax-h3/fasth3-omniref", + description: "Video and audio from a prompt plus ordered image, video, and audio references.", + tags: ["Reference to video", "Audio", "NVFP4"], + featured: true, + }, { id: "fastwan21", family: "Wan", @@ -93,10 +120,31 @@ const familyInfo: Record = { "FastVideo/FastVideo-FastH3-8-Step-V2": "fasth3-8step", "FastVideo/FastVideo-FastH3-Trim-8-Step-NVFP4": "fasth3-trim", + "FastVideo/FastVideo-Minimax-FastH3-Preview-v0.2": "fasth3-preview", + "FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree": "fasth3-4step", + "FastVideo/FastH3-OmniRef-NVFP4-QAD": "fasth3-omniref", "FastVideo/FastWan2.1-T2V-1.3B-Diffusers": "fastwan21", "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": "wan21-i2v", "FastVideo/LTX2-Distilled-Diffusers": "ltx2-distilled", }; +const TASK_GROUPS: Array<[RegExp, string]> = [ + [/world/, "Text to world"], + [/video\/text to audio|video.*to audio/, "Video to audio"], + [/text to audio/, "Text to audio"], + [/text to image/, "Text to image"], + [/reference media to video/, "Reference to video"], + [/first\/last frame to video/, "Image to video"], + [/text or image to video/, "Text/image to video"], + [/image to video/, "Image to video"], + [/super-resolution/, "Video upscale"], + [/video/, "Text to video"], +]; +/** A small discovery vocabulary derived from the recipe task, not a capability claim. */ +export function taskLabel(value: string): string { + const normalized = value.toLowerCase(); + return TASK_GROUPS.find(([pattern]) => pattern.test(normalized))?.[1] ?? "Text to video"; +} + export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.model))] .map((model) => { const recipes = sourceRecipes.filter((recipe) => recipe.model === model); @@ -129,7 +177,7 @@ export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.m : model === "MiniMaxAI/MiniMax-H3" ? "MiniMax H3" : first.label.split(" on ")[0]), - family: first.family === "minimax_h3" ? "FastH3" : (backend?.family ?? family.title), + family: backend?.family ?? family.title, familyPath: `/cookbook/${family.path}`, path: backend?.path ?? `/cookbook/${family.path}/models/${first.id}`, logo, @@ -137,6 +185,7 @@ export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.m category: family.category, description: backend?.description ?? first.summary, tags: backend?.tags ?? [...new Set(recipes.map((recipe) => recipe.task))], + task: taskLabel(backend?.tags[0] ?? first.task), platforms: [...new Set(recipes.flatMap((recipe) => recipe.gpu_types))], configurable: !!backend, featured: backend?.featured ?? false, @@ -145,7 +194,7 @@ export const modelDirectory = [...new Set(sourceRecipes.map((recipe) => recipe.m .sort( (a, b) => Number(b.featured) - Number(a.featured) || - Number(b.family === "FastH3") - Number(a.family === "FastH3") || + Number(b.family === "MiniMax H3") - Number(a.family === "MiniMax H3") || Number(b.configurable) - Number(a.configurable), ); export type DirectoryModel = (typeof modelDirectory)[number]; diff --git a/docs-site/lib/fast-models.ts b/docs-site/lib/fast-models.ts index 60302c919..dd22f1ee6 100644 --- a/docs-site/lib/fast-models.ts +++ b/docs-site/lib/fast-models.ts @@ -2,11 +2,23 @@ export const families = [ { name: "FastH3", mark: "H3", - summary: "Distilled MiniMax-H3 video with synchronized audio.", + summary: "Distilled MiniMax-H3 video with synchronized audio, from text or ordered references.", collection: "https://huggingface.co/collections/FastVideo/fasth3-v2", guide: "/cookbook/minimax-h3", - detail: "8-step V2 \u00b7 FP8 and NVFP4 exports", + detail: "OmniRef \u00b7 8-step V2 \u00b7 QAD v2 \u00b7 FP8 and NVFP4 exports \u00b7 NVFP4 VAE decoder", models: [ + { + id: "FastH3-OmniRef-NVFP4-QAD", + url: "https://huggingface.co/FastVideo/FastH3-OmniRef-NVFP4-QAD", + }, + { + id: "FastH3-QAD-v2", + url: "https://huggingface.co/FastVideo/FastH3-QAD-v2", + }, + { + id: "FastH3-NVFP4-VAE-Decoder", + url: "https://huggingface.co/FastVideo/FastH3-NVFP4-VAE-Decoder", + }, { id: "FastVideo-FastH3-8-Step-V2", url: "https://huggingface.co/FastVideo/FastVideo-FastH3-8-Step-V2", diff --git a/docs-site/lib/hardware.ts b/docs-site/lib/hardware.ts new file mode 100644 index 000000000..4f9205ef8 --- /dev/null +++ b/docs-site/lib/hardware.ts @@ -0,0 +1,352 @@ +import { useEffect, useState } from "react"; + +export type MemoryType = "dedicated" | "unified" | "system"; +export type MemoryUnit = "GB" | "GiB"; + +/** One capacity variant from the shared hardware catalog. */ +export interface HardwareRecord { + id: string; + name: string; + memory_type: MemoryType; + memory_gb: number; + memory_unit: MemoryUnit; + backend: string; + supported_dtypes?: string[]; + sources?: string[]; + notes?: string; + ui_id?: string; + aliases?: string[]; + vendor?: string; + kind?: string; + family?: string; + variant?: string; + form_factor?: string; + gpu_count?: number; + host_ram_gb?: number; + usable_budget_gb?: number; +} + +export interface CustomHardware { + name: string; + backend: string; + memory_type: MemoryType; + memory_gb: number; + memory_unit: MemoryUnit; + usable_budget_gb?: number; + host_ram_gb?: number; +} + +export interface HardwareSelection { + id: string; + custom?: CustomHardware; +} + +export interface HardwareCatalogState { + status: "loading" | "ready" | "unavailable"; + records: HardwareRecord[]; + error?: string; + reload: () => void; +} + +const HARDWARE_PATH = "/assets/cookbook-config/capacity/hardware.json"; +const CUSTOM_PARAMS = ["hwName", "hwBackend", "hwType", "hwMemory", "hwUnit", "hwBudget", "hwRam"] as const; + +const MEMORY_TYPES: MemoryType[] = ["dedicated", "unified", "system"]; + +function text(value: unknown, max = 200): string | undefined { + return typeof value === "string" && value.trim() ? value.trim().slice(0, max) : undefined; +} + +function positive(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 4096 ? value : undefined; +} + +function numberText(value: string | null): number | undefined { + if (!value) return undefined; + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 && parsed <= 4096 ? parsed : undefined; +} + +function memoryType(value: unknown): MemoryType | undefined { + return MEMORY_TYPES.includes(value as MemoryType) ? (value as MemoryType) : undefined; +} + +function memoryUnit(value: unknown): MemoryUnit { + return value === "GiB" ? "GiB" : "GB"; +} + +function backend(value: unknown, fallback = "cuda"): string { + if (value === "mlx" || value === "metal") return "mlx"; + if (value === "cuda" || value === "rocm") return value; + return fallback; +} + +/** Catalog records keep backends the cookbook cannot run, so they never pose as CUDA targets. */ +function catalogBackend(value: unknown): string { + if (value === "sycl" || value === "unknown") return value; + return backend(value, "unknown"); +} + +/** Accepts the generated `{ version, devices: [] }` asset and the older mapping forms. */ +function recordsFrom(payload: unknown): HardwareRecord[] { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) return []; + const container = payload as Record; + let entries: Array<[string, unknown]> = []; + if (Array.isArray(container.devices)) { + entries = container.devices.map((entry) => { + const item = entry && typeof entry === "object" ? (entry as Record) : {}; + return [text(item.id, 120) ?? "", entry]; + }); + } else { + const source = + container.hardware && typeof container.hardware === "object" && !Array.isArray(container.hardware) + ? (container.hardware as Record) + : container; + entries = Object.entries(source); + } + const records: HardwareRecord[] = []; + for (const [fallbackId, value] of entries) { + if (!value || typeof value !== "object" || Array.isArray(value)) continue; + const item = value as Record; + const id = text(item.id, 120) ?? text(fallbackId, 120); + const name = text(item.name, 120); + const memory = + item.memory && typeof item.memory === "object" ? (item.memory as Record) : undefined; + const nominal = + memory?.nominal && typeof memory.nominal === "object" ? (memory.nominal as Record) : undefined; + const capacity = positive(nominal?.value) ?? positive(item.memory_gb); + const type = memoryType(memory?.type) ?? memoryType(item.memory_type); + if (!id || !name || !capacity || !type) continue; + const usable = + memory?.usable && typeof memory.usable === "object" ? (memory.usable as Record) : undefined; + const host = + item.host_memory && typeof item.host_memory === "object" + ? (item.host_memory as Record) + : undefined; + const aliases = Array.isArray(item.aliases) + ? item.aliases.map((alias) => text(alias, 80)).filter((alias): alias is string => !!alias) + : undefined; + const sources = Array.isArray(item.sources) + ? item.sources + .map((source) => + typeof source === "string" + ? text(source, 500) + : source && typeof source === "object" + ? text((source as Record).url, 500) + : undefined, + ) + .filter((url): url is string => !!url) + : undefined; + records.push({ + id, + name, + memory_type: type, + memory_gb: capacity, + memory_unit: memoryUnit(nominal?.unit ?? item.memory_unit), + backend: catalogBackend(item.backend), + supported_dtypes: Array.isArray(item.supported_dtypes) + ? item.supported_dtypes.filter((dtype): dtype is string => typeof dtype === "string") + : undefined, + sources, + notes: text(item.notes, 600), + ui_id: text(item.ui_id, 40), + aliases, + vendor: text(item.vendor, 80), + kind: text(item.kind, 40), + family: text(item.family, 80), + variant: text(item.variant, 80), + form_factor: text(item.form_factor, 40), + gpu_count: positive(item.gpu_count), + host_ram_gb: positive(host?.value) ?? positive(item.host_memory_gb) ?? positive(item.host_ram_gb), + usable_budget_gb: positive(usable?.value) ?? positive(item.usable_memory_gb) ?? positive(item.usable_budget_gb), + }); + } + return records.sort((a, b) => a.name.localeCompare(b.name)); +} + +/** Loads only the shared lookup data. It does not estimate fit or inspect the browser's hardware. */ +export function useHardwareCatalog(): HardwareCatalogState { + const [attempt, setAttempt] = useState(0); + const [state, setState] = useState>({ + status: "loading", + records: [], + }); + useEffect(() => { + if (typeof window === "undefined") return; + const controller = new AbortController(); + const url = new URL(`${import.meta.env.BASE_URL.replace(/\/$/, "")}${HARDWARE_PATH}`, window.location.origin); + setState({ status: "loading", records: [] }); + fetch(url.href, { signal: controller.signal }) + .then(async (response) => { + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return recordsFrom(await response.json()); + }) + .then((records) => { + if (controller.signal.aborted) return; + setState( + records.length + ? { status: "ready", records } + : { status: "unavailable", records: [], error: "The hardware catalog is empty." }, + ); + }) + .catch((error) => { + if (!controller.signal.aborted) + setState({ + status: "unavailable", + records: [], + error: error instanceof Error ? error.message : "The hardware catalog could not be loaded.", + }); + }); + return () => controller.abort(); + }, [attempt]); + return { ...state, reload: () => setAttempt((current) => current + 1) }; +} + +export function readHardwareSelection(search: URLSearchParams): HardwareSelection { + const requested = search.get("hardware")?.trim() ?? ""; + if (requested.startsWith("custom:")) { + return { + id: "custom", + custom: { + name: requested.slice(7).slice(0, 80), + backend: "cuda", + memory_type: "dedicated", + memory_gb: 0, + memory_unit: "GB", + }, + }; + } + if (requested !== "custom") return { id: requested }; + return { + id: "custom", + custom: { + name: text(search.get("hwName"), 80) ?? "", + backend: backend(search.get("hwBackend")), + memory_type: memoryType(search.get("hwType")) ?? "dedicated", + memory_gb: numberText(search.get("hwMemory")) ?? 0, + memory_unit: memoryUnit(search.get("hwUnit")), + usable_budget_gb: numberText(search.get("hwBudget")), + host_ram_gb: numberText(search.get("hwRam")), + }, + }; +} + +export function writeHardwareSelection(target: URL | URLSearchParams, selection: HardwareSelection) { + const search = target instanceof URL ? target.searchParams : target; + for (const key of CUSTOM_PARAMS) search.delete(key); + if (!selection.id) { + search.delete("hardware"); + return; + } + search.set("hardware", selection.id); + if (selection.id !== "custom" || !selection.custom) return; + const custom = selection.custom; + if (custom.name) search.set("hwName", custom.name.slice(0, 80)); + search.set("hwBackend", custom.backend); + search.set("hwType", custom.memory_type); + if (custom.memory_gb > 0) search.set("hwMemory", String(custom.memory_gb)); + search.set("hwUnit", custom.memory_unit); + if (custom.usable_budget_gb) search.set("hwBudget", String(custom.usable_budget_gb)); + if (custom.host_ram_gb) search.set("hwRam", String(custom.host_ram_gb)); +} + +export function resolveHardware(selection: HardwareSelection, records: HardwareRecord[]): HardwareRecord | undefined { + if (selection.id === "custom") { + const custom = selection.custom; + if (!custom?.name.trim() || !custom.memory_gb) return undefined; + return { + id: "custom", + name: custom.name.trim(), + memory_type: custom.memory_type, + memory_gb: custom.memory_gb, + memory_unit: custom.memory_unit, + backend: custom.backend, + usable_budget_gb: + custom.usable_budget_gb && custom.usable_budget_gb <= custom.memory_gb ? custom.usable_budget_gb : undefined, + host_ram_gb: custom.host_ram_gb, + }; + } + return ( + records.find((record) => record.id === selection.id) ?? + records.find((record) => record.aliases?.includes(selection.id)) + ); +} + +/** A query-only selector shared by the hardware picker and tests. */ +export function searchHardware(records: HardwareRecord[], query: string): HardwareRecord[] { + const tokens = query + .toLowerCase() + .split(/\s+/) + .map((token) => token.trim()) + .filter(Boolean); + if (!tokens.length) return records; + return records.filter((record) => { + const haystack = [ + record.name, + record.id, + record.ui_id, + record.vendor, + record.kind, + record.family, + record.variant, + record.form_factor, + record.gpu_count ? `${record.gpu_count}gpu` : undefined, + record.backend, + record.memory_type, + `${record.memory_gb}gb`, + `${record.memory_gb}gib`, + ...(record.aliases ?? []), + ] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return tokens.every((token) => haystack.includes(token)); + }); +} + +export function memoryText(record: HardwareRecord, includeUnit = true): string { + const value = `${record.memory_gb} ${record.memory_unit}`; + if (!includeUnit) return value; + if (record.memory_type === "dedicated") return `${value} VRAM`; + if (record.memory_type === "unified") return `${value} unified memory`; + return `${value} system memory`; +} + +export function memoryContext(record: HardwareRecord): string { + if (record.memory_type === "dedicated") return "Dedicated GPU memory. It is per GPU and is not shared with host RAM."; + if (record.memory_type === "unified") + return "Unified memory is shared by the CPU and GPU; the full amount is not available to one process."; + return "System memory is host RAM used by the CPU and any offload."; +} + +export function backendLabel(value: string): string { + if (value === "mlx") return "Apple MLX"; + if (value === "rocm") return "ROCm"; + if (value === "sycl") return "Intel SYCL"; + if (value === "unknown") return "Unknown backend"; + return "CUDA"; +} + +/** GPU count never multiplies a catalog capacity into a pool. */ +export function gpuScopeNote(record: HardwareRecord | undefined, gpuCount: number): string | undefined { + if (!record || gpuCount <= 1) return undefined; + if (record.memory_type === "unified") + return `${gpuCount} GPUs share unified memory. This catalog entry describes one system; its ${record.memory_gb} ${record.memory_unit} is not multiplied.`; + if (record.memory_type === "system") + return `${gpuCount} GPUs use host system memory. This entry describes one host; the total is not multiplied.`; + return `${gpuCount} GPUs × ${record.memory_gb} ${record.memory_unit} VRAM. Memory is per device, not a combined pool.`; +} + +export function gpuCountFrom(config: Record | undefined): number { + const generator = config?.generator; + if (!generator || typeof generator !== "object" || Array.isArray(generator)) return 1; + const root = generator as Record; + const engine = + root.engine && typeof root.engine === "object" && !Array.isArray(root.engine) + ? (root.engine as Record) + : undefined; + for (const value of [root.num_gpus, engine?.num_gpus]) { + if (typeof value === "number" && Number.isInteger(value) && value > 0) return value; + } + return 1; +} diff --git a/docs-site/navigation.json b/docs-site/navigation.json index f265cec93..d9a18ed76 100644 --- a/docs-site/navigation.json +++ b/docs-site/navigation.json @@ -142,10 +142,18 @@ "href": "/cookbook/openai-api" }, { - "label": "FastH3", + "label": "MiniMax H3", "display": "group", "collapsed": false, "items": [ + { + "label": "MiniMax H3", + "href": "/cookbook/minimax-h3/models/minimax-h3-t2v" + }, + { + "label": "FastH3 OmniRef", + "href": "/cookbook/minimax-h3/fasth3-omniref" + }, { "label": "FastH3 8-Step V2", "href": "/cookbook/minimax-h3/fasth3-8step" @@ -154,6 +162,14 @@ "label": "FastH3 Trim", "href": "/cookbook/minimax-h3/fasth3-trim" }, + { + "label": "FastH3 Preview V0.2", + "href": "/cookbook/minimax-h3/fasth3-preview" + }, + { + "label": "FastH3 4-Step V1", + "href": "/cookbook/minimax-h3/fasth3-4step" + }, { "label": "Recipes and setup", "href": "/cookbook/minimax-h3" diff --git a/docs-site/package-lock.json b/docs-site/package-lock.json index 371dcf7ec..6a69593a3 100644 --- a/docs-site/package-lock.json +++ b/docs-site/package-lock.json @@ -9,6 +9,8 @@ "blume": "2.2.3", "class-variance-authority": "0.7.1", "cn": "^0.4.0", + "lucide-react": "1.59.0", + "motion": "14.4.0", "radix-ui": "^1.8.1", "react": "19.3.1", "react-dom": "19.3.1", @@ -8066,6 +8068,29 @@ "node": ">= 0.6" } }, + "node_modules/framer-motion": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.4.0.tgz", + "integrity": "sha512-HtO1fxord2rnXYRyr1laG4uY7+OtgwDc9Szcg1wlMq/LF7BO3/S7a+KEo691U7pSi+i0ueRuRqaD1UaLEmZHuQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "14.4.0", + "motion-utils": "14.1.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -9109,6 +9134,21 @@ "node": "20 || >=22" } }, + "node_modules/lucide-react": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.59.0.tgz", + "integrity": "sha512-5qgMyyCPU6OvnKpLS2t693+ZtiqA8oaSweWoUows0BLFKFf6V75m4+CptpmohZHpD5IuOVIBXHBBiv2vpLJmtw==", + "license": "ISC", + "peerDependencies": { + "@types/react": "*", + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -10127,6 +10167,43 @@ "node": ">= 18" } }, + "node_modules/motion": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-14.4.0.tgz", + "integrity": "sha512-JJTIdeNjtF2zi/py0ndRKxQF+8z2pcDUgJYXul7zqbhMvyTLRsPFR9ifE/j4cFCpCMlehQVJkTwd7BZFiPKgQg==", + "license": "MIT", + "dependencies": { + "framer-motion": "14.4.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.4.0.tgz", + "integrity": "sha512-1q8RyHSdr5COpzrv3JgJ9XRjOO88ZN9CwflpkGgyO/SR/SNRP/dD6EU8SEo092kcKY34IzHC5diJPKrQRMA8lQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "14.1.1" + } + }, + "node_modules/motion-utils": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-14.1.1.tgz", + "integrity": "sha512-3VvN5Uvf8B6qG+OfOui6Nht5EWp+vLuU3IqcAj7ItZGKqtmTjCebl5sGZF7/08gEHRnEl1AP/fIwbcEyVrcQGQ==", + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", diff --git a/docs-site/package.json b/docs-site/package.json index b6eb4d4fa..e8f752114 100644 --- a/docs-site/package.json +++ b/docs-site/package.json @@ -21,6 +21,8 @@ "blume": "2.2.3", "class-variance-authority": "0.7.1", "cn": "^0.4.0", + "lucide-react": "1.59.0", + "motion": "14.4.0", "radix-ui": "^1.8.1", "react": "19.3.1", "react-dom": "19.3.1", diff --git a/docs-site/playwright.config.ts b/docs-site/playwright.config.ts index dcdae4397..ee1e14bab 100644 --- a/docs-site/playwright.config.ts +++ b/docs-site/playwright.config.ts @@ -1,12 +1,17 @@ import { defineConfig } from "@playwright/test"; +const externalBase = process.env.PLAYWRIGHT_BASE_URL; +const localBase = "http://127.0.0.1:4327"; + export default defineConfig({ testDir: "./tests", testMatch: "**/*.spec.ts", - use: { baseURL: "http://127.0.0.1:4327", trace: "retain-on-failure" }, - webServer: { - command: "npm run preview -- --host 127.0.0.1 --port 4327", - url: "http://127.0.0.1:4327/FastVideo/cookbook", - reuseExistingServer: !process.env.CI, - }, + use: { baseURL: externalBase ?? localBase, trace: "retain-on-failure" }, + webServer: externalBase + ? undefined + : { + command: "npm run preview -- --host 127.0.0.1 --port 4327", + url: `${localBase}/FastVideo/cookbook`, + reuseExistingServer: !process.env.CI, + }, }); diff --git a/docs-site/scripts/transfer-docs.mjs b/docs-site/scripts/transfer-docs.mjs index a349f2b4a..e76799474 100644 --- a/docs-site/scripts/transfer-docs.mjs +++ b/docs-site/scripts/transfer-docs.mjs @@ -350,7 +350,10 @@ export async function transferDocs() { } else if (item.items) { const guide = item.items.find((child) => /Family guide|Recipes and setup/.test(child.label)); if (guide) { - const extras = modelDirectory.filter((model) => !model.configurable && model.familyPath === guide.href); + const listed = new Set(item.items.map((child) => child.href)); + const extras = modelDirectory.filter( + (model) => !model.configurable && model.familyPath === guide.href && !listed.has(model.path), + ); item.items.splice( item.items.indexOf(guide), 0, diff --git a/docs-site/tests/commands.test.mjs b/docs-site/tests/commands.test.mjs index 91e00f075..dba6ae324 100644 --- a/docs-site/tests/commands.test.mjs +++ b/docs-site/tests/commands.test.mjs @@ -98,3 +98,15 @@ test("Wan I2V inference requires a source image and preserves the serving baseli const existing = parse(inferenceOutput({ ...result, config: withDefaults }).yaml); assert.deepEqual(existing.request.inputs, withDefaults.default_request.inputs); }); + +test("OmniRef inference writes the source image as an ordered Ref2VA reference", () => { + const config = parse( + readFileSync(new URL("../../examples/serving/openai_fasth3_omniref_rtx5090.yaml", import.meta.url), "utf8"), + ); + const result = { config, command: "fastvideo serve --config config.yaml" }; + assert.throws(() => inferenceOutput(result), /source image path/); + const saved = parse(inferenceOutput(result, " /data/reference.png ").yaml); + assert.deepEqual(saved.request.inputs.references, [{ source: "/data/reference.png", media_type: "image" }]); + assert.equal(saved.request.inputs.image_path, undefined); + assert.equal(saved.generator.pipeline.components.override_pipeline_cls_name, "MiniMaxH3Ref2VAModularPipeline"); +}); diff --git a/docs-site/tests/cookbook.spec.ts b/docs-site/tests/cookbook.spec.ts index 102874123..40e41486f 100644 --- a/docs-site/tests/cookbook.spec.ts +++ b/docs-site/tests/cookbook.spec.ts @@ -6,9 +6,25 @@ const route = "/FastVideo/cookbook/minimax-h3/fasth3-8step"; // deterministic when GitHub/Trendshift are slow or unreachable. test.beforeEach(async ({ page }) => { await page.route(/^https?:\/\/(?!127\.0\.0\.1|localhost)/, (request) => request.abort()); + await page.route("**/assets/cookbook-config/capacity/hardware.json", (route) => + route.fulfill({ path: "tests/fixtures/hardware.json", contentType: "application/json" }), + ); }); +async function chooseHardware(page: Page, query: string) { + const search = page.getByLabel("Search hardware", { exact: true }); + if ((await search.count()) === 0) + await page + .getByRole("button", { name: /Find or add|Choose hardware|Change hardware/ }) + .first() + .click(); + await search.fill(query); + await page + .getByRole("radio", { name: new RegExp(query, "i") }) + .first() + .click(); +} async function settings(page: Page, section: string) { - await expect(page.locator(".recipe-matrix")).toBeVisible(); + await expect(page.locator(".recipe-matrix")).toBeVisible({ timeout: 30000 }); if (section === "Serving" || section === "Hardware") { const advanced = page.locator(".matrix-advanced"); if ((await advanced.getAttribute("open")) === null) await advanced.locator("summary").first().click(); @@ -100,14 +116,36 @@ test("deployment changes reset edits and support MLX and streaming", async ({ pa await settings(page, "Serving"); await expect(page.getByLabel("Port", { exact: true })).toHaveValue("8000"); await page.goto("/FastVideo/cookbook/ltx/ltx2-distilled"); + await expect(page.locator(".recipe-matrix")).toBeVisible({ timeout: 30000 }); await expect(page.getByLabel("Client command", { exact: true })).toContainText("/health"); await page.locator(".recipe-request-code > summary").click(); await expect(page.getByText(/WebSocket URL:/)).toBeVisible(); await page.goto("/FastVideo/cookbook/wan/wan21-i2v"); + await expect(page.locator(".recipe-matrix")).toBeVisible({ timeout: 30000 }); await expect(page.getByLabel("Client command", { exact: true })).toContainText("input_reference"); await expect(page.getByRole("heading", { level: 1 })).toHaveText("Wan2.1 I2V 14B"); }); +test("runtime and hardware choices support browser back", async ({ page }) => { + await page.goto(route); + await expect(page.locator(".recipe-matrix")).toBeVisible({ timeout: 30000 }); + await backend(page, "MLX"); + await expect(page).toHaveURL(/deployment=fasth3-8step%2Fmlx-rest/); + await backend(page, "CUDA"); + await expect(page).toHaveURL(/deployment=fasth3-8step%2Fcuda-rest/); + await page.goBack(); + await expect(page).toHaveURL(/deployment=fasth3-8step%2Fmlx-rest/); + await expect(page.getByRole("button", { name: "MLX", exact: true })).toHaveAttribute("aria-pressed", "true"); + await page.goForward(); + await expect(page).toHaveURL(/deployment=fasth3-8step%2Fcuda-rest/); + await chooseHardware(page, "RTX 5090"); + await expect(page).toHaveURL(/hardware=geforce-rtx-5090-32gb/); + await page.goBack(); + await expect(page).not.toHaveURL(/hardware=/); + await page.goForward(); + await expect(page).toHaveURL(/hardware=geforce-rtx-5090-32gb/); +}); + test("failed loads offer retry without keeping stale commands", async ({ page }) => { await page.route("**/recipes/fasth3-8step/cuda-rest.json", (route) => route.fulfill({ status: 503, body: "unavailable" }), @@ -185,8 +223,8 @@ test("inherited values, explicit false, zero and null retain their meanings", as test("catalog discovery opens dedicated model recipes and filters by family and task", async ({ page }) => { await page.goto("/FastVideo/cookbook"); const cards = page.locator(".recipe-card"); - await expect(cards).toHaveCount(34); - await expect(cards.first()).toContainText("FastH3 8-Step V2"); + await expect(cards).toHaveCount(35); + await expect(cards.first()).toContainText("FastH3 OmniRef"); await page.getByLabel("Model family", { exact: true }).selectOption("Wan"); await page.getByLabel("Search recipes").fill("Wan2.1 I2V"); await expect(cards).toHaveCount(1); @@ -202,8 +240,9 @@ test("catalog discovery opens dedicated model recipes and filters by family and test("hardware preferences stay distinct from schema-backed GPU count and topology", async ({ page }) => { await page.goto(route); - await page.getByRole("radio", { name: "RTX PRO 6000" }).check(); - await expect(page).toHaveURL(/hardware=rtx/); + await chooseHardware(page, "RTX PRO 6000"); + await expect(page).toHaveURL(/hardware=rtx-pro-6000-blackwell-workstation-96gb/); + await expect(page.locator(".hardware-selected")).toContainText("96 GB VRAM"); await settings(page, "Hardware"); await page.getByLabel("Tensor parallelism", { exact: true }).fill("2"); await page.getByRole("button", { name: "Use automatic parallelism" }).click(); @@ -238,7 +277,7 @@ test("output tabs support keyboard navigation and precision chips use the backen await page.getByLabel("Search recipes").fill("does not exist"); await expect(page.getByRole("heading", { name: "No matching recipes" })).toBeVisible(); await page.getByRole("button", { name: "Clear filters" }).click(); - await expect(page.locator(".recipe-card")).toHaveCount(34); + await expect(page.locator(".recipe-card")).toHaveCount(35); }); test("cookbook cards and controls remain readable in dark mode and reflow at 320px", async ({ page }) => { @@ -303,9 +342,9 @@ test("FastH3 Trim edits drive inference, serving, and complete copyable launch s }); }); await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-trim"); - await page.getByRole("radio", { name: "DGX Spark GB10", exact: true }).check(); - await expect(page.getByRole("radio", { name: "DGX Spark GB10", exact: true })).toBeChecked(); - await expect(page.getByRole("radio", { name: "H100 Data center", exact: true })).toHaveCount(0); + await chooseHardware(page, "DGX Spark"); + await expect(page).toHaveURL(/hardware=dgx-spark-gb10-128gb/); + await expect(page.getByText(/128 GB unified memory/)).toBeVisible(); await page.getByLabel("Prompt", { exact: true }).fill("A potter finishes a blue bowl."); await page.getByLabel("Seed", { exact: true }).fill("0"); await page.getByRole("button", { name: "Copy config + command", exact: true }).click(); @@ -387,50 +426,93 @@ test("source-backed serving pages expose their actual server settings", async ({ test("source model hardware preferences survive hydration and reload", async ({ page }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); - await page.goto("/FastVideo/cookbook/flux/models/flux2-klein-t2i?hardware=custom%3ARTX%204090"); - await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked(); + await page.goto( + "/FastVideo/cookbook/flux/models/flux2-klein-t2i?hardware=custom&hwName=RTX+4090&hwType=dedicated&hwMemory=24&hwUnit=GB", + ); + await expect(page.getByText("RTX 4090", { exact: true })).toBeVisible(); + await expect(page.locator(".hardware-selected")).toContainText("24 GB VRAM"); await page.reload(); - await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked(); + await expect(page.getByText("RTX 4090", { exact: true })).toBeVisible(); expect(errors).toEqual([]); }); test("custom devices persist and no preview control can block generated YAML", async ({ page }) => { await page.goto(route); await page.getByRole("button", { name: "+ Find or add a GPU", exact: true }).click(); + await page.getByRole("button", { name: "Can't find it? Add a device", exact: true }).click(); await page.getByLabel("Device name", { exact: true }).fill("RTX 4090"); - await page.getByRole("button", { name: "Add device", exact: true }).click(); - await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked(); + await page.getByRole("combobox", { name: "Memory type", exact: true }).selectOption("dedicated"); + await page.getByRole("spinbutton", { name: "Nominal memory", exact: true }).fill("24"); + await page.getByRole("spinbutton", { name: "Usable budget (optional)", exact: true }).fill("32"); + await expect(page.getByText("The usable budget cannot exceed the nominal memory.", { exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: "Use this device", exact: true })).toBeDisabled(); + await page.getByRole("spinbutton", { name: "Usable budget (optional)", exact: true }).fill("22"); + await page.getByRole("spinbutton", { name: "Host RAM (optional)", exact: true }).fill("64"); + await page.getByRole("button", { name: "Use this device", exact: true }).click(); + await expect(page.getByText("RTX 4090", { exact: true })).toBeVisible(); + await expect(page.locator(".hardware-selected")).toContainText("24 GB VRAM"); + await expect(page.locator(".hardware-selected")).toContainText("Your budget: 22 GB"); + await expect(page.locator(".hardware-selected")).toContainText("Host RAM: 64 GB"); + await expect(page).toHaveURL(/hardware=custom/); + await expect(page).toHaveURL(/hwName=RTX\+4090/); await page.reload(); - await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toBeChecked(); + await expect(page.getByText("RTX 4090", { exact: true })).toBeVisible(); + await expect(page.locator(".hardware-selected")).toContainText("24 GB VRAM"); await expect(page.locator(".component-plan")).toHaveCount(0); await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeEnabled(); await page.getByRole("button", { name: "Clear hardware target", exact: true }).click(); - await expect(page.getByRole("radio", { name: "RTX 4090 Custom device", exact: true })).toHaveCount(0); + await expect(page.getByText("RTX 4090", { exact: true })).toHaveCount(0); await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible(); }); -test("hardware is never preselected and a target does not change generated YAML", async ({ page }) => { +test("hardware is never preselected; a single-GPU device sets one GPU with valid parallelism", async ({ page }) => { await page.goto(route); await expect(page.locator(".hardware-picker")).toBeVisible({ timeout: 30000 }); - await expect(page.locator('input[type="radio"]:checked')).toHaveCount(0); await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible(); await output(page, "config.yaml"); const yaml = page.getByLabel("Generated YAML", { exact: true }); - const baseline = await yaml.innerText(); - await page.getByRole("radio", { name: /RTX 5090/ }).check(); - await expect(page.getByRole("radio", { name: /RTX 5090/ })).toBeChecked(); - await expect(page.getByText(/RTX 5090 is saved as your target/)).toBeVisible(); - await expect(yaml).toHaveText(baseline); + await expect(yaml).toContainText('"num_gpus": 4'); + await chooseHardware(page, "RTX 5090"); + await expect(page).toHaveURL(/hardware=geforce-rtx-5090-32gb/); + await expect(page.getByText(/GeForce RTX 5090 .*32 GB VRAM/)).toBeVisible(); + await expect(page.getByText(/Set to 1 GPU for .*RTX 5090/)).toBeVisible(); + await expect(yaml).toContainText('"num_gpus": 1'); + await expect(yaml).toContainText('"sp_size": -1'); + await expect(yaml).not.toContainText('"hardware"'); + await expect(page.getByText("Valid configuration", { exact: true })).toBeVisible(); await page.getByRole("button", { name: "Clear hardware target", exact: true }).click(); - await expect(page.locator('input[type="radio"]:checked')).toHaveCount(0); await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible(); }); -test("a saved target missing from the list stays visible and clearable", async ({ page }) => { - await page.goto(`${route}?hardware=retired-gpu`); - await expect(page.getByText("Saved hardware target is not in this list.", { exact: true })).toBeVisible(); - await page.getByRole("button", { name: "Clear hardware target", exact: true }).click(); +test("lowering the GPU count resets parallelism that no longer fits", async ({ page }) => { + await page.goto(route); + await output(page, "config.yaml"); + const yaml = page.getByLabel("Generated YAML", { exact: true }); + await page.getByLabel("GPU count", { exact: true }).fill("2"); + await expect(page.getByText("Parallelism set to automatic for 2 GPUs.")).toBeVisible(); + await expect(yaml).toContainText('"num_gpus": 2'); + await expect(yaml).toContainText('"sp_size": -1'); + await expect(page.getByText("Valid configuration", { exact: true })).toBeVisible(); +}); + +test("browser back after a hardware-only change keeps the editor loaded", async ({ page }) => { + await page.goto(route); + await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeEnabled(); + await chooseHardware(page, "RTX 5090"); + await expect(page).toHaveURL(/hardware=geforce-rtx-5090-32gb/); + await page.goBack(); await expect(page).not.toHaveURL(/hardware=/); + await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: "Download YAML", exact: true })).toBeEnabled(); + await expect(page.getByText("Loading deployment settings…")).toHaveCount(0); +}); + +test("hardware links resolve catalog aliases and non-CUDA devices are not CUDA targets", async ({ page }) => { + await page.goto(`${route}?hardware=5090`); + await expect(page.locator(".hardware-selected")).toContainText("RTX 5090"); + await page.getByRole("button", { name: "Change hardware", exact: true }).click(); + await page.getByLabel("Search hardware", { exact: true }).fill("Arc B580"); + await expect(page.getByRole("radio", { name: /Arc B580/ })).toHaveCount(0); }); test("recipe facts separate validity, fit, and run evidence with inspectable assumptions", async ({ page }) => { @@ -444,6 +526,97 @@ test("recipe facts separate validity, fit, and run evidence with inspectable ass await expect(page.getByText(/do not establish this configuration/)).toBeVisible(); }); +test("catalog lookup resolves aliases, variants, and unified memory without pooling", async ({ page }) => { + await page.goto(route); + await chooseHardware(page, "PRO 6000"); + await expect(page).toHaveURL(/hardware=rtx-pro-6000-blackwell-workstation-96gb/); + await expect(page.locator(".hardware-selected")).toContainText("96 GB VRAM"); + await expect(page.locator(".hardware-address-note")).toContainText("Per device, not a combined pool"); + await page.getByRole("button", { name: "Clear hardware target", exact: true }).click(); + await page.getByRole("button", { name: "+ Find or add a GPU", exact: true }).click(); + await page.getByLabel("Search hardware", { exact: true }).fill("GB10"); + await page.getByRole("radio", { name: /DGX Spark/ }).click(); + await expect(page.locator(".hardware-selected")).toContainText("128 GB unified memory"); + await expect(page.locator(".hardware-selected")).toContainText("CUDA"); + await expect(page.locator(".hardware-address-note")).toContainText("not dedicated VRAM"); + await settings(page, "Hardware"); + await page.getByLabel("GPU count", { exact: true }).fill("8"); + await expect(page.getByText(/8 GPUs share unified memory/)).toBeVisible(); + await expect(page.getByText(/its 128 GB is not multiplied/)).toBeVisible(); +}); + +test("MLX lookup uses unified memory and a custom Mac stays reusable", async ({ page }) => { + await page.goto("/FastVideo/cookbook/minimax-h3/fasth3-8step?deployment=fasth3-8step%2Fmlx-rest"); + await page.getByRole("button", { name: "+ Find or add a Mac", exact: true }).click(); + await page.getByLabel("Search hardware", { exact: true }).fill("M4 Max"); + await page.getByRole("radio", { name: /Apple M4 Max/ }).click(); + await expect(page.locator(".hardware-selected")).toContainText("128 GB unified memory"); + await expect(page.locator(".hardware-selected")).toContainText("Apple MLX"); + await expect(page.locator(".hardware-selected")).toContainText("Usable budget: 96 GB"); + await expect(page.locator(".hardware-address-note")).toContainText("not dedicated VRAM"); + await expect(page.getByRole("button", { name: "Clear hardware target", exact: true })).toBeVisible(); +}); + +test("the directory filters by runtime without a hardware picker", async ({ page }) => { + await page.goto("/FastVideo/cookbook"); + await expect(page.locator(".recipe-directory .hardware-picker")).toHaveCount(0); + await page.getByLabel("Runtime", { exact: true }).selectOption("mlx"); + await expect(page).toHaveURL(/runtime=mlx/); + const card = page.locator(".recipe-card").filter({ hasText: "FastMetal 1.3B" }); + await expect(card).toBeVisible(); + await expect(card).toHaveAttribute("href", /\/cookbook\/wan\/models\/[^?]+$/); + await expect(page.locator(".recipe-card").filter({ hasText: "FastWan2.1 1.3B" })).toHaveCount(0); +}); + +test("task discovery filters the directory and round-trips through the URL", async ({ page }) => { + await page.goto("/FastVideo/cookbook"); + await page.getByLabel("Task", { exact: true }).selectOption("Image to video"); + await expect(page).toHaveURL(/task=Image\+to\+video/); + await expect(page.locator(".recipe-card").filter({ hasText: "Wan2.1 I2V 14B" })).toBeVisible(); + await expect(page.locator(".recipe-card").filter({ hasText: "FastWan2.1 1.3B" })).toHaveCount(0); + await page.reload(); + await expect(page.getByLabel("Task", { exact: true })).toHaveValue("Image to video"); + await page.getByLabel("Task", { exact: true }).selectOption("All tasks"); + await expect(page).not.toHaveURL(/task=/); +}); + +test("directory filters round-trip through the URL", async ({ page }) => { + await page.goto("/FastVideo/cookbook?category=Image&runtime=cuda"); + await expect(page.getByRole("button", { name: "Image", exact: true })).toHaveAttribute("aria-pressed", "true"); + await expect(page.getByLabel("Runtime", { exact: true })).toHaveValue("cuda"); + await expect(page.locator(".recipe-card")).toHaveCount(4); + await page.reload(); + await expect(page.getByRole("button", { name: "Image", exact: true })).toHaveAttribute("aria-pressed", "true"); + await expect(page.getByLabel("Runtime", { exact: true })).toHaveValue("cuda"); + await expect(page.locator(".recipe-card")).toHaveCount(4); +}); + +test("a saved target missing from the catalog stays visible and clearable", async ({ page }) => { + await page.goto(`${route}?hardware=retired-gpu`); + await expect(page.getByText(/Saved hardware target \(retired-gpu\) is not in this catalog/)).toBeVisible(); + await page.getByRole("button", { name: "Clear hardware target", exact: true }).click(); + await expect(page).not.toHaveURL(/hardware=/); + await expect(page.getByText("No hardware target selected.", { exact: true })).toBeVisible(); +}); + +test("catalog search renders one plain no-results message", async ({ page }) => { + await page.goto(route); + await page.getByRole("button", { name: "+ Find or add a GPU", exact: true }).click(); + await page.getByLabel("Search hardware", { exact: true }).fill("no-such-device"); + await expect(page.getByText("No matching devices.", { exact: true })).toHaveCount(1); + await page.getByRole("button", { name: "Can't find it? Add a device", exact: true }).click(); + await expect(page.getByLabel("Device name", { exact: true })).toBeVisible(); +}); + +test("hardware lookup closes with Escape and returns focus", async ({ page }) => { + await page.goto(route); + await page.getByRole("button", { name: "+ Find or add a GPU", exact: true }).click(); + await expect(page.getByLabel("Search hardware", { exact: true })).toBeFocused(); + await page.keyboard.press("Escape"); + await expect(page.getByLabel("Search hardware", { exact: true })).toHaveCount(0); + await expect(page.getByRole("button", { name: "+ Find or add a GPU", exact: true })).toBeFocused(); +}); + test("WebSocket deployments expose only the serving interface", async ({ page }) => { await page.goto("/FastVideo/cookbook/ltx/ltx2-distilled"); const modes = page.getByRole("group", { name: "Run mode" }); diff --git a/docs-site/tests/documentation.spec.ts b/docs-site/tests/documentation.spec.ts index a00a152f6..91d19af61 100644 --- a/docs-site/tests/documentation.spec.ts +++ b/docs-site/tests/documentation.spec.ts @@ -33,8 +33,8 @@ test("documentation navigation opens transferred guides", async ({ page }) => { await page.goto("/FastVideo"); await expect(page.getByRole("heading", { name: "Welcome to FastVideo", exact: true })).toBeVisible(); await page.locator("#blume-nav-drawer").getByRole("link", { name: "Installation", exact: true }).click(); - await expect(page).toHaveURL(/getting_started\/installation/); - await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + await expect(page).toHaveURL(/getting_started\/installation/, { timeout: 30000 }); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible({ timeout: 30000 }); }); test("the welcome page introduces the project and opens the cookbook", async ({ page }) => { @@ -133,13 +133,14 @@ test("generated module links stay inside the transferred Python reference", asyn }); test("search finds a transferred guide and opens it by keyboard", async ({ page }) => { + test.setTimeout(180000); await page.goto("/FastVideo"); await page.getByRole("button", { name: "Search", exact: true }).click(); const query = page.getByRole("combobox", { name: "Search docs", exact: true }); await query.fill("offloading"); - await expect(page.getByRole("option", { name: /^Offloading/ }).first()).toBeVisible(); + await expect(page.getByRole("option", { name: /^Offloading/ }).first()).toBeVisible({ timeout: 120000 }); await query.press("Enter"); - await expect(page).toHaveURL(/FastVideo\/inference\/offloading/); + await expect(page).toHaveURL(/FastVideo\/inference\/offloading/, { timeout: 30000 }); }); test("the welcome cards reflow on mobile without horizontal scrolling", async ({ page }) => { @@ -153,7 +154,7 @@ test("the welcome cards reflow on mobile without horizontal scrolling", async ({ test("the model catalog filters by family and checkpoint name", async ({ page }) => { await page.goto("/FastVideo/fast-models"); const catalog = page.locator(".model-catalog"); - await expect(catalog.getByRole("status")).toHaveText("18 checkpoints"); + await expect(catalog.getByRole("status")).toHaveText("21 checkpoints"); await page.getByRole("button", { name: "FastMetal", exact: true }).click(); await expect(catalog.getByRole("status")).toHaveText("3 checkpoints"); await page.getByRole("searchbox", { name: "Search checkpoints", exact: true }).fill("14B"); @@ -161,7 +162,7 @@ test("the model catalog filters by family and checkpoint name", async ({ page }) await page.getByRole("searchbox").fill("no-such-model"); await expect(catalog.getByRole("status")).toHaveText("0 checkpoints"); await page.getByRole("button", { name: "Clear filters", exact: true }).click(); - await expect(catalog.getByRole("status")).toHaveText("18 checkpoints"); + await expect(catalog.getByRole("status")).toHaveText("21 checkpoints"); }); test("hardware guides are grouped below general inference tasks", async ({ page }) => { diff --git a/docs-site/tests/evidence/cookbook-hardware-catalog/README.md b/docs-site/tests/evidence/cookbook-hardware-catalog/README.md new file mode 100644 index 000000000..cd8170761 --- /dev/null +++ b/docs-site/tests/evidence/cookbook-hardware-catalog/README.md @@ -0,0 +1,17 @@ +# Hardware catalog picker evidence + +Captured 2026-10-19 against `blume build` + `blume preview` on Chromium 1248. +Third-party requests were aborted. The test fixture mirrors the generated +`capacity/hardware.json` shape (`{version, devices:[...]}`) with representative +NVIDIA and Apple entries; production data comes from Lane A's catalog. + +| State | Capture | +| -------------------------------------------------------------------- | ---------------------------- | +| Directory filtered to Apple MLX with a selected M4 Max target | `after/directory-m4-max.png` | +| Model page with a selected unified-memory device and its lookup open | `after/model-unified.png` | +| Lookup with no matching devices | `after/no-results.png` | +| Narrow lookup | `after/narrow-lookup.png` | +| Directory task filter (image to video) | `after/directory-task.png` | + +The captures show one selected target, separate dedicated/unified wording, and +one plain empty-state line. They are not memory-fit or performance evidence. diff --git a/docs-site/tests/evidence/cookbook-hardware-catalog/after/directory-m4-max.png b/docs-site/tests/evidence/cookbook-hardware-catalog/after/directory-m4-max.png new file mode 100644 index 0000000000000000000000000000000000000000..deb9cee00f0ab77319d2fcde0f9143370eca1a36 GIT binary patch literal 152097 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�B}Kb#Z--`!Vrd#QmzV`T3x_$C0$%;D1Wqu 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¨kUHrJ&XPhki{6bt-XQKHI?n7N~;1>-XB?>VL5;UGFBFitJ; zE1u2|{D*G+6G$O0Qnn(vZ^%x5H*cO&F}{VH0NpeOf&ZafNXDDdh9YApfpgiDcScN1 zj95^OG>>A^hH{Yfe@U3o-sS+MF;GZPsC*Ge>QHoERb=PQC50X8PUC`Uq~|opzwz?D zzyXt^h8SRBMA6`aGRb{@-25~bBwYmE0T4QnjZK@QcrI@>WHLGgjs-;C{?)BuP!dB> zUNR^RFnMJ8whd`XiypD)2B$B^puWC;xg-(hRsja#csU5`aTluhzPCnTd4UN$LM|Aa z_fR0jFeu%qZpE8Jd3gnY0@yh(e0Mb6RV+paiO2j6@D!e*L1HvlT-H5|P6&c;Z$Zx1 z1OE)6tvFbD9il``UtvJ6WMg9@y?5_>{)o-)ucT#V-2l`eK-qnKwSpvr@mdk=R8i2Y ztEiL%A2_2%@XLI6KpXlK%m89whe}_+=(@eiAUau22u%qX3sD;4p|}4`cA?b${Opa7 zlOyC;SToTj2#sARj4Aq-3&G_@I_bx-1q)?MkmB$gZM|OjPOt9>0TBcSRL0-ZQ!A6b zSKZnVOM8NoGLdC3b6^k~E$RRyydC^YeW_iSl~D#7t9LIkv^*7Bqo^y);%EOMpElkS zrWkN%&wyG5uXyo0`T}IzFTI7&S(AuX0s|;J4m?y*kU(o|H6O7HI)X$2!y;>>y!Tto z@xvU#7$*PyHuD18WM#XxPWsbv8iBU)^J0={Hs;Hr&Qxc~B#O?16=IHuNWlT-B9c_6 zvGZY65C>FLGT5KLLZ-b6kCe$$Q2ekiBOJ|p0iJOTgUS9r3R>F6@C=y5cP@xe2p zH*Olo-&}-qjhZDlYj;K@;WLXYy_-jv{Wswctg7lTrNUcTm=eQCB#Qz83Kc|NmJEcD z11*2EORA6;q3O!v%b-EM_B>05ozF>T`{ghWfYvmQt)Y5NCZiIx%NPZ5RC+Z~0|5_QIWFhelE%|EP1n zTpS|QckJ~@czOIzV$~g^wWSv+-qxYs@92bN%LJ=_Nk>EtKmV{-`~1Jlvm+b{m1e=F z@a;0*P8Pe_7vH`N>6cgLE3wLA>fB+7(t1WVNRr~M;iR|HSmN^9N881d?Gz3Nwbefb z^hmwQ&CAaClXQ7WTiL)DIZ1NEnQW|qo84K4@8nF0-XlMzyHWP|*D zx6nG+v9A5JzcuuRw)5l(l1T;TBU3ALYdii4uBz4Bpb8uW)6LxHzPTf_&eokDM|%VJ z%;cocZav4wvYGA0x0ee1cZ}A*S+mjFJFC{Vw!%EH(7cC#uy@sYhqNr!5A(v%;9*oU zAe8-O-xxBMKH&9r;Fr8OQ7{)#%Sa}4S*FBMwMl{h0QGyVTR{r!g=v@S)erudWv3j| z`IfMi{d-t@mVL9`20qi^{%BX8Ex9TnKmeLLyJiSA0O07pSWYyN?&tw9uYeHa0{$1q zeM@Wav^7~|9L#X)kAT08=_7foPb5s(>xV^q1uz>+Af zvCquH&RNljCy*}^@Mms$$GGy36==s1!4j`#)> zMj2Jirn`?m*ihVyk+jW6;_^BVT3>z5grb9i!VAns3>{? zYk)A9&GD%gb{Qa`pseaekqiv~pUPk`_P6Yqo-r*UQLaE-!>ma`?Mw24?b*=qX$Agg z#SS(noh*zETC3~o*2}K0QKi)JxYfrLP-oEXS81R+$6zy(b^_e{uKb;aLA&+o)11QxlcBU!_P?q7WftD)W>? z%1maNhmx7bL{@PyMGz3 z>-v5^Yn|&{=Q#W#d-|c|6h=} zk`U)tM^(9cKmJfYk*&SR1+iOPNDpIpwz1>(@L>N%xhwI^4^Qv?uy)Y=YTmw!HuM%l z>SvX|X3i_Q7+#JfTs3ZctxY8`@);|FF@Z3JaLd547J<|olL#csnCZ2{fTr2c;AGdt?L+cMlJ=&M_87q`lecub1Jn=PvcB1gv(@gkBhk*P~N< zq&tqpNS$pf3d`~fQM@>jRMgabl}|&5{-vk;#lCbQXWM5|-Q}E-@!MSF>m{lwRbN;+ zpZ>DwMgOhL%2`3tDmdZ$Q}w43beoUWOWm&}-4AQNB2dB0Ih_;X4(zGC^8hs;?FOna z0e~%}YNGLi6WoubGQ*Bvz35yXL-uk5E&<5e;G*m}^Ur!yLJcZ&suU;{W%tE>UUkq$ z5)=wVEWnVcezSpc04gxubxKATPYs+P67IjSfo{?vL>f{&v*1~q4Ws8utb~sVD9H9J zD=m)>HS2O0S~X>eRBjyQG4#F@V@~2>i*{XmHF8|tM)_&43gfvgqe(xTE$usnWQWsq z$3XnZVMTV!#bAvJ57zoiSPGhYlWxWBW!ED_Vn{}ilAWeIq! z39t8EZLX`Vm6OLZjo3?fJ1CA<*Nz^kW1yoO`tg&}e~fYLV6#6U4pbV2cSzQP2lxJl z@=x~=70e82%NDL!y4`W%5ARieY#Nm}7e7;{vG&aKSHa9^u%v29&5U5T$2|3c;EX=o zu+J?U9G7;PdqyagcqGxYS4;Jg4Gip?dDwnEse|TEvRGluMNRuF(v$h6pqFi}ZXjh{ zXtvn+F1+8jDk<{8`8&=TIUGF1?lZ(`Rzb7;xH{BvZEnr!fx-Cr(Z9tIiAbw6N3Sw{ z<@pyEa7$e8W$BMvX*TMkTD!+Tue{}UlDlHLarCt9_s81aW)vH|Nz2kP^WSEfcDH=> zJN@DmyPHIo@1MuZ|`nL7O|n+?Pm0!Q+Cn54O~+{P-A}DrmF8$0t%w&hK1sh6? zNA~`Cb7#>ASi2ke2*(zf9jfFYEd3dQ98MC7M+CPz1rQ+(K&PnU%D+}X%76-xcF(b@ z)pTOfB)$O}$&q{+r!>F1)0#uB`v9AM_9+(3`kpO=W%nuymECmck zLw`6OEOZE)Xa(k_ItJv}(W=F`;W>0d*RSWB+Zh#To4%keP<}{&Zy>ZN{rA1?+$}#fH1MlirM6f>roP1L z<~MKdW#%LwdY006oXNXB{Qb~>?Z+I#-hH6^*z9lLSd$VEaQS(pcQfUtt*jr5%y#qE zqQ8vcMnpX!3@&5BHs1|JQIYYxY94(kJ}_V=ci}<~%E8)TmhGG$Lw+ERBk2UWVOtTD zGCPl|ovq9O(FfZnqCbMn4UOn1+%>Sq$h%kl1NC4?fZ&c1O-oon92sOGbRiTb_KsS8vt^~J-#I)_} zlEU+mJeYfu-arg}U!mR3!UAcn?*1E!s<<}ys83pabbK2y{5C8dBq$U>MyEMBw|rn1 z6pYd$G@Om!Ra8;+EEb~(9@}pI{c&7VcohS~6ABQ{L5x3eNKh&645CQ&c?IJ)m+}xK zCSu@0^&7E1f-kY!kMY45dREDMo}OfF_*O|V;1*0&ANot;NiTE2SQ^3 zTg%|$Cf2RBs>zjtp?O5L?Cz8SD%dDc>O)*pWbJw zP2RUpro}n4Q%I~%Q^)MDg&oaoI=4~DZ7~(ptoR*Ii1{f%Pytf?wizt_oJ6w4)4sq? zOiv+EW6+?LISGf)9MdI+&_=FY0QI-n5wR~2iJ{nq$}zz?=z-v<2v_;cnJ#&!$CCL za_Fpt1W={3caaYYJWPj_GyhLw!j3&oSBXGLz@mf$Lgfs`IP~5;;GpA?h(=hAL=RLa zWELZN`5t%xbi)rFl!+193Iaj&7o(jA3{?T0R%1N4fC}n1z({LIeO29R*pD1(?0D%a z{uAWMPX%T^Vd1jdw=p;ZqvHio+C?6z)nmN>@;7t5?tNoaP4#ueI{WKH&q2IA{xiab zEa0cE^LQ#TRsLL=7-DAb7>?lin+i)@+ZMObsuJuCi8yJrEZ5AP;2DVN@B~_7!ptV; zg}9K{M)EaSL83+q6T~<(aNf^JrUzeM`l8b*gFjMIPh5b=a4tq&!=Y`&HUu|RWf{~U z1Cfc?7e%^8OK#&Z0?2u5VU21ze8V2J4B39ih&^1rrjeIW!~Rqt8hJQ0=IZqPY+ly= zp(==pFY)G^4ryUWiiZ4XRvGQRCUKX~{pb2*{Xs*Xf#EEX^zt@C@9g~P)}{-Ve-P1~ zZR6*cj`A*{QH4pxo6swB><*m80d&UrW=uRbcCJBuCk$N}IR&+GTgqE#op-qx5rjJGb}Pv18rh#>N?4ecKoalAictJ%R{_ zzv6z45j~E-2N2!|6DOYheLMJX8q|MjGU7k|+v&mIBaeSjM(8*wv43&e|NFfc*8hgT z>OUh>6gOEnVPPrgzEV;fk0SspQnS?iP-=kdfam6=%tJgp!EQ}I;ampR2STR5pF%YW zt}b5)yDAuvfj$y~QiD*Kp(Yh|`jN6|f{ru-+K-B5ROINTuXkJ2z414+nTI9W_ z$}OPVv0tWnEpmoP&(B;GJxy# z3-Sqb4~#(n-vVZm5o3T_ii-D^l>oj@y`uVg1I#iQ6Jd2CT@s`_+|2;l+`y=T&noc6^{xG)@9){X-WX zeu^bQK`r58a2;)JR0&L|P~iNC9`U+$uuw2VG=-gWECfD(iK|e-b0iH_z)r$M2dVf~ zZSAa)l~q0iCrHCwL@Z*kK1|_^f2$SP_3itm;W~Bu7Qo)e;~1t~o+8+e<2+M1eqiml zi$lX|6e%GoBSVZ*QO9%O(^Jo&2fsb183ip+SEhdCF zhqSlf-hLm>Qs&_yoDi{nd`BbM4;)b3Yv`p8YK!mm)_rv_2g&Dme@#{%{8Dq)s~Xycki0ZPj!%3V z?fLZ~e;{JD0&Z6_&>xNo6L}2bXpdS26JE#135SV}aI7YzpvN%iRmtPv=WKWt8klXs ztUdh2F9=K3@2wJB6Wux>5E-lmXCZ5SW`k`$vdA^f>at1TlGH9ynxyBxQ zUd%cRaYU?iq+Mh>y)kzX2@`A`dmI6UU{9j3Z{KSq?3Zs~=;HnAEk##QQ2X_(5$NZL z3(YuE0+&3*3?M4%D{b3VjLA`{UBT7kuHDUiyZc{Uz%1iW1(2*Bgwh0b8oDL^x>&9F z4EpZ>IHB;TI(BSZ&==THy296Z+xk|4Mv3w1JHPR1`0*{a=|H_jhwS(7+xNBh3jlx1 zp3b#!|68Hd`%}-6UdDrvw?FEET5arLpwXV~$EQDU<8+IEO;{zP6n^R}#u`Z_&WDj& zO2(kY0%(#B$;VoSUcE!Q`lx8|5xgjCcT`y8zJk_ni4`nBE$4R|kdtWlnG6BqjBQ5~ zPAdErCFUUgC~o!m9jVh}RlWk>VYF%u#Wv%OzvM(H!#Sf{w*__H9J=r`N&6w)K`DmIymzd&3+&M# zOgJ#EcG4r-w~Yp`I*B=XH_W(W-J&|||ApC~MQ)aguu^BH#wbBrESeD?bTq>=KOrnU z*`2h-zgfXHR=~R2+-h(5?$fI{YY&(LS5dfLC!Je^XO{7ev!pwr$G%*i<|ntzJu%@8 zcaGYA=?<}Pzs8b7fH6Vucwc>jnaPHC9Zk;9iHM3U6LfVB>mJio_b#J}N;~)k#6Z$_ z!0^k#>`GsKa~q=t&|bb$Y{9~W@Lv-!6_qf!BB}MCG^80* z_G=zXLQMewCB7&)kYh>oP1u~g*p_C?Kr8hhHG%ffZDg?%W3bNRY?Fo?+iafUuc1bi zYiM3HZl7I|LFN)9Rx&R}942U!cXTq)T&v5<`hCa-Ii)YQDN+BxeWc8i`0J>Aw+a06 z-#NG;D*5EMpH!myX#7+Aq*`d-w}dgqR}aEzG?avl*Sd`ElN&;g-jRb$J)c3d067s2 zazuei+#-hu2d{8IlSYI2CTy3z*1a|z*SZt~U`R5K|Q#Lr^ zue*mgid6rIBEn?!?Bj8dkEn5|-c$fYFR?XmwEl@3{#UrNEh=ozLuwfJ)F_ZoziwMN zRZ0MIVb6fIInV|+Q&+Vb!|@k)QSVL-g>CxK@pxVSaP;mU*%7X8HQbRB;T%d{iOryj zh~B*PHG*l1A32tgyH}?W%#Pr30h75E?iWJ0^`HGz3KbgA$vCLSa{wBOdkJ2Zv$7CCox)X#b3D4{% zI!q7=9$4;m`*BocOH$x)tK6^M|0IJS`FZ<_4>Tmctj?S14FPLn+V1S{qop2 zl&xZYI9GN+WK$Z`+_gKI1C1W8@mns7V&EI`_U+rZ&HJ-=(RN?emiwof>N141-Tirk zo_!L`B^~SZhA~KM^_WS9kEw(5gLjnPPy~1&!;mRq8Ww^Xd$~vSAMX^qIe=)qlhg4D z-%nw0uDO55^?tU*rctH|~b zzGQPWi*_Q3dk}`J6>6_4pnKVWKVW{~uy7im=~)NyEqktmHeNk$qAOOCQk=+i+@q~i zGx)u7W8}5C!F1?y!e~Us_x~xQM%a^gF&Ow=^VsrwqYJkLG;#`=8yr4VvmAY~>{ZJz zNmG%N8QNz1?X3PyqWBc-XB%H5b-(!bi(}#y=}VYA)tzlZclurE$FBs~r8+0EZrUy$``Hj- zL($o&sFp@UqtV$%Pg{fwpjt%ij2VVB3A+yn(}~7-v%Y-2+h-&x*EJbUBQ5R!=buDm z-!9ki9l-cE@jRD0VDj_QX?uV?0W0BT7}UlLm(ntrq&M9}hbcgS-=8AfUo}gWI3Z9< zK331|0ecm0!Z>c&zwtV1hhh9EOe&*2JBTnZ&kl$Gxgk*`Z z4Bq*V^rz0n?f32bFvLyIxNd6A-bM;!@Jkh zR=pYP`{(YL{lsseD$XGOhm5Ru|EJ&oOZH5rYTfU@nTL3i|Ncui>$*+9|B^4Vp7!@& zuDWbs{{5G#pPL1K|HX4^yVdW%{J;7^<2!cmHX?pZ$YD!zF?D)2Hc{+SvK7Gl1fzm3 zq6VZefiG*AN~-uz~-z{HPIqC#wvP(bUWUU-5$$h=@h@fG(DV{b z8z@!3K^D;Yw2|_!8+$#-VF+ebM-z&=;iQ1`=;r38l6!Ar7nn1&eO`iYLBGG~QZ!%* z4A#uPLX)>P|JXw0@foEkTq(edF$I&dAx0L)^dE+Xj^iWn43w-|r*BUfzCgYY z5eI4{)O&=Op2uF@4_}M=^E=?pPnOE{+OoICgVu4SoCeOFQIk;%avA_HFC8uZf|Ll zrCX^xZDqseg(Ra1EG( zA===&&w;kr$z{xpYCj{|*r-Ji_Y9wr&>)MJKtL;GQSOuMVSudr+%V$gdu4QQ5@ZR@ zxpOU;On~M}Rh6wbcCqtoA@WI4%LpZllrb7woLkiZqi}i9TOD;tTytc$v4(DIB3PRG zYlXSLFZ{j4Y0?P+n2n^Jse+P5yWYM>aad@5SAH8M4WZ2zy6KR2{bvH7oWGA+M!jL8 zeTTb@9lYm9F3>temo(dXMs?jDW<|Mf%wiA#Z?ED3 zy^ELO0*`(G$7{#&)JSw(XljQk0=7E;{F)h3AXI$#UHUhWFKW6=xJ_Nu_}!aEQ)6Z| zJeD)lAGLi94?Kd2SSR1=?HhRlq++?!g@B5SCJeWNO1Act3)jt1ykb4S0s=j+K3X!) zb;=0iOlG-h(TR>{Yqau&rVNR>kHQuVqNFsuQF=b8oFvrqj#I#j^L>td{F&S*J(4|p z@wwY%CfF;iJl6L`U!8+yu|aoD87fkMsSIyS2}e4V25$b__uGCX-r)%xvi4_MEby~ zFNO2~WI31VyB$G%#yM-33zh#|Hr}CK^z){~I&k#bwAd+mq3fBjv8)V!XYY99kpchI zhXR>Zt~hJ^ytHJixRK66q4;r|Q_Cz$3M1bkN-f8(hAj3&36y;b&yxPIJe{limuxW*YA;hRt zi`DtW8t{F+XuxjU_K8>SR9pf4-RMQ7kg8XFyIF5P~Z=omlKKlOwBMc@H(J~^9%YVnTyYpzbU8#E%jL%#`*;-*Gf0+CmGP^@KJt3-O`t@xtk* zYWg-rqihRLr)Z_SDh4bbW@jfKS;f6x{aVlh%0Ky0h)xzckeBm(YloW3_p6qsnrtF&j!n@3Yv3dZI;b|+0g6DS(n60Btf4qX)(|psIK$14Kx$5 zxZ;H6{;YxA1?~`8rXmnt01+Fa}RPvHz7fh}N2bMz(h8)1`Q{wAwre$0ks{YahB7 zM;;8B#MD5gu91L!2}aV7NFC_<=V`N$R&hnlq4P~^jo`dhRqhh*w#`k4$B(IOW%0-k zcJk7oT7rC`bFwwo-t!EWfq@ErIC?TPYHUDkG?g?H;xV?${zbL{*<2@Ai+|n{jVzhj z{b|z{=H@wPcqf)7P8feY%5jSSE*Y>xeaWPzAsTKu#5^tXq@^|nR9(XOD5AZd7~ zmC|z`MY`G6wDQwIlY)y|W`E&_sjzOW+2ztj`>>{H9?VveL`_&MRk9icPo zTkBJ!g_MY{T{F$$4N9Z6!*5|#n$*$}gM_Sfi!I`AU63JNpV&2Vr<9j=bd&U6_P7;|-5tWD$N36znr`8R@8-ni zN14OV_QTx|Qu4spnP;ZY0 zVv3fYNUafO8ss)L8tFZB3R)VRFflunkm~E}3zzB{d{|ai7ETr84W&xGaR>Fr zB4^#khn$_KVTD7*8hrWZVP4BsrsS^2wHC%w-uAW$rIR~@V zlG~<-t6RmNooBIUwX@6F*4AkYut7L z?=rRj#ZCU6LwhVtws++_g@l}pUQ~`-*?Q(zm6f(cB6TtP2-8NXsa!Dx*+%z*gYhI4 z|EZ}La~|0jQ=-tW9gw|FH5rtBxj1h*dM^ttJX|PTU}J-a`*iSiVpLlt{%pbCO0{ewpks` zyXrqu1t0FV7Pezp+K%JuS+3tf&+PMMg4!Et{%h-7iP3^M&g>~P%1}oMZOIIqYEHKM zsc_u)>6UECQ4RDA3@Ml=?r2snnqE=JtFbd+m*dO%el|V%%7Y9cG-Vv}lJ!O}e-1U# zpSIhd+}Ec-o0|S@Y9(XyLdO<|*%wTlwWe(MdHasVK60Em(z*9bVhWRVfN{;xmru$M z*X(L!uaj%db~LvR=ZP&eN?ktng}758;nYqPzMKLASRzx2V$C9}^Rccj!z&ocDyu_r zH-HA=mv1QuUs)z+TICbpVilY`&JH?pxW1YyoE(?HXo9@854J{ulXcG~QR~43#++4I zt~M#oKFOn&F@*bI($_${-<3RXIdz#=jg@vBCdbZ3$_$h$nkzvjgtPx*MRpLnkWPNW z)WTK|WZ}F<=$(-dATdFVj>MBzj1(0W0YGwnjp4C%A+#0*f5dGtjzou!Xm?!7SRV|B zNZjHm2?sJ>9^zQ8NR!#L4^<~%=eGcp&}WTv4!J&Y!d;zfHO5fKr! zZm0wctWZ6JNV_O6Zvihr_%RL&&*IVInYloI&N78abMb*$r zvL<_vPo{;!-H8_LP;IEk9Ninp0yVvH+X=wfa|sGu@Q?IIG+WIJ`b)6;h85YR$mhur2L^|Pu<;q+6Cx2BO@!1&VmTx3uL>?}r!GGg z3hC^%MRc>?qh81J%dD~7#VTYcTdr%c0kydD)(_E(pgOR}DY?0e!%CL$YGcjha76)!+;~WR zKhfSo|lWJ&u;^kyM$EkCl!k^$#K3RrZu@O^C}K!H_0i_z2Wp#*3 z?gng(nE2~F(BI#W36ZV_T$!vUb^h|XxjAZ^V=q4POlj1p5*KndX)F6tJm{&e+!!DJ z8nm8O?BO*MWwVo0T+uAT;$e`m_2)a!9`a}5QkyDc!Oy-O>>K5Tl%Lfe$-MI7nUnP$ z^-B$H2RMt3(22_(hSb577B0~9%#GDh*MY7yrrHhl34)gTFc6E$X|DT__GaDRcroL3 zW+r3m#jFdj%*(KI!hKPyG-Q?%W{fd7lTGid54r>W<|(I zC%aAW-=n>5(>;4{Utg?qVBd&1QY;KK|ULMRhm(wN9=sPv>_5+{DUQ-cpJVy_+V9ozDP!Qe5*hn0Tg6Q8iIK)b|Da03Y(O0rt5P}SmeTQ~3Ra;E6=yGb2D zm{#ADB5Z&rB!~jl`$^1lPIuPvErJEvMs`k8TvhE-c4noV1YmNZ6Ms%)bs3%1q&;+r zxS7%XO;4KX&KcXxuBXWj)j`dn-o@NH0g-E;tC31YNON!fzfPDZdEIkRPMw$4TRyt- z+?Mai9*s4=-Gpvl~`^DAnOQ$74AjwrHq%WYd)_*MyO5d4+tjsg|8$u4d) zaa2Jl#!ii94nnq?sW^K)o?(W}SR4`gxNm0`So(;ECf+?JdC?4aF$|Rv3a47$_Xu6? zH?n%$FCj@lQDHMbp~lXL4Fh3GNoZhL_C22mX+ld&OSp3Ot+WQ7$c?Pt9%+=US=a;_ zz7A+5Skspu0eWuRRvu!=d|jvEx_$n zm;?^;u9UafZsdq>M}CVA!-hlUFl^Q)yfezesznSqSXo)Id!dl^fu1s7@}BC&=M<+9 zZ_)GPG}tuJovqfhxrc5dK6~q9ZRw_V-J?g3+P(&j%DiI-oG8{6Ss|xMFO^s-;PWZ1 zhDe@H&-kEemNNNk%6jb%;o&2U$KpSSHuLvAHMncTa3c?w5Z%f;r>`gyUESPBE2E+d zwymD`sj}Il6~Em*`v6K|&B)>uS2oKR%J3RdGO*RU0S?rduuR6-gzV_@Mg*LcO~0eP zP$PPv(P!MAdu^GQ$KlJd{bh0`E-0)MTsQi0yxq1VbL@k6$PY1 z`Dlb_iL|F(-Z3^5< zxRwE?T69_>=m#M$wY^AYkM^EMPH&JCv{Wv76AI|_>E3uySt<5bCQ?SegDo&%xpv=*PJHNGXnx;VImhwL6ZvNeFBq#B0Orq4D_w` z7RV2JF%O7ekkX4R`g)Fp1JJDfXgEqK34aqn#4AV}^ubK0@^D#z#VM(%TwrL(*Nb^1 z6I#%1C~g$ce`zvlj~+#1cNdTL9t0sex(>V=gq}Tha&QRB#}6VbGqDEe_gmE0Ss*bm zgJU5Wex8}1KZ#xIO|66IfA&} zYCuEB>2);W+eyhu)N5FT(NqSR4@6!FO##8dC*!Y1cL{f5fRIeC86rx*pr<~-*uh_WrH+1Vrd!pa)d6d zPR{E-JNslp^VII4#;EpzfdS^`utOnhZ{ECl9+m#|KAt5EQT0F%61nh@xcVO=2+swl zfvBd)$f7#%j$K_{LA<;E{CX+!2Pg}?YkomNK^`7?``F2tf4tj#fvEBHzC-7a@aFFCms+IlPbd%#19XZQ=;`Mni=1S3#}x0aBhr>jKx4_IFnn$VCr{>iVPZ}6b}|4Y zXr>ZW0BSpS?fMCF88W2FBhGkK5)u-*N{s6MgLRxKIFVp~Son-vL7w$#Q+#f05|(Pp z!09DcVnbhsL^zEe4#m?QsZ6@AGq1rO$iSi0R+>2NMEzTtL_A&M=HNV`0m7T?OU(0t z5yH`6?1lUYw+v@43o$-`2|$(?BG3r1Kx1gY%AF#L#-%wEG(>4qZ2e7|Y(b164IGb` z%`$|}?y^J4-n&?)5|SV>2lU7#l^+x>7x>sZ^=Ko8xSw&qXTPH z+Ua%r;)zBzUU%t_Pe0(@xY6Uy?bKhq2F0Yr!l8At_6F8$eP4dp<562=`o+5D6(7lk zO`JV3Icx1W4Yy{p?^Pd3msQESUrUsFa&>wecAbA)B1tF(9Iuvm{aX!z1>_(9?Aupe z$BW4yoF+Yd6InhzD=+0I61t|M$B#cjkA9>9dOV0Z65%aE-~b`J47Dk=L17jSR=zA$ zj~MpD90Hc+(6~4|6VBp@$kT@eJ=R=-lko}X+{acPqA!hy_a?3zTVNG^&F0va-JrAu+qdZhIdQDtibTSto3ODdfRy+H;%XfU~g(Ced4h zVbi1E@~DrRA5>2a4mGCzj&pJnIw}ERK7gQzd}7m0=gD8o%edqRJ3kN!#<9wg0-x2F z@$t;&6?{Z|uE5u5zu_b120wzp#y&BHzL^Ik-*Q5YODGnpsHjxV;g@5cPVWK7HY`ih zz(BY#l;FcuZ`_iY6Jfl72zV2qmxvxWon2i1jsj^pW4ND~AB6K5@i?$ayTwC24tDIn z78f&*Lk8=DyMzI-7chs;mPbH^QY`lt!Y}quJO%KUKpI1cd_!ZLK{ z5r_kO!TW>fo|&J=68KZoJDV;oHsEcfRhs(Cg&X4mzG`SV-|*x6_f}77&Ug6-kNct@ zgUTqQ>MRY_=FNn=AVOG<5T!NbDw&j8y5205c8xX>#y$WQ@1Jv2%NEgC7*gSNep&~a z&UaC$uCr-9Aj`lZ&Aoi$0Ky?mssgEVw*7;!2x6P?g_PT%dM&8aQ-^Wk_5)TRr+%^> zd-imr>q_%5WN+tL%hRPX>Y1zc*5MW$UDodIzC};pc)UMlz{G3(Co*h=*oP#+vWAQS z8to~B-w>qd*LOZ40uinZe;Kr;1+A`QKF^o%?)`&;)Z8LCI2CtUf!;xH)^x`g_trKP z2dvw+B^a1&N9xylg>z0|W&uM$Ri7_%G#j@yt8m&WZpQu81P_Og6;?wTicRfVf*RSE z3Px~406{cx`m&*>@xmrJtUm4ZYCNx{{OskbIT6ByW&aa?T4r)MFG5mnC@(9cvjP)! zcGao84960<8Ovb^(};Wb?twnViNCTag>2c`qEV7iPavFy3IyE4gi{_7V!w93D|d0Y zc;9BN*&(#Vl6d$LiVsUApkjj@EoY<2TEGEl;CEs3FMu$PSX10-VwE3Uf`LGK3XDSR zW&9sM{)SM`r-t0zSq2Z!?3u%=HL;JE6%C)DLxGponV}?cl;6mk5tS*jV+%2ejT;2t zDGs4`7qoj4Y=-xmG@ZbZ)z-Xyprww+K^sHzOmpa^aH3?@S_6%*$NlbPB`lws7JjY%*8LJ2IW@vO*yA zy(b1c_IaKTdwMAT(fwt1ltR;7B`Hii><&Hos%+mwpVBZCSK#Z&+Wb*C{{HiSAsf4A zU%JSBe^XhjZ`=J%dkgY*@DNm%XsGA>^8wa-&8SkVs@gPI?uBY8Q`2$SK z%#71j4F+Rzt=L_r!e1WB&hm;q|O>4~W;0zrUG>h)4fP z!*<92%jmzjfdB9GY_1nch2gvGYH|8AzU{DWtkhucrfbn+{-a-gH3al@!0 zy-tbMOJ*hp29qFV629vR>Wx#nSRm;N(@q#}QSd@+DJdpah54@B`-D&r`4LJjbZo{>4r_Wd{)KbrY!naPWIZiidg|J*)XFy|JAr%N-n0lx6_ zkyl)hwb$0x2()%Dl(36KV+CSG0iGz_Y6XaW_y~z2Uaj#}BqIp3;aGalD+KRy(g6RV z>AVeAR1!AGrdLsi043X1$fpCY6T%5AtM7Q%5vKOQFqi|5Zb4dZq{MiC)JSqQFi+I^ zQ;``Is*ntVamUiXjJ2Kaq*aH4HSEzqbWUSHQ@zJY+~nMa3#!tF#CciR3i_@SvE@1} zCyY?!G+KqA2B88Blo3o3`-#I zDsMjrXbLAMy3Cc;Zyxc-ultM?AZe5rKFGNG8S`sPmLDoTJU}TR)MV~Z`vp%GuwTzn z!{PTXBKek0Ih%08lJ;iBI!e5)5}AGq&zxKk+bG+*GM7=H8Z|t)IQp~ieTZ%Ar6n+_ z*mf`=Car9a*u4N0Edu!-6)d!42Ze7!wdmaNOvn+S0;_tUN(U>XxRSDEd4BQ~gdE} zIL280`mWb`6nxBsR3$uSm241#mR+iKNxzPCs!`F>T0onO&e>ZV>X=?TC)sESc~!SN*#zDxok7}lMi zpXX=0om5#*#n%cH0bU?u1zuIE?&u=9zTSmreKRWe@I%|y0RXue@JT<;dw6(k)C8oo zh#ndrH(8^9y)WAT2W*fe&dtOwe{bl*o*VuWfJ)jFui3-%+(nLI@2pQ0%dg6inQ09eFW3LpR; zOoX;DTn!*cvyYm9g@uaE3`kHeI=Bf1OvIxRj(=$pp1t7cSOSXA5QVEyiZsm&Tr6BN zoRAy8KY4PHdB^OYSKurOwdVyFemM6GyP%tyQO$eV(6BSMzz$RXeYWJ4&mi03Ix+Pu zWU8th`_0C%3i;$b^yb>q2o}>2*dVuO50>n$&QZiSqYE?fI6ht%5C99%UV{sG9#z@j zj`vxKxsUq%x%bqkp{&q=?FY)0*If8!+q}mcIcZgF67QIyVFZdlTKlcv+D_Eh^C9|u z@t^zAtGlLil8AmEce9Q=eH8JO`IG!tL*Y{Yg%u`Eu%PWNTX>aH#RX<wqemH4I6Yju^cmGhHU0{liZt20I6mAIQpv^GbQ2Z`IR{B@J09Oynk`= z0=fJjdJ1KX=iFi4iWOqAp7`)WSOE>ioDmVT!5R{E)AzAK4OQZw{%i8ezxhGk4hOg-`8SO-b=ElDJ8!g39&H#!D{a(GV?~1#BRdmb@#)k z7nhcZg5Q&T-KMM8E6$vm*}zi%6&v`~3<(hsMU5ljWD*dG+f1?C#l38x3b? zPk1(R9XzQ%F8ySmx3I{TG2FrLqMHRgFYH5A))ui@mo2_*ofGxm6VDl)xUqDA<5+8Z zdkuaO+1hTz{Y>r3?QWCmf$|p1hbO+zbg@0E-n2M!(jOZDD-^WPg_{y%vLmc+fn_(WuE ztSk3~G>Rc@SM6)p?(2ErR3o}poT+fK8AUZ-l5BF~WmidLXUk^Ro-`ti5i&QSB}2)f zB2VE`x3Y3K*D{om)XV3I%M+sDr{EX(ob} z_9B*I7W*6O!g@s~mVyf37%t@wy3e0r{3D0ioBXQwkfV!1V*IzcnV-Qych8%drwuSp zYltTQ9oFHM({_ywQP@Q(fW8`?&!%OF#VtBA-a({mGunK%^0Zk`iS>1e3O>Fk_9XyHj0_Iy>8k;Rba&$iMChN6 z#NrH{w|TbbG&MEt>(GZ&6Z%SFc+wi(gK)pR_3nIGO=p!w`%%)v1BKVH|!wKb#FQY({ z6PnHW5$GLYQKo`#<~x!ZssP69ThKWf8vvF~7#8M_*|^5`Dq&PQ6WdoSFNhpahys?V)D;fF#Lc|qp(*tfs zAEwhf+=sBeVP=kP-U7UQ3Qj)gufVFT^T;Bk?wBc_gY2|DpMK*utWQNBgLnE8@-3R> zJ3w~nOXIrbKfxxbOFn3AhCe9-#0*Sq>-UG2xj&$bW3@1d4a}*@SQOZtW{L zYbYUjV)Znjf(WaN8}Z=T7C;W7`)gR*WdJ&~M-CrO+jD%3RR+~iqK@Cy$1m!! z1D>|*eU{5uhR>Yng-L1Q8XUx@lyWju-4r6xkuJr3RW@gwu+%Od#?$=bW-oYnxXUo zcssPlg3NoItj+Rnysj%&AEhI-;tzJIKGlBeL=$vH+WKldMM#VNX25Z|SrDw&JSy9gky?P1D7u44v{Nl)brjVq8hW!{FB)(K_w)!H0UVv-a|sOEyRcqVTxJ z(s`UJLeaQ@qo>szzx)hF81aFWUdSLU1Ua$ac!FcsyF!rb8efwlDLJ8Y&s@iTdPMHh zUG;)1Kd89jwNvIyMO|yM$t?yepPGoczzD|({k2Tl9k1%mr$&3;_P)_k-(1X9=+tlPFNMY@onUw>Xm?%W)0fjp_EQh$S~} zQLB%UU9k4?X>!R! z(U?^uI+@c3a;syDM!dFfxW!1CttHs2&Y66}Y}@N2-i-*H?YB}F%amC-9)bSC%*3+kLX;P|K3Od`h4-%Akwe)euQ##B5oTtngZW#k zqUgxS_g*sERpgIR>y)Wu#3gQH`YB$ut~=)WKtNPcLwoRJ6wUz|4G~=Fo5i9EpD)RA z5Yrl_ay+HZv_*q`sm@ z@Q%8D=}bN^v3h)x>Rgb9ukMTYs7X_)Mmn452O4It)Bz2NUfAmp?-CKKmi)vj7&-H>E&-*}~!ksE>+s0)!gi3!?! z6tn3H1OX$E7gWT0EWO>nxmIbU@8yV~@WR^KLM6Hi254EqNA%cvJF?8{)fmK!J{lrq zoPZ=8BJ#5vz4g4!VsHKA8yL36I4Ko0FZPvK*Bp6w>xtIbN^A$32n2v2*J_$;RPmh@ zQYT@kl+ISu8%b07jy{E08^}4r*h!e*xO)RiPRpJ?IyiE}wnc6O-OWA0EN(C(R!#Dh z$SjcS2zq};gp>2!KVDwcsjN_6_zF4!hp3hyi6BN}<*5Hy-yfG6RdIjd=c<~n0fkY*9dCoao``g+|*dhNQQsw(4U7r9t|y50=%E4R3c2o^os7DDp#torZXAp*47dV5bmoA$8w z%|Y*t=;L&K>BYRS<(W=-JDwXzglOqaU?}8AFmOzE+RXQ{WApG(a0F~=n#{ZiF6YL( zB=6t}T?C+V4B?gs*VrGk6sc)02SLsA*iz6 zdY?s0h~tamVS-%>7fs0$kQTfh7`i}e4T z8{O~i8@T>ix~GJjyL}d}snG+D5)qprzeOf?=S+RAa)+q|n%b67%@^FmUAU_18HwTl zRR3eJBQY*|BqzG-sQHWZxgoGvo+AVo2fdYgw%P`AWNT=ziMWAoRhqmU;Q z?M-DO7BZ?nb``&|3<$NASs;Ps`OPB3QbQf!UqWefQOyaRXphxNU4HYFko%$debJ&j zlj&C$DoMGQQgfc+Fe48Gh!dmOpr#QM&Rseba=T)sFf@YiQgLI6Vs8a$ZF#QvHzeP( zb?X4kajtA)2EZBsC<<<}8_KNd14m4a3Ep9Jn%l*TXph({W>pHYjQxo5+}a1a9`X^! zJ$p)^9KfTQbX?cosfM$V3-J_rvL!iidAJ&Qr6OdUn4E?k5{ADd*KFR;P9it z2xTsAYF>Ga&9xIgR*#9X(dIan2Vcj>F~Cd0J{*Ao<}vLlgq7gCciVTrfk;G6#T#jK z2zrzg7}DV%0Vc@jxreU708C~{v+*S3t$FsaFdGGH+@u6+tSCX-H1&(i1_mu!|6{VR z#nbawdrVZyx(95!Z2k3%DcCP1_UuE@S;DIpggDLicC3pE|7#083;zM<)z7G}ouv=# zVEqGL*WDv!>UI3N)s}5{uO64nynBvCVRGOs0a;mD;GW!14qR_pKxZRxdz*G{Savny zdAiQ-$f}uu57U7_w@DrIAOi>slZ$Nft#1Y;}wC)@barWW&bkoK7VxQ%9ngM;D~^a!k3(g zf{jY$CMSUv^DW?^WOF4iRqh5Z4_tM9?d05ZqDjfgrCDC?ldK|B@5}~f&zBD_KULz8 ztpQG5fvylNFMskZZR-1^>>U9Di?_}*$qWM8QYxo+HMr#a3*dfsE5+x3G$;@N|JeU+W=r#?;T<>cl43tUh;ujy zv~e780lnL`WV>H)foampyS)6metHtHGY(YCa6*IsV_R#xs&jr|Mnb}q@BI1afi@J;)!*+QQ?qAcYDQ}9<$tMrefFE)SzqV&7&@*j z>3a8WON&kkt2=PVjsGp%KQMVN@R1}AZYP0*AfKY9fb=te`mx;9>~cQu_to<+LJugB zJbL)BvO)fVrP_jSVhgVFoSJ^!xAn|k)4bikBzqg6BQr(MPRvuT-qt;Roy5#rOHT`0 z0%K>Q?e5=MGfT6V6n{0I;tpKh44M%EPDOX^tJHp!l_K{)1~jI$&yN4&cC&Yjj~oFG zKmXVN6crM%Z_ms3_tN{;7G7UH-x2Bx{-#~?uP@3wa`gXyzm30+wf)tc@!{`(!B&5V zch+?uT-0tzec$v;5;zPEvV-A<-1!G!#(*Bm0(8Of8Ble!Y4HVmND8z^0LvjZf9-*i l|Nqa3s?-27*g&e78E)p;T-p-3F#ssS;OXk;vd$@?2>?`2D02V+ literal 0 HcmV?d00001 diff --git a/docs-site/tests/evidence/cookbook-hardware-catalog/after/directory-task.png b/docs-site/tests/evidence/cookbook-hardware-catalog/after/directory-task.png new file mode 100644 index 0000000000000000000000000000000000000000..98aa4a3188a7f6bacf3ed90235cfb30cb13b3e4b GIT binary patch literal 113769 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

    5A#EO#!WyAlqftvXGT(nZcb0N3+HX5gKNm7xi*^ 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
    8r5gLHIT$jEeH1`P)A+yM=_0^qyE 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 literal 0 HcmV?d00001 diff --git a/docs-site/tests/evidence/cookbook-hardware-catalog/after/model-unified.png b/docs-site/tests/evidence/cookbook-hardware-catalog/after/model-unified.png new file mode 100644 index 0000000000000000000000000000000000000000..24c80e5b0b0ea7ed1e4ad9e5d2f700d40ca4bacd GIT binary patch literal 139142 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

    tRDlH9P$6h|Ufv?6Bv!3KPU{+&kKLeU>9 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⪙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

    |X77-v2JPeVJRU(TaYuQss)U3w#U|gLM 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 literal 0 HcmV?d00001 diff --git a/docs-site/tests/fixtures/hardware.json b/docs-site/tests/fixtures/hardware.json new file mode 100644 index 000000000..2d1e686b7 --- /dev/null +++ b/docs-site/tests/fixtures/hardware.json @@ -0,0 +1,214 @@ +{ + "version": 1, + "devices": [ + { + "id": "h100-sxm-80gb", + "name": "NVIDIA H100 SXM 80 GB", + "vendor": "nvidia", + "kind": "gpu", + "family": "nvidia-h100", + "variant": "SXM 80 GB", + "form_factor": "sxm", + "memory": { + "type": "dedicated", + "nominal": { + "value": 80, + "unit": "GB" + } + }, + "backend": "cuda", + "gpu_count": 1, + "partitionable": true, + "partitions": [ + { + "id": "1g.10gb", + "label": "1g.10gb", + "memory": { + "value": 10, + "unit": "GB" + } + }, + { + "id": "3g.40gb", + "label": "3g.40gb", + "memory": { + "value": 40, + "unit": "GB" + } + } + ], + "aliases": ["h100"], + "supported_dtypes": ["fp32", "fp16", "bf16", "fp8"], + "sources": [ + { + "url": "https://www.nvidia.com/en-us/data-center/h100/", + "checked": "2026-10-19" + } + ], + "memory_gb": 80 + }, + { + "id": "geforce-rtx-5090-32gb", + "name": "NVIDIA GeForce RTX 5090 32 GB (desktop)", + "vendor": "nvidia", + "kind": "gpu", + "family": "geforce-rtx-50", + "variant": "5090 desktop", + "form_factor": "desktop", + "memory": { + "type": "dedicated", + "nominal": { + "value": 32, + "unit": "GB" + } + }, + "backend": "cuda", + "gpu_count": 1, + "aliases": ["rtx 5090", "5090"], + "supported_dtypes": ["fp32", "fp16", "bf16"], + "sources": [ + { + "url": "https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/", + "checked": "2026-10-19" + } + ], + "memory_gb": 32 + }, + { + "id": "rtx-pro-6000-blackwell-workstation-96gb", + "name": "NVIDIA RTX PRO 6000 Blackwell Workstation Edition 96 GB", + "vendor": "nvidia", + "kind": "gpu", + "family": "rtx-pro-6000", + "variant": "Blackwell Workstation Edition", + "form_factor": "workstation", + "memory": { + "type": "dedicated", + "nominal": { + "value": 96, + "unit": "GB" + } + }, + "backend": "cuda", + "gpu_count": 1, + "aliases": ["pro 6000", "rtx pro 6000"], + "supported_dtypes": ["fp32", "fp16", "bf16"], + "sources": [ + { + "url": "https://www.nvidia.com/en-us/design-visualization/desktop-graphics/", + "checked": "2026-10-19" + } + ], + "memory_gb": 96 + }, + { + "id": "dgx-spark-gb10-128gb", + "name": "NVIDIA DGX Spark GB10 128 GB", + "vendor": "nvidia", + "kind": "system", + "family": "dgx-spark", + "variant": "GB10", + "form_factor": "desktop", + "memory": { + "type": "unified", + "nominal": { + "value": 128, + "unit": "GB" + } + }, + "backend": "cuda", + "gpu_count": 1, + "aliases": ["spark", "gb10"], + "supported_dtypes": ["fp32", "fp16", "bf16"], + "sources": [ + { + "url": "https://www.nvidia.com/en-us/products/workstations/dgx-spark/", + "checked": "2026-10-19" + } + ], + "memory_gb": 128 + }, + { + "id": "apple-m4-max-128gb", + "name": "Apple M4 Max 128 GB", + "vendor": "apple", + "kind": "soc", + "family": "apple-m4", + "variant": "M4 Max", + "form_factor": "laptop or desktop", + "memory": { + "type": "unified", + "nominal": { + "value": 128, + "unit": "GB" + } + }, + "backend": "metal", + "gpu_count": 1, + "aliases": ["m4 max", "macbook m4 max"], + "supported_dtypes": ["fp32", "fp16", "bf16"], + "sources": [ + { + "url": "https://www.apple.com/macbook-pro/specs/", + "checked": "2026-10-19" + } + ], + "memory_gb": 128, + "usable_memory_gb": 96 + }, + { + "id": "apple-m3-ultra-192gb", + "name": "Apple M3 Ultra 192 GB", + "vendor": "apple", + "kind": "soc", + "family": "apple-m3", + "variant": "M3 Ultra", + "form_factor": "desktop", + "memory": { + "type": "unified", + "nominal": { + "value": 192, + "unit": "GB" + } + }, + "backend": "metal", + "gpu_count": 1, + "aliases": ["m3 ultra", "mac studio m3 ultra"], + "supported_dtypes": ["fp32", "fp16", "bf16"], + "sources": [ + { + "url": "https://www.apple.com/mac-studio/specs/", + "checked": "2026-10-19" + } + ], + "memory_gb": 192 + }, + { + "id": "intel-arc-b580-12gb", + "name": "Intel Arc B580 12 GB", + "vendor": "intel", + "kind": "gpu", + "family": "arc-b", + "variant": "B580", + "form_factor": "desktop", + "memory": { + "type": "dedicated", + "nominal": { + "value": 12, + "unit": "GB" + } + }, + "backend": "sycl", + "gpu_count": 1, + "aliases": ["arc b580", "b580"], + "supported_dtypes": ["fp32", "fp16", "bf16"], + "sources": [ + { + "url": "https://www.intel.com/content/www/us/en/products/sku/241598/intel-arc-b580-graphics/specifications.html", + "checked": "2026-10-19" + } + ], + "memory_gb": 12 + } + ] +} diff --git a/docs-site/theme.css b/docs-site/theme.css index 57ea724e4..86a7093ce 100644 --- a/docs-site/theme.css +++ b/docs-site/theme.css @@ -523,42 +523,150 @@ background: var(--blume-muted); font-weight: 500; } +.recipe-filter-count { + margin-inline-start: 0.375rem; + color: var(--blume-muted-foreground); + font-size: 0.6875rem; + font-variant-numeric: tabular-nums; + font-weight: 400; +} .recipe-directory-subbar { display: flex; + flex-wrap: wrap; justify-content: space-between; - align-items: center; - gap: 1rem; - padding-block: 1.25rem 0.75rem; + align-items: flex-end; + gap: 0.75rem 1.5rem; + padding-block: 1.25rem 0.25rem; color: var(--blume-muted-foreground); font-size: 0.75rem; } -.recipe-directory-subbar select { +.recipe-directory-subbar p { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0.25rem 0.75rem; + margin: 0; +} +.recipe-directory-selects { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} +.recipe-directory-selects label { + display: flex; + flex-direction: column; + gap: 0.375rem; +} +.recipe-directory-selects label > span { + color: var(--blume-muted-foreground); + font-size: 0.6875rem; + font-weight: 500; + white-space: nowrap; +} +.recipe-directory-selects select { + min-inline-size: 9.5rem; max-inline-size: 12rem; + min-block-size: 2.25rem; background: var(--blume-background); - padding: 0.5rem; + padding: 0.375rem 0.5rem; border: 1px solid var(--recipe-line); border-radius: 0.375rem; + font-size: 0.75rem; +} +.recipe-clear { + color: var(--blume-foreground); + font-size: 0.75rem; + text-decoration: underline; + text-underline-offset: 0.2em; + cursor: pointer; +} +.recipe-section { + margin-block-start: 2rem; +} +.recipe-section-head { + margin-block-end: 1rem; +} +.recipe-section-head h2 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + letter-spacing: -0.01em; +} +.recipe-section-head p { + margin-block: 0.25rem 0; + color: var(--blume-muted-foreground); + font-size: 0.8125rem; +} +.recipe-family-group + .recipe-family-group { + margin-block-start: 1.75rem; +} +.recipe-family-head { + display: flex; + align-items: baseline; + gap: 0.625rem; + margin-block-end: 0.75rem; + padding-block-end: 0.5rem; + border-block-end: 1px solid var(--recipe-line); +} +.recipe-family-head h3 { + margin: 0; + font-size: 0.9375rem; + font-weight: 600; +} +.recipe-family-head span { + color: var(--blume-muted-foreground); + font-size: 0.6875rem; +} +.recipe-family-head a { + margin-inline-start: auto; + color: var(--blume-muted-foreground); + font-size: 0.75rem; + text-decoration: none; +} +.recipe-family-head a:hover { + color: var(--blume-foreground); } .recipe-card-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); + grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr)); gap: 1rem; } + +/* Curtain cards: identity at rest; hover or focus inverts the card and reveals the details. */ .recipe-card { - display: flex; - flex-direction: column; + display: block; + position: relative; min-inline-size: 0; - padding: 1.25rem; + overflow: hidden; border: 1px solid var(--recipe-line); border-radius: 0.75rem; + background: var(--blume-background); text-decoration: none; color: inherit; + isolation: isolate; +} +.recipe-card-reveal { + min-block-size: 15.5rem; + block-size: 100%; + gap: 0; +} +.recipe-family-group .recipe-card-reveal { + min-block-size: 13.5rem; +} +.recipe-card-body { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1.25rem 1.25rem 0.5rem; } .recipe-card-top { + position: relative; + z-index: 1; display: flex; align-items: center; gap: 0.625rem; - margin-block-end: 1.25rem; + margin-block-end: auto; + padding-block-end: 0.75rem; } .recipe-card-top img { inline-size: 2.5rem; @@ -568,29 +676,48 @@ background: white; outline: 1px solid oklch(0 0 0 / 0.1); } +.recipe-family-group .recipe-card-top img { + inline-size: 2.25rem; + block-size: 2.25rem; +} .recipe-card-kicker { + flex: 1; color: var(--blume-muted-foreground); font-size: 0.6875rem; - flex: 1; } .recipe-featured { border: 1px solid var(--recipe-line); - border-radius: 0.375rem; + border-radius: 999px; font-size: 0.625rem; - padding: 0.25rem 0.375rem; + padding: 0.125rem 0.5rem; } -.recipe-card h2 { +.recipe-card-title { margin: 0; - font-size: 0.9375rem; - line-height: 1.5; + font-size: 1rem; + line-height: 1.35; font-weight: 600; + letter-spacing: -0.01em; overflow-wrap: anywhere; } +.recipe-card-details { + display: flex; + flex-direction: column; + gap: 0.625rem; +} +.recipe-card-description { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + margin: 0; + color: var(--blume-muted-foreground); + font-size: 0.75rem; + line-height: 1.55; +} .recipe-tags { display: flex; flex-wrap: wrap; gap: 0.375rem; - margin-block: 0.75rem 1.25rem; } .recipe-tags span { font-size: 0.625rem; @@ -599,19 +726,78 @@ background: var(--recipe-soft); color: var(--blume-muted-foreground); } -.recipe-card footer { +.recipe-card-modes { + margin: 0; + color: var(--blume-foreground); + font-size: 0.6875rem; + font-weight: 500; +} +/* Open curtain: the card inverts (black in light mode, white in dark) and every text color switches with it. */ +.recipe-card-curtain { + z-index: 0; + mix-blend-mode: normal; + background: var(--blume-foreground); +} +.recipe-card-body > :not(.recipe-card-curtain), +.recipe-card-footer { + position: relative; + z-index: 1; +} +.recipe-card + :is(.recipe-card-kicker, .recipe-card-title, .recipe-card-description, .recipe-card-modes, .recipe-card-cta), +.recipe-card .recipe-card-footer span:first-child, +.recipe-card .recipe-tags span, +.recipe-card .recipe-featured { + transition: + color 0.2s ease-out, + background-color 0.2s ease-out, + border-color 0.2s ease-out; +} +.recipe-card-reveal[data-curtain-open] :is(.recipe-card-title, .recipe-card-modes, .recipe-card-cta) { + color: var(--blume-background); +} +.recipe-card-reveal[data-curtain-open] :is(.recipe-card-kicker, .recipe-card-description), +.recipe-card-reveal[data-curtain-open] .recipe-card-footer span:first-child { + color: color-mix(in srgb, var(--blume-background) 72%, var(--blume-foreground)); +} +.recipe-card-reveal[data-curtain-open] .recipe-tags span { + background: color-mix(in srgb, var(--blume-background) 14%, var(--blume-foreground)); + color: color-mix(in srgb, var(--blume-background) 80%, var(--blume-foreground)); +} +.recipe-card-reveal[data-curtain-open] .recipe-card-footer { + border-color: color-mix(in srgb, var(--blume-background) 25%, var(--blume-foreground)); +} +.recipe-card-reveal[data-curtain-open] .recipe-featured { + border-color: color-mix(in srgb, var(--blume-background) 40%, var(--blume-foreground)); + color: var(--blume-background); +} +.recipe-card-footer { display: flex; flex-wrap: wrap; justify-content: space-between; + align-items: center; gap: 0.75rem; - margin-block-start: auto; - padding-block-start: 0.875rem; + margin-inline: 1.25rem; + padding-block: 0.75rem 1rem; border-block-start: 1px solid var(--recipe-line); font-size: 0.6875rem; } -.recipe-card footer span:first-child { +.recipe-card-footer span:first-child { color: var(--blume-muted-foreground); } +.recipe-card-cta { + display: inline-flex; + align-items: center; + gap: 0.25rem; + font-weight: 500; +} +.recipe-card-cta svg { + inline-size: 0.875rem; + block-size: 0.875rem; +} +.recipe-card-grid-configurable .recipe-card { + border-block-start: 2px solid var(--blume-accent); +} .recipe-empty { padding-block: 2rem; text-align: center; @@ -946,48 +1132,46 @@ gap: 1.5rem; align-items: start; } -.hardware-matrix { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.25rem; - background: var(--recipe-soft); +.hardware-selected { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; border: 1px solid var(--recipe-line); border-radius: 0.625rem; - padding: 0.25rem; - min-inline-size: 0; + background: var(--recipe-soft); + padding: 0.75rem; } -.hardware-matrix label { - position: relative; - padding: 0.625rem 0.5rem; - border: 1px solid transparent; - border-radius: 0.375rem; - cursor: pointer; -} -.hardware-matrix label[data-selected="true"] { - background: var(--blume-background); - border-color: var(--recipe-control); -} -.hardware-matrix label:has(:focus-visible) { - outline: 2px solid var(--blume-accent); - outline-offset: 1px; -} -.hardware-matrix input { - position: absolute; - opacity: 0; - inline-size: 1px; - block-size: 1px; -} -.hardware-matrix strong { +.hardware-selected-name { display: block; - font-size: 0.75rem; - line-height: 1.4; + font-size: 0.8125rem; font-weight: 500; + line-height: 1.4; } -.hardware-matrix small { +.hardware-selected-meta { display: block; color: var(--blume-muted-foreground); + font-size: 0.6875rem; + line-height: 1.5; + margin-block-start: 0.15rem; +} +.hardware-address-note { + margin-block: 0.35rem 0; + color: var(--blume-muted-foreground); + font-size: 0.6875rem; + line-height: 1.5; +} +.hardware-picker-caption { + margin-block: 0.35rem 0; + color: var(--blume-muted-foreground); font-size: 0.625rem; - margin-block-start: 0.25rem; + line-height: 1.5; +} +.hardware-scope { + color: var(--blume-muted-foreground); + font-size: 0.6875rem; + line-height: 1.6; + margin-block: 0.5rem 0; } .matrix-resource-title { display: block; @@ -1179,9 +1363,6 @@ margin-block-start: 0.5rem; display: block; } - .hardware-matrix { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } .matrix-advanced summary span { display: block; margin-inline-start: 0; @@ -1191,7 +1372,6 @@ @media (hover: hover) { .recipe-card:hover { border-color: var(--blume-muted-foreground); - background: var(--recipe-soft); } .recipe-family-filters button:hover, .option-chips button:hover { @@ -1199,7 +1379,7 @@ } } -/* Compact component experiments and custom hardware stay inside the recipe matrix. */ +/* Custom hardware and catalog lookup stay inside the recipe matrix. */ .recipe-detail [data-variant="outline"] { color: var(--blume-foreground); background: var(--blume-background); @@ -1219,40 +1399,182 @@ color: var(--blume-muted-foreground); cursor: pointer; } +.hardware-add { + border: 1px solid var(--recipe-control); + border-radius: 0.5rem; + padding: 0.375rem 0.625rem; + background: var(--blume-background); +} +.hardware-add:hover { + color: var(--blume-foreground); + border-color: var(--blume-muted-foreground); +} .hardware-clear { + flex: none; text-decoration: underline; text-underline-offset: 3px; } .hardware-current { - margin-block-start: 0.5rem; + margin: 0; color: var(--blume-muted-foreground); font-size: 0.6875rem; } -.hardware-search { +.hardware-lookup { display: grid; - gap: 0.5rem; + gap: 0.625rem; margin-block-start: 0.5rem; + padding: 0.75rem; + border: 1px solid var(--recipe-line); + border-radius: 0.625rem; + background: var(--blume-background); +} +.hardware-lookup > input { font-size: 0.75rem; } -.hardware-search p { - font-size: 0.6875rem; - color: var(--blume-muted-foreground); - line-height: 1.6; +.hardware-result-list { + display: grid; + gap: 0.25rem; + max-block-size: 18rem; + overflow-y: auto; } -.hardware-search button { - justify-self: start; - min-block-size: 2.25rem; - border: 1px solid var(--recipe-control); - padding: 0.375rem 0.75rem; - border-radius: 0.5rem; +.hardware-result { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.5rem; + border: 1px solid transparent; + border-radius: 0.375rem; cursor: pointer; } -.hardware-search button:disabled { +.hardware-result:hover { + background: var(--recipe-soft); +} +.hardware-result[data-selected="true"] { + border-color: var(--recipe-control); + background: var(--recipe-soft); +} +.hardware-result:has(:focus-visible) { + outline: 2px solid var(--blume-accent); + outline-offset: 1px; +} +.hardware-result input { + margin-block-start: 0.15rem; + accent-color: var(--blume-foreground); +} +.hardware-result strong { + display: block; + font-size: 0.75rem; + font-weight: 500; + line-height: 1.4; + overflow-wrap: anywhere; +} +.hardware-result small { + display: block; + color: var(--blume-muted-foreground); + font-size: 0.625rem; + line-height: 1.5; + margin-block-start: 0.125rem; +} +.hardware-lookup-status { + display: grid; + gap: 0.25rem; + margin: 0; + color: var(--blume-muted-foreground); + font-size: 0.6875rem; + line-height: 1.6; +} +.hardware-lookup-status span { + font-size: 0.625rem; +} +.hardware-retry, +.hardware-manual-toggle { + justify-self: start; + color: var(--blume-muted-foreground); + font-size: 0.6875rem; + text-decoration: underline; + text-underline-offset: 3px; + cursor: pointer; +} +.hardware-form { + display: grid; + gap: 0.75rem; + padding-block-start: 0.75rem; + border-block-start: 1px solid var(--recipe-line); +} +.hardware-form-title { + margin: 0; + font-size: 0.75rem; + font-weight: 500; +} +.hardware-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.625rem; +} +.hardware-form-wide { + grid-column: 1 / -1; +} +.hardware-form-field { + display: grid; + gap: 0.25rem; + min-inline-size: 0; + color: var(--blume-muted-foreground); + font-size: 0.6875rem; +} +.hardware-form-field select { + min-block-size: 2.25rem; + max-inline-size: 100%; + padding: 0.375rem 0.5rem; + border: 1px solid var(--recipe-control); + border-radius: 0.5rem; + background: var(--blume-background); + color: var(--blume-foreground); + font-size: 0.75rem; +} +.hardware-form-error { + margin-block: 0.5rem 0; + color: var(--blume-destructive, #b42318); + font-size: 0.6875rem; + line-height: 1.5; +} +.hardware-form-note { + margin: 0; + color: var(--blume-muted-foreground); + font-size: 0.625rem; + line-height: 1.6; +} +.hardware-form-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} +.hardware-form-actions button { + min-block-size: 2.25rem; + padding: 0.375rem 0.75rem; + border: 1px solid var(--recipe-control); + border-radius: 0.5rem; + background: var(--blume-background); + color: var(--blume-foreground); + font-size: 0.6875rem; + cursor: pointer; +} +.hardware-form-actions button[type="submit"] { + border-color: var(--blume-foreground); + background: var(--blume-foreground); + color: var(--blume-background); +} +.hardware-form-actions button:disabled { opacity: 0.5; cursor: default; } -.hardware-matrix strong { - overflow-wrap: anywhere; +@media (max-width: 40rem) { + .hardware-selected { + flex-direction: column; + gap: 0.5rem; + } + .hardware-form-grid { + grid-template-columns: minmax(0, 1fr); + } } .release-intro { font-size: 0.875rem; diff --git a/docs/assets/cookbook-recipes.json b/docs/assets/cookbook-recipes.json index b70755c40..d0e65a6ba 100644 --- a/docs/assets/cookbook-recipes.json +++ b/docs/assets/cookbook-recipes.json @@ -858,6 +858,67 @@ "install": "Install FastVideo following the DGX Spark installation guide.", "audio": true } + }, + { + "id": "fasth3-omniref-rtx5090", + "family": "minimax_h3", + "stage": "inference", + "task": "Reference media to video with audio", + "label": "FastH3 OmniRef on RTX 5090", + "summary": "Eight-forward video and audio from a prompt plus ordered image, video, and audio references, with the NVFP4 stack.", + "model": "FastVideo/FastH3-OmniRef-NVFP4-QAD", + "source": "examples/inference/basic/basic_fasth3_omniref_pdd.py", + "command": "python examples/inference/basic/basic_fasth3_omniref_pdd.py --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --profile rtx-5090 --image assets/images/couple.jpg --prompt 'A person on a sofa, lit by a warm lamp, speaking to camera.'", + "gpu_types": [ + "NVIDIA" + ], + "hardware": { + "platform": "cuda", + "gpu_count": 1, + "evidence": "source-configured" + }, + "evidence": "Source-backed" + }, + { + "id": "fasth3-omniref-rtx-pro6000", + "family": "minimax_h3", + "stage": "inference", + "task": "Reference media to video with audio", + "label": "FastH3 OmniRef on RTX PRO 6000", + "summary": "Eight-forward video and audio from a prompt plus ordered image, video, and audio references, with the NVFP4 stack.", + "model": "FastVideo/FastH3-OmniRef-NVFP4-QAD", + "source": "examples/inference/basic/basic_fasth3_omniref_pdd.py", + "command": "python examples/inference/basic/basic_fasth3_omniref_pdd.py --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --profile rtx-pro-6000 --image assets/images/couple.jpg --prompt 'A person on a sofa, lit by a warm lamp, speaking to camera.'", + "gpu_types": [ + "NVIDIA" + ], + "hardware": { + "platform": "cuda", + "gpu_count": 1, + "evidence": "source-configured" + }, + "evidence": "Source-backed" + }, + { + "id": "fasth3-omniref-spark", + "family": "minimax_h3", + "stage": "inference", + "task": "Reference media to video with audio", + "label": "FastH3 OmniRef on DGX Spark", + "summary": "Eight-forward video and audio from a prompt plus ordered image, video, and audio references, with the NVFP4 stack.", + "model": "FastVideo/FastH3-OmniRef-NVFP4-QAD", + "source": "examples/inference/basic/basic_fasth3_omniref_pdd.py", + "command": "python examples/inference/basic/basic_fasth3_omniref_pdd.py --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --profile spark --image assets/images/couple.jpg --prompt 'A person on a sofa, lit by a warm lamp, speaking to camera.'", + "gpu_types": [ + "NVIDIA" + ], + "hardware": { + "platform": "cuda", + "gpu_count": 1, + "device": "spark", + "evidence": "source-configured" + }, + "evidence": "Source-backed" } ] } diff --git a/docs/contributing/capacity_estimation.md b/docs/contributing/capacity_estimation.md index 6d5da86a8..9c52a17dd 100644 --- a/docs/contributing/capacity_estimation.md +++ b/docs/contributing/capacity_estimation.md @@ -49,7 +49,7 @@ node docs/js/build-cookbook-capacity.mjs --check node docs/js/build-cookbook-capacity.mjs --preview fastwan21 ``` -The check currently prints `Validated 1 capacity model(s) and 4 hardware variant(s).` +The check currently prints `Validated 1 capacity model(s) and 50 hardware variant(s).` The preview prints the model with component references expanded and each example's baseline under `base_config`. Inspect its identity, counts, geometry, and evidence. Both commands validate deployments and capacity metadata without @@ -68,7 +68,7 @@ Commit authored sources; generated assets are build output and must not be hand- | Model `capacity` | `memory_rules`, nonempty `components` | `sources`, `reference_runs` | | `components.` | `ref` | `alternatives` (other registered refs) | | Shared component | `name`, `parameter_count`, `sources` | `block_count` + `block_parameter_count`, `loaded_subset`, `geometry`; VAE-only `decode_feature_multiplier` | -| Hardware variant | `name`, `memory_type`, `memory_gb`, `backend`, `sources` | `ui_id`, `supported_dtypes`, `notes` | +| Hardware variant | `name`, `vendor`, `kind`, `memory`, `backend`, `sources` | `aliases`, `family` + `variant`, `form_factor`, `gpu_count`, `host_memory`, `partitionable`, `partitions`, `supported_dtypes`, `notes` | Roles are `transformer`, `transformer_2`, `text_encoder`, `text_encoder_2`, `image_encoder`, `vae`, or `audio_vae`; refs must match the role's component type. @@ -214,12 +214,14 @@ a fixed memory discount from `vae_tiling: true`, or assume this mode uses less memory than the default cached decoder. Other model families can use different tiling controls. This setting flow was checked in code, not by GPU generation. -Hardware IDs describe variants such as H100 SXM 80 GB. `memory_gb` is nominal -**decimal GB per device** (`1 GB = 1e9 bytes`); convert to GiB with -`memory_gb * 1e9 / 1024**3`. Use `memory_type: dedicated` for discrete VRAM or +Hardware IDs describe variants such as H100 SXM 80 GB. The authored +`memory.nominal` preserves `GB` or `GiB`; the build exports normalized decimal +`memory_gb` (`1 GB = 1e9 bytes`), converted to GiB with +`memory_gb * 1e9 / 1024**3`. Use `memory.type: dedicated` for discrete VRAM or `unified` for capacity shared with the CPU. Neither means measured free memory. `supported_dtypes` describes hardware format capability, not FastVideo kernel -support; omission means unknown. `ui_id` connects a variant to an existing UI choice. +support; omission means unknown. `aliases` connect a variant to existing UI IDs +and search names. Use checkpoint `model_index.json`/component `config.json` files, tensor headers or indexes, and the FastVideo loader to establish composition, counts, and geometry. @@ -232,8 +234,105 @@ names do not establish that components can be shared. Offline scripts or agents may prepare candidate metadata for review; builds and browser loaders do not run them, import model code, or download checkpoints. +## Hardware catalog + +[`hardware.yaml`](../cookbook/capacity/hardware.yaml) is the authored lookup +catalog. It records device specifications, not a runtime support matrix: a +searchable variant can still be unsupported by a model, backend, or kernel. +Every entry requires a vendor source URL and the date it was checked. The +[coverage checklist](../cookbook/capacity/hardware-coverage.md) tracks what is +present, what is next, and what is deliberately excluded. The catalog is for +discovery and reuse, not a prerequisite for running a model: a missing variant +uses the custom-device path, and `unknown` means no estimate, not that FastVideo +cannot attempt the recipe. + +```yaml +h100-sxm-80gb: + name: NVIDIA H100 SXM 80 GB + vendor: nvidia + kind: gpu + family: nvidia-h100 + variant: SXM 80 GB + form_factor: sxm + memory: + type: dedicated + nominal: { value: 80, unit: GB } + backend: cuda + partitionable: true + partitions: + - id: 1g.10gb + label: 1g.10gb + memory: { value: 10, unit: GB } + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [h100] + sources: + - url: https://www.nvidia.com/en-us/data-center/h100/ + checked: 2026-10-19 +``` + +`memory.type` is `dedicated` or `unified`. `memory.nominal` preserves the +vendor's value and unit (`GB` or `GiB`). Unified memory is shared system memory, +not VRAM; do not describe it as a GPU-only allocation. Add `memory.usable` plus a +`memory.policy` only when a documented policy derives an application budget. +Without that policy the estimator uses a labeled 75% application default +(`DEFAULT_UNIFIED_BUDGET_FRACTION`); `memoryBudgetGiB` overrides it. Nominal +unified memory is never treated as available VRAM, and the assumption is +reported in `memory.budgetSource` and the result notes. + +`family` groups exact `variant` records, so board, form-factor, and capacity +differences stay separate entries. `partitions` list selectable MIG/vGPU-style +allocations; selecting one never silently inherits full-card capacity. `gpu_count` +describes the device record and does not authorize summing GPUs into one pool. +`host_memory` is host RAM for CPU offload, separate from dedicated VRAM and from +unified memory. `aliases` preserve old links and search names; canonical IDs stay +stable. + +Custom devices use the same memory fields and must declare `backend` and +`supported_dtypes`. They are not catalog-verified: + +```javascript +const custom = { + id: "custom", + name: "Custom accelerator", + custom: true, + memory: { type: "dedicated", nominal: { value: 48, unit: "GB" } }, + backend: "cuda", + supported_dtypes: ["fp32", "fp16", "bf16", "fp8"], +}; +const result = estimateMemory({ + config, + workload, + model, + hardware: custom, + memoryBudgetGiB: 40, // optional application cap, in GiB + partitionId: "1g.10gb", // optional catalog partition + allocationMemoryGb: 10, // optional provisioned allocation, in GB + hostMemoryGb: 64, // optional host RAM for offload +}); +``` + +An unknown backend or missing memory type/capacity returns `unknown`. A missing +format list is accepted for fp32/fp16/bf16 storage and called out as an +assumption; fp8 or NVFP4 storage still returns `unknown` because capability was +not cataloged. The estimate +reports `memory` (type, nominal, budget, partition) and `host` (required, +capacity, status) separately from `capacityGiB` and `status`. + +The generated `hardware.json` is versioned: + +```json +{ "version": 1, "devices": [{ "id": "h100-sxm-80gb", "...": "..." }] } +``` + +Values keep authored aliases, normalized decimal `memory_gb`, optional +`usable_memory_gb` / `host_memory_gb`, and normalized partition capacities. + ## Optional successful-run evidence +HTTP/OpenAPI schema compatibility is a separate contract; see +[serving documentation](serving_documentation_contract.md). A capacity estimate +and a completed generation do not establish HTTP client compatibility. + Add records inline under the manifest's `capacity.reference_runs`. One completed generation is useful; benchmarking every configuration is unnecessary. Follow the [serving guide](../cookbook/openai-api.md) with the FastWan baseline and set the @@ -246,6 +345,7 @@ load alone is insufficient. Record the effective request, including inherited de | --- | --- | | `id`, `date` | Unique run ID within the model; date as `YYYY-MM-DD` | | `hardware`, `gpu_count` | Hardware catalog ID and actual device count | +| `hardware_partition`, `hardware_allocation_gb` | Optional MIG/vGPU partition ID and provisioned allocation in decimal GB; a full-card run does not apply to a partition | | `outcome`, `execution` | `completed`; `first_request` or `warm_request` | | `evidence_url` | Accessible HTTP(S) logs or benchmark evidence | | `context.runtime`, `context.deployment` | Serving runtime ID and the cookbook deployment ID used for the run; the deployment must exist and use that runtime | @@ -313,7 +413,7 @@ also re-exported by the shared cookbook API/bundle: | --- | --- | | `loadCapacityIndex(url, options?)` | Index with model summaries, `hardware_url`, and resolved source `url` | | `loadCapacityModel(index, id, options?)` | Model with components mapped by role, examples, and `reference_runs` | -| `loadCapacityHardware(index, options?)` | Hardware mapping keyed by ID | +| `loadCapacityHardware(index, options?)` | `{version: 1, devices, url}` with stable IDs and aliases | Start with the generated index URL, then pass the returned index to the other loaders. Each selected component is expanded with its `ref` and geometry; @@ -328,6 +428,8 @@ required structure/identity, or unknown model IDs. There is no module-level cach These loaders fetch static facts; the calculator must resolve execution settings and check rule coverage before estimating memory or recommending hardware. -For detailed return shapes and the proposed PR #1978 adapter, see the -[temporary API contract](../cookbook/capacity-api-contract.review.md). -Delete that handoff and this link when integration lands, retaining stable API behavior here. +`assessCapacity()` combines the analytical estimate with applicable measured +evidence; `matchEvidence()` reports applicable, unverified, and rejected runs; +`recommendCapacity()` returns measured or analytical fits and keeps unknown +results explicit. No successful generation is treated as a minimum-VRAM +measurement, and allocator-only peaks never establish device fit. diff --git a/docs/contributing/cookbook_configuration.md b/docs/contributing/cookbook_configuration.md index 646ba7e8e..57376d0b9 100644 --- a/docs/contributing/cookbook_configuration.md +++ b/docs/contributing/cookbook_configuration.md @@ -332,56 +332,93 @@ response alone does not establish generation readiness. ## Memory estimates `estimateMemory()` in `docs/js/cookbook-memory.mjs` estimates one request's peak -memory on one GPU from a resolved configuration. It is analytical: a result is -not a measurement and never marks a recipe as verified. +memory on one device from a resolved configuration. It is analytical: a result +is not a measurement and never marks a recipe as verified. ```javascript +const hardware = await api.loadCapacityHardware(index); const { config } = api.resolveConfig(catalog, edits); -const result = api.estimateMemory({ config, workload: catalog.workload, model, hardware }); +const result = api.estimateMemory({ + config, + workload: catalog.workload, + model, + hardware: hardware.devices.find((device) => device.id === "rtx-5090-32gb"), + memoryBudgetGiB: 28, // optional override; unified memory otherwise uses a labeled 75% default +}); ``` -Each phase (encoding, denoising, decoding) adds the weights resident on the GPU -to that phase's working memory. The largest phase plus headroom (10% and 1 GiB -by default) is compared with the device memory. `status` is `fits`, `exceeds`, -`unsupported` when the device lacks a weight type, or `unknown` with a `reason`. -The result also reports each phase's weights and working memory in GiB. +Each phase (`startup`, `encoding`, `denoising`, `decoding`) adds the weights +resident on the device to that phase's working memory. The largest phase plus +headroom (10% and 1 GiB by default) is compared with the device budget. `status` +is `fits`, `exceeds`, `unsupported` when the device lacks a weight type, or +`unknown` with a `reason`. The result reports each phase's weights and working +memory in GiB, plus `capacityGiB`, `memory`, `host`, and `hardware`. | Model field | Meaning | | --- | --- | | `memory_rules` | Working-memory rule; `wan` is the only rule | -| `geometry` | For `wan`: `temporal_compression`, `spatial_compression`, `patch_size`, `hidden_size`, `ffn_size` and `vae_decoder_channels` | -| `components[].role` | `text_encoder`, `transformer` or `vae` | +| Shared transformer geometry | For `wan`: `hidden_size`, `ffn_size`, and `patch_size` | +| Shared VAE geometry | `temporal_compression`, `spatial_compression`, and `vae_decoder_channels` | +| `components[].role` | `text_encoder`, `transformer`, `image_encoder`, or `vae` | | `components[].parameter_count` | Parameters of the loaded subset | -| `components[].dtype` | Weight precision; optional `activation_dtype` sets the compute precision | | `components[].block_count`, `block_parameter_count` | Repeated blocks and their total parameters, for streaming offload | -| `components[].streams_when_offloaded` | The text encoder streams its blocks through FSDP when offloaded | -| `components[].quantized_parameter_count` | Transformer parameters stored in the requested quantized format | -A hardware entry has `name`, `type: discrete_gpu`, `memory_per_device` with a -`value` and a `GiB` or `GB` unit, and `supported_dtypes`. +Shared component records contain no selected dtype or offload setting; those +come from the resolved serving configuration. A model manifest references +shared entries instead of copying counts: + +```yaml +capacity: + memory_rules: wan + components: + transformer: { ref: wan2.1-t2v-1.3b-dit } + text_encoder: { ref: umt5-xxl-encoder } + vae: { ref: wan2.1-vae-decoder } +``` + +The hardware lookup is versioned and source-backed; see the +[capacity guide](capacity_estimation.md#hardware-catalog). Dedicated memory uses +nominal or selected-partition capacity. Unified memory is shared with the CPU and +OS, so the estimator uses `memoryBudgetGiB`, a catalogued `memory.usable` policy, +or a labeled 75% default, and never treats nominal system memory as VRAM. +`host_memory` and `hostMemoryGb` describe separate host RAM for CPU offload; the +result reports a +host requirement and status independently from device fit. Custom devices use +the same fields and should set `custom: true`; they are not catalog-verified. Offload follows the native runtime: - Unset offload switches are on (`OffloadConfig` in `fastvideo/api/schema.py`). Layerwise DiT offload replaces whole-model DiT offload. -- An offloaded component leaves the GPU outside its own phase and is complete +- An offloaded component leaves the device outside its own phase and is complete during it. Layerwise DiT offload and FSDP text-encoder offload instead keep the weights outside the blocks plus two blocks. -- Without FSDP, whole-model DiT offload leaves the transformer on the GPU. +- Without FSDP, whole-model DiT offload leaves the transformer on the device in + every request phase, not only denoising. +- `FASTVIDEO_LAYERWISE_RESIDENT_BLOCKS` keeps that many transformer blocks on + the device; the estimate counts them in addition to two streamed blocks during + denoising and as resident outside it. +- Unified-memory CUDA devices disable every CPU-offload flag at runtime; the + estimate applies that policy instead of subtracting offloaded weights. - Packed NVFP4 weights are buffers, so layerwise offload keeps them resident. - `lazy_module_load` defers only the components a pipeline lists in `_lazy_module_names`. Wan lists none, so the switch changes nothing. -Multiple GPUs, FSDP, unified memory, image-to-video, more than one video per -prompt, missing parameter counts, text-encoder quantization and loading peaks -return `unknown`. With VAE tiling enabled, a note says the -decode estimate assumes untiled decoding. +Multiple GPUs, FSDP, VSA, compilation, quantization, lazy loading, image-to-video, +more than one video per prompt, missing parameter counts, and unsupported +attention backends return `unknown`. A disabled VAE feature cache also returns +`unknown` because the full-clip decode path has no rule. Unified memory without +an application budget uses the labeled 75% default. With VAE tiling enabled, a +note says the decode estimate assumes untiled decoding. `WAN_RULE` holds the uncalibrated tensor counts. To calibrate them, run with `FASTVIDEO_STAGE_LOGGING=1`: each stage then records `peak_allocated_mb` and `peak_reserved_mb` and resets the peak counter. The request-level `peak_memory_mb` is read after those resets, so it does not cover earlier stages. Allocator figures exclude the CUDA context, which the headroom covers. +Successful-run evidence and analytical fit stay separate; see +`assessCapacity()`, `matchEvidence()`, and `recommendCapacity()` in the +[capacity guide](capacity_estimation.md#optional-successful-run-evidence). To add a model family, write a function that returns working-memory bytes per phase, register it in `WORKING_MEMORY` with its required geometry fields, and diff --git a/docs/contributing/serving_documentation_contract.md b/docs/contributing/serving_documentation_contract.md new file mode 100644 index 000000000..67dc7d68d --- /dev/null +++ b/docs/contributing/serving_documentation_contract.md @@ -0,0 +1,123 @@ +# Serving documentation contract + +This page defines the contributor-facing boundary between the HTTP schema, runtime +capabilities, and evidence. It is a handoff contract for the server and docs-site +owners; it is not an HTTP reference and does not restate endpoint behavior. + +## Ownership + +- **Server owner:** FastAPI routes, Pydantic request/response models, error + envelopes, status codes, authentication, streaming, and the generated OpenAPI + document. +- **Docs-site owner:** the HTTP reference route, copyable client examples, and + reference navigation. Keep it separate from the Python SDK/CLI reference. +- **Capacity owner:** hardware identity, memory estimates, and evidence + applicability. These fields never prove HTTP or model capability. + +OpenAPI is a schema description. It does not establish OpenAI API or SDK +behavioral compatibility. Claim compatibility only for an exact endpoint, +client and version, payload, response, and error behavior that has a test or a +recorded run. + +## Generated schema + +Generate the HTTP schema from the application's actual routes and Pydantic +models and commit a deterministic export for review. Do not maintain a parallel +hand-written schema. + +The export/check path must not start the application lifespan, load model +weights, initialize CUDA/MLX, require a GPU, bind a port, or fetch a remote +source. Inspect imports before claiming that property; moving an import can +reintroduce a lifespan or hardware dependency. Do not publish local paths, +credentials, or private deployment URLs in the export or examples. + +The current server returns request-validation failures as HTTP 400 with its +OpenAI-style error envelope. Document those actual responses. Do not copy +FastAPI's default 422 schema when the mounted application does not return it. + +## Workload IDs + +Use these shared IDs in runtime `workloads`, recipe/deployment `workload`, and +capability rows. The shared media map lives in +`docs/js/cookbook-workloads.mjs`; the catalog builder and sample client use it as +the single workload allow-list. The server owner implements endpoints and the +docs-site owner renders from the declared input/output media rather than +assuming video. + +| ID | Input | Output | Minimal semantics | +| --- | --- | --- | --- | +| `t2v` | text | video | Text prompt to a video clip | +| `i2v` | image + text | video | Image reference drives the first frame or motion | +| `t2i` | text | image | Text prompt to one still image | +| `t2a` | text | audio | Text prompt to an audio clip | +| `v2a` | video + text | audio | Video soundtrack, speech, or audio response | + +The map records the canonical sample endpoint, extension, and media type used +by cookbook examples; it does not prove that a model or deployment supports the +workload. Do not infer support from an ID. The capability row must name the +actual operation and media type, and unsupported combinations stay +`unsupported` or `unknown` rather than falling back to a video route. + +## Capability matrix + +Schema presence does not mean every model supports an operation. Serving docs +must publish a matrix with stable fields: + +| Field | Meaning | +| --- | --- | +| `model_id` | Checkpoint identity used by the deployment | +| `deployment_id` | Canonical cookbook deployment (`/`) | +| `workload` | Shared workload ID and its input/output media | +| `runtime` | FastVideo runtime/adapter identifier | +| `endpoint` | Method and path | +| `operation` | Create, poll, download, cancel, list, or delete | +| `status` | `supported`, `unsupported`, or `unknown` | +| `constraint` | Required option, model limitation, or admission rule | +| `evidence` | Link and kind, with revisions; see below | + +Mark FastVideo extensions explicitly. State unsupported and unknown operations +instead of implying parity. Cover success and error envelopes, multipart and +binary responses, and the implemented job lifecycle (create, poll, download, +cancel/delete). Document authentication as configured. Call out streaming and +WebSocket limitations rather than describing a capability the server does not +mount. + +## Evidence + +Use three distinct labels: + +- `schema`: the route and shapes exist in the generated OpenAPI document. +- `contract_test`: a focused test exercises the request/response or client path. +- `measured_generation`: a recorded run produced the media on named hardware. + +Only the last label supports a GPU-validated generation claim. A schema check, +unit test, or successful process start does not establish output quality, model +capability, or memory fit. Link the revision and the exact client version with +any compatibility claim. + +Capacity evidence follows the same rule: a completed generation is +compatibility evidence, while a device-wide memory measurement is required for a +measured fit. See [capacity estimation](capacity_estimation.md#optional-successful-run-evidence) +for the memory-evidence schema. + +## User documentation + +Start from a maintained cookbook recipe and a selected hardware/runtime +combination, then show the first generation with a matching cURL, OpenAI Python, +or OpenAI JavaScript example where that path is tested. Follow with polling, +download, and troubleshooting. Keep in-process FastVideo SDK/CLI examples +separate so readers do not confuse them with HTTP clients. + +A static documentation site must remain useful without a running inference +server. A browser "Try it" control is optional and may only target a +user-selected reachable server within that server's actual CORS, authentication, +and HTTPS policy. Do not add a shared public inference endpoint, a hosted +request proxy, a broad CORS exception, or an authentication bypass for a +tutorial. + +## Contributor checks + +Update the model/deployment/runtime capability row when adding serving support. +Include the generated schema, focused contract/client tests, link checks, and +the evidence links. CI should detect a stale export and check meaningful +request, response, and client behavior independently of the schema diff. diff --git a/docs/cookbook/capacity-api-contract.review.md b/docs/cookbook/capacity-api-contract.review.md deleted file mode 100644 index dbfd41b37..000000000 --- a/docs/cookbook/capacity-api-contract.review.md +++ /dev/null @@ -1,261 +0,0 @@ -# Temporary capacity API contract for calculator integration - -**Temporary review handoff.** This document describes the current static loaders -for authors integrating the calculator proposal in -[PR #1978](https://github.com/hao-ai-lab/FastVideo/pull/1978/changes). -It adds no calculator API. **TODO: delete this exact file, -`docs/cookbook/capacity-api-contract.review.md`, and its discoverability link in -`docs/contributing/capacity_estimation.md` when integration lands and essential -stable API behavior has moved into that contributor guide.** Keep the API -modules, authored metadata, and catalog generation. - -The [contributor guide](../contributing/capacity_estimation.md) owns authoring and -recorded-run requirements. The implementation is -[`cookbook-capacity.mjs`](../js/cookbook-capacity.mjs); generated JSON is build -output, not a second source to edit. - -## Load index → model → hardware - -The capacity module is pure ESM with no DOM, inference/runtime, or estimator -imports. From the repository root in an ESM consumer: - -```javascript -import { - loadCapacityIndex, - loadCapacityModel, - loadCapacityHardware, -} from "./docs/js/cookbook-capacity.mjs"; - -const options = { signal: new AbortController().signal }; -const index = await loadCapacityIndex( - "https://haoailab.com/FastVideo/assets/cookbook-config/capacity/index.json", - options, -); -const model = await loadCapacityModel(index, "fastwan21", options); -const hardware = await loadCapacityHardware(index, options); -const h100 = hardware["h100-sxm-80gb"]; -``` - -These three exports are also re-exported by -[`cookbook-config.mjs`](../js/cookbook-config.mjs). For a browser using the shared -bundle, load `/FastVideo/assets/cookbook-config/cookbook-config.js`, then use -`FastVideoConfigCookbook.loadCapacityIndex`, `.loadCapacityModel`, and -`.loadCapacityHardware`. Adjust the site prefix when hosted elsewhere. -Generate assets before serving them; the loaders do not build missing files. -Use your preview site's origin while these assets are not yet published. - -| Function | Promise result | -| --- | --- | -| `loadCapacityIndex(url, {fetcher, signal} = {})` | Index JSON with a resolved source `url` added | -| `loadCapacityModel(index, id, {fetcher, signal} = {})` | One self-contained model record | -| `loadCapacityHardware(index, {fetcher, signal} = {})` | Hardware mapping keyed by hardware ID | - -`fetcher` defaults to `globalThis.fetch`; it is called as -`fetcher(url, {signal})`. A custom fetcher must return a response with `ok`, -`status` for errors, `json()`, and preferably `url`. Each call receives its own -options; options passed to the index call are not saved for later calls. - -The index's `url` uses `response.url` when present, including after redirects. -Otherwise it resolves the requested URL against `globalThis.location?.href`. -Use an absolute request URL outside a browser when a custom response lacks -`url`. Model and hardware paths resolve relative to `index.url` with `new URL`. -Pass the returned index to later loaders; raw index JSON lacks this source URL. - -Failures reject the promise. A non-OK response throws -`Could not load capacity metadata: HTTP `; fetch and JSON errors -propagate. The signal is forwarded to the fetcher and checked after fetching -and after parsing JSON, preserving cancellation even with a custom fetcher. -Invalid shapes/identities also reject. There is no module-level cache, retry, -or fallback; ordinary fetch cache behavior still applies. -Browser validation checks the loader's required structure; full authoring -validation belongs to the catalog build. - -## Returned data - -The generated [index](../assets/cookbook-config/capacity/index.json) currently -contains one participating model: - -```javascript -{ - hardware_url: "hardware.json", - models: [{ - id: "fastwan21", - name: "FastWan2.1 1.3B", - model_id: "FastVideo/FastWan2.1-T2V-1.3B-Diffusers", - catalog_url: "models/fastwan21.json", - }], - url: "https://haoailab.com/FastVideo/assets/cookbook-config/capacity/index.json", -} -``` - -`id` is the cookbook manifest key; `model_id` is the checkpoint identity. -Models whose manifests omit optional `capacity` remain valid cookbook entries -but do not appear in this index. An unknown `id` rejects with -`Unknown capacity model ` before fetching a model. - -The [FastWan model](../assets/cookbook-config/capacity/models/fastwan21.json) -returns `id`, `name`, `model_id`, `memory_rules`, `components`, `examples`, -`reference_runs`, and optional model `sources`. `components` is a **mapping by -execution role**, not an array. Each selected shared component has already -been expanded, retaining its `ref`: - -| Role | `ref` | `kind` | `parameter_count` | -| --- | --- | --- | ---: | -| `transformer` | `fastwan2.1-t2v-1.3b-vsa-dit` | `transformer` | 1,489,821,760 | -| `text_encoder` | `umt5-xxl-encoder` | `encoder` | 5,680,910,336 | -| `vae` | `wan2.1-vae-decoder` | `vae` | 73,296,659 | - -Components also have `name`, `sources`, and optional `block_count`, -`block_parameter_count`, `loaded_subset`, `geometry`, and VAE-only -`decode_feature_multiplier`. Counts are numbers -for the loaded subset. The transformer has 30 blocks with 1,464,046,080 block -parameters; its geometry includes `hidden_size: 1536`, `ffn_size: 8960`, -`patch_size: [1, 2, 2]`, and `gated_ffn: false`. The text encoder records weight -and block counts and its loaded subset, with no geometry object. Detailed -encoder geometry remains optional for future activation rules. VAE geometry is: - -```javascript -{ - temporal_compression: 4, - spatial_compression: 8, - latent_channels: 16, - output_channels: 3, - vae_decoder_channels: 96, -} -``` - -Compression determines the latent grid. Optional `latent_channels` sizes the -compressed buffer; `output_channels` counts final decoded channels, not raw -convolution channels before any unpatching. Neither changes video-token count. -Decoder width scales decode working memory. The VAE exports an optional -top-level `decode_feature_multiplier: 48`, a positive finite number supplying -an **uncalibrated feature/cache heuristic** borrowed from PR #1978. It means -equivalent internal feature buffers, not 48 RGB frames or an architecture-config -fact. The builder preserves omission rather than supplying a default. See the -[decode feature multiplier](../contributing/capacity_estimation.md#vae-decode-feature-multiplier) -for its approximate formula and limits. Output/merge buffers and resident -weights remain separate. Actual batch, size, precision, cache/tiling mode, -buffer placement, and workspace still need execution-specific rules; catalog -loading does not resolve them or establish full VAE peak coverage. -See the -[authoring reference](../contributing/capacity_estimation.md#authoring-reference) -for field meanings and validation. Tile/cache/precision selections remain -execution inputs from native configuration and runtime defaults, not component -geometry. Optional `alternatives`, when authored, are expanded component objects; -the loader does not select one. - -Each manifest deployment becomes an example. Its `defaults` YAML is resolved -into `base_config`; its launch environment becomes `env`. FastWan currently has: - -```javascript -model.examples[0] // selected fields; base_config remains the nested native config -// { -// id: "cuda-rest", -// deployment_id: "fastwan21/cuda-rest", -// config: "examples/serving/openai_fastwan21_1_3b.yaml", -// runtime: "fastvideo-cuda-rest", -// env: { FASTVIDEO_ATTENTION_BACKEND: "VIDEO_SPARSE_ATTN" }, -// base_config: { generator: {...}, server: {...}, default_request: {...} } -// } -``` - -The exported baseline includes `generator.engine.compile.enabled: true`, -`generator.pipeline.experimental.VSA_sparsity: 0.8`, and -`generator.pipeline.experimental.dmd_denoising_steps: [1000, 757, 522]`. -`base_config` preserves the authored baseline; it is **not a fully -runtime-resolved configuration** or a completed-generation measurement. - -`reference_runs` is optional when authoring; the builder normalizes omission to -`[]`, and model loader results always require an array. FastWan currently has -`reference_runs: []`. Future records can carry completed-generation evidence, -execution context, and optional per-rank memory measurements. Use the -contributor guide's schema and applicability rules; absence means no recorded -evidence, not zero memory usage. - -The [hardware catalog](../assets/cookbook-config/capacity/hardware.json) is a -mapping, for example: - -```javascript -hardware["h100-sxm-80gb"] -// { -// name: "NVIDIA H100 SXM 80 GB", ui_id: "h100", -// memory_type: "dedicated", memory_gb: 80, backend: "cuda", -// supported_dtypes: ["fp32", "fp16", "bf16", "fp8"], -// sources: [...], notes: "..." -// } -``` - -`memory_gb` is nominal **decimal GB per device** (`1 GB = 1e9 bytes`), not free -memory. Convert to GiB using `memory_gb * 1e9 / 1024**3`. Recorded-run memory -uses `unit: "GiB"`; its `torch_peak_allocated`, `torch_peak_reserved`, and -`device_used` metrics have different scopes. `memory_type` is `dedicated` or -`unified`; unified capacity is shared with the CPU. Optional -`supported_dtypes` describes hardware format capability, **not FastVideo kernel -support**. Missing capability metadata means unknown. - -## Illustrative bridge to PR #1978 - -The following is **consumer-side illustration, not an implemented/exported -adapter**. It illustrates the proposal's flat geometry/component array and hardware -shape at [revision 349cc0b](https://github.com/Ishxn20/FastVideo/blob/349cc0b184ffcb10027234bd86b53316a93f580b/docs/js/cookbook-memory.mjs#L168), -passing through optional component metadata for later rules. This does not -implement consumption of `decode_feature_multiplier` in that proposal. -Do not pass this output to an estimator until its coverage has been checked. -`runtimePrecision` must be supplied from actual execution resolution; never -infer weight or activation precision from parameter counts or geometry. - -```javascript -function illustrateInputs(model, device, runtimePrecision) { - if (device.memory_type !== "dedicated") - throw new Error("Unified memory requires an explicit calculator policy"); - const components = Object.entries(model.components).map(([role, component]) => { - const precision = runtimePrecision[role]; - if (!precision?.dtype || !precision?.activation_dtype) - throw new Error(`Resolve actual storage and activation precision for ${role}`); - return { - role, - parameter_count: component.parameter_count, - block_count: component.block_count, - block_parameter_count: component.block_parameter_count, - geometry: component.geometry, - decode_feature_multiplier: component.decode_feature_multiplier, - dtype: precision.dtype, - activation_dtype: precision.activation_dtype, - }; - }); - return { - model: { - memory_rules: model.memory_rules, - geometry: { ...model.components.transformer.geometry, ...model.components.vae.geometry }, - components, - }, - hardware: { - name: device.name, - type: "discrete_gpu", - memory_per_device: { value: device.memory_gb, unit: "GB" }, - supported_dtypes: device.supported_dtypes, - }, - }; -} -``` - -When supplied, per-component geometry preserves encoder dimensions without -colliding with transformer dimensions in the flat model geometry. The initial -encoder inventory contains weight counts only; an approximate encoding-stage -allowance must be explicit rather than deriving zero activations from absent -geometry. Detailed encoder rules can consume optional geometry later. - -This illustration is specific to the shown Wan composition. Quantized parameter -subsets, retained original weights, streaming/offload behavior, batching, -topology, cache/tiling modes, backend workspaces, and actual tensor lifetimes -require execution-specific accounting. A selected dtype alone does not cover -these allocations. The later calculator also needs effective configuration, -request/workload, and launch environment; this bridge does not resolve them. - -`memory_rules: "wan"` identifies a rule family; it does not validate kernels or -prove that a particular estimator covers this variant. FastWan VSA has extra -gate weights beyond ordinary Wan, already included in the transformer count. -Its VSA workspace, compile buffers, and DMD execution schedule also need explicit -coverage. The initial ordinary-Wan rule must return unsupported/unknown for -uncovered FastWan execution; matching geometry is insufficient. No working -calculator integration, hardware recommendation, or fit guarantee is added here. diff --git a/docs/cookbook/capacity/hardware-coverage.md b/docs/cookbook/capacity/hardware-coverage.md new file mode 100644 index 000000000..d91e1cee8 --- /dev/null +++ b/docs/cookbook/capacity/hardware-coverage.md @@ -0,0 +1,88 @@ +# Hardware coverage checklist + +This checklist tracks the hardware lookup catalog, not FastVideo runtime or kernel +support. A searchable device can still be unsupported by a model, backend, or +kernel. Capacity is never inferred from a marketing family name. + +Batch 1 is the reviewable foundation in +[hardware.yaml](hardware.yaml): **52 variants across 40 families** — 31 NVIDIA, +6 AMD, 3 Intel, and 12 Apple entries; 38 dedicated-memory devices and 14 +unified-memory systems. Every entry has a vendor source URL and a checked date. + +## Batch 1 — shipped in this change + +Priority order: devices named by existing recipes/baselines and the previous +picker, then common consumer, workstation, and data-center variants. + +| Priority | Family | Variants | Memory | Why now | +| --- | --- | --- | --- | --- | +| 1 | NVIDIA H100 | SXM 80 GB, NVL 94 GB | Dedicated | Recipe/baseline anchor; SXM MIG profiles | +| 1 | NVIDIA H200 | SXM 141 GB | Dedicated | Common data-center target | +| 1 | NVIDIA B200 | 180 GB | Dedicated | Previous picker | +| 1 | NVIDIA GB200 | Blackwell GPU 186 GB | Dedicated | Previous picker; per-GPU, not pooled | +| 1 | NVIDIA A100 | SXM 80 GB | Dedicated | Common data-center target; MIG profiles | +| 1 | NVIDIA L40S / L4 | 48 GB / 24 GB | Dedicated | Common serving cards | +| 1 | NVIDIA RTX 5090 | 32 GB | Dedicated | FastH3 consumer baseline | +| 1 | NVIDIA RTX PRO 6000 Blackwell | Workstation 96 GB | Dedicated | FastH3 consumer baseline | +| 1 | NVIDIA RTX 4090 | 24 GB | Dedicated | FastH3 FP8 baseline | +| 1 | NVIDIA DGX Spark GB10 | 128 GB, 64 GB OEM | Unified | FastH3 Spark baseline | +| 1 | Apple M4 Max | 36, 48, 64, 128 GB | Unified | FastMetal tested 36 GB recipe | +| 1 | Apple M5 Max / M5 Ultra | 48/64/128 GB; 96/256/512 GB | Unified | Current Mac Studio/MacBook Pro options | +| 2 | Apple M1 Pro / M2 | 16 GB / 8 GB | Unified | M1 Pro FastMetal evidence; low-memory Apple systems | +| 2 | NVIDIA RTX 50 series | 5080 16, 5070 Ti 16, 5070 12, 5060 Ti 16/8 | Dedicated | Common consumer cards | +| 2 | NVIDIA RTX 40 series | 4080 Super 16, 4070 Ti Super 16, 4060 Ti 16/8 | Dedicated | Common consumer cards | +| 2 | NVIDIA RTX 30 series | 3090 24, 3080 12/10 | Dedicated | Widely installed | +| 2 | NVIDIA RTX 6000 Ada / A6000 / A5000 / A4000 | 48 / 48 / 24 / 16 GB | Dedicated | Workstations | +| 2 | NVIDIA RTX 5000 Ada / 4000 Ada | 32 / 20 GB | Dedicated | Workstations | +| 2 | AMD Instinct MI300X / MI325X / MI355X | 192 / 256 / 288 GB | Dedicated | Data-center alternatives | +| 2 | AMD Radeon RX 7900 XTX / RX 9070 XT | 24 / 16 GB | Dedicated | Consumer alternatives | +| 2 | AMD Radeon PRO W7900 | 48 GB | Dedicated | Workstation alternative | +| 2 | Intel Arc A770 / B580 | 16 / 12 GB | Dedicated | Consumer alternatives | +| 2 | Intel Data Center GPU Max 1550 | 128 GB | Dedicated | Data-center alternative | + +## Batch 2 — next reviewable additions + +These are deliberately not asserted in the catalog yet. Add them only with a +vendor source and checked date. + +- [ ] NVIDIA H100 PCIe 80 GB, H200 NVL, B300/GB300 variants. +- [ ] NVIDIA A100 40 GB, A40, A10, A30, T4, V100 16/32 GB. +- [ ] NVIDIA laptop GPUs, separated by laptop power/board variant and VRAM. +- [ ] NVIDIA RTX 5070 laptop, 5060/5050 desktop, 4070/4060 desktop variants. +- [ ] NVIDIA RTX PRO 6000 Max-Q and Server Edition (separate from Workstation). +- [ ] NVIDIA DGX Station / OEM Grace-Blackwell systems. +- [ ] AMD Instinct MI250X, MI210, MI300A; Radeon RX 9060 XT, W6800. +- [ ] Intel Arc B570, Flex 170/140. +- [ ] Apple M3 Max/Ultra, M2 Max/Ultra, M4 Pro, and remaining older configurable sizes. +- [ ] Additional MIG/vGPU profiles where a vendor documents the partition size. +- [ ] Systems with a documented host-RAM configuration for offload estimates. + +## Validation rules + +The build fails on duplicate IDs, names, aliases, or family variants; duplicate +or malformed sources; missing checked dates; unknown units, vendors, kinds, +backends, or dtypes; usable budgets above nominal capacity; partitions above +the full device; and unified entries that hide capacity in a generic `memory_gb` +field. + +- `memory.nominal` and `memory.usable` keep the authored value/unit (`GB` or + `GiB`); the build also exports normalized decimal `memory_gb` and + `usable_memory_gb`. +- Dedicated records expose VRAM. Unified records expose shared system memory and + never expose it as VRAM; the estimator uses a labeled 75% application default + and accepts an explicit `memoryBudgetGiB` override. +- `partitions` are separate selectable allocations. Selecting one never silently + inherits the full-card capacity; a vendor-documented profile may explicitly + span the full memory. If no partition is selected, the full device is used. +- `aliases` preserve old UI links and search names; canonical IDs remain stable. +- `sources[].checked` records when the vendor page was verified, not when the + device was released. + +## Deliberately out of scope + +- Claiming kernel, dtype, backend, or model support from catalog presence. +- Browser-side detection of the remote generation host. Lookup is user input. +- Summing per-GPU capacity into a pool. GPU count and topology are separate. +- Inventing usable-memory reserves for macOS, Windows, or OEM systems. A budget + must come from a documented policy or the user. +- Treating a successful generation as a measured minimum-VRAM result. diff --git a/docs/cookbook/capacity/hardware.yaml b/docs/cookbook/capacity/hardware.yaml index 11d6ab07a..d9c2aefda 100644 --- a/docs/cookbook/capacity/hardware.yaml +++ b/docs/cookbook/capacity/hardware.yaml @@ -1,62 +1,980 @@ -# Initial variants corresponding to existing cookbook hardware choices. -# memory_gb is nominal capacity per device in decimal GB (1 GB = 1e9 bytes), -# not memory available to the application. Unified memory is shared with the CPU. -# supported_dtypes, when listed, describes hardware format capability rather than -# FastVideo kernel support. Omission means that formats have not been cataloged. -# NVFP4 entries are backed by E2M1 matrix instructions with E4M3 block scales -# for the device's documented compute capability. +# Source-backed hardware inventory for cookbook lookup and capacity estimates. +# +# This is a specification catalog, not a runtime support matrix. Inclusion means +# the device/system is searchable; kernel, dtype, and model support are separate. +# memory.nominal is the vendor-stated capacity in its original unit. For unified +# systems it is shared system memory, never dedicated VRAM. memory.usable is an +# application budget derived by an explicit policy; omit it when no honest +# budget exists. The estimator uses a labeled 75% default for unified memory and +# lets callers override it. Sources carry the URL used and the date checked. +# +# Batch 1 prioritizes devices named by existing recipes, baselines, and the +# previous picker, then common consumer, workstation, and data-center variants. +# See hardware-coverage.md for the maintained checklist. hardware: h100-sxm-80gb: name: NVIDIA H100 SXM 80 GB - ui_id: h100 - memory_type: dedicated - memory_gb: 80 + vendor: nvidia + kind: gpu + family: nvidia-h100 + variant: SXM 80 GB + form_factor: sxm + memory: + type: dedicated + nominal: { value: 80, unit: GB } + backend: cuda + partitionable: true + partitions: + - id: 1g.10gb + label: 1g.10gb + memory: { value: 10, unit: GB } + - id: 2g.20gb + label: 2g.20gb + memory: { value: 20, unit: GB } + - id: 3g.40gb + label: 3g.40gb + memory: { value: 40, unit: GB } + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [h100] + sources: + - url: https://www.nvidia.com/en-us/data-center/h100/ + checked: 2026-10-19 + notes: H100 SXM; MIG profiles must be selected explicitly and never inherit the full 80 GB. + + h100-nvl-94gb: + name: NVIDIA H100 NVL 94 GB + vendor: nvidia + kind: gpu + family: nvidia-h100 + variant: NVL 94 GB + form_factor: pcie + memory: + type: dedicated + nominal: { value: 94, unit: GB } + backend: cuda + partitionable: true + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [h100-nvl] + sources: + - url: https://www.nvidia.com/en-us/data-center/h100/ + checked: 2026-10-19 + - url: https://www.nvidia.com/en-us/data-center/technologies/hopper-architecture/ + checked: 2026-10-19 + + h200-sxm-141gb: + name: NVIDIA H200 SXM 141 GB + vendor: nvidia + kind: gpu + family: nvidia-h200 + variant: SXM 141 GB + form_factor: sxm + memory: + type: dedicated + nominal: { value: 141, unit: GB } + backend: cuda + partitionable: true + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [h200] + sources: + - url: https://www.nvidia.com/en-us/data-center/h200/ + checked: 2026-10-19 + - url: https://www.nvidia.com/en-us/data-center/technologies/hopper-architecture/ + checked: 2026-10-19 + + b200-180gb: + name: NVIDIA B200 180 GB + vendor: nvidia + kind: gpu + family: nvidia-b200 + variant: SXM 180 GB + form_factor: sxm + memory: + type: dedicated + nominal: { value: 180, unit: GB } + backend: cuda + partitionable: true + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [b200] + sources: + - url: https://www.nvidia.com/en-us/data-center/dgx-b200/ + checked: 2026-10-19 + - url: https://www.nvidia.com/en-us/data-center/technologies/blackwell-architecture/ + checked: 2026-10-19 + notes: DGX B200 lists 1,440 GB HBM3e across eight GPUs; this record is per GPU. + + gb200-186gb: + name: NVIDIA GB200 Blackwell GPU 186 GB + vendor: nvidia + kind: gpu + family: nvidia-gb200 + variant: Grace Blackwell Superchip GPU 186 GB + form_factor: sxm + memory: + type: dedicated + nominal: { value: 186, unit: GB } + backend: cuda + partitionable: true + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [gb200] + sources: + - url: https://www.nvidia.com/en-us/data-center/gb200-nvl72/ + checked: 2026-10-19 + - url: https://www.nvidia.com/en-us/data-center/technologies/blackwell-architecture/ + checked: 2026-10-19 + notes: >- + The GB200 Grace Blackwell Superchip table lists 372 GB HBM3E for its two + GPUs; this record is one GPU. Never pool the two. + + a100-sxm-80gb: + name: NVIDIA A100 SXM 80 GB + vendor: nvidia + kind: gpu + family: nvidia-a100 + variant: SXM 80 GB + form_factor: sxm + memory: + type: dedicated + nominal: { value: 80, unit: GB } + backend: cuda + partitionable: true + partitions: + - id: 1g.10gb + label: 1g.10gb + memory: { value: 10, unit: GB } + - id: 2g.20gb + label: 2g.20gb + memory: { value: 20, unit: GB } + - id: 3g.40gb + label: 3g.40gb + memory: { value: 40, unit: GB } + - id: 7g.80gb + label: 7g.80gb + memory: { value: 80, unit: GB } + supported_dtypes: [fp32, fp16, bf16] + aliases: [a100] + sources: + - url: https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/a100/pdf/a100-80gb-datasheet-update-nvidia-us-1521051-r2-web.pdf + checked: 2026-10-19 + - url: https://docs.nvidia.com/datacenter/tesla/mig-user-guide/latest/supported-mig-profiles.html + checked: 2026-10-19 + + l40s-48gb: + name: NVIDIA L40S 48 GB + vendor: nvidia + kind: gpu + family: nvidia-l40s + variant: PCIe 48 GB + form_factor: pcie + memory: + type: dedicated + nominal: { value: 48, unit: GB } backend: cuda supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [l40s] sources: - - https://www.nvidia.com/en-us/data-center/h100/ - notes: H100 SXM variant; the separate H100 NVL variant has a different capacity. + - url: https://www.nvidia.com/en-us/data-center/l40s/ + checked: 2026-10-19 - geforce-rtx-5090-32gb: - name: NVIDIA GeForce RTX 5090 32 GB (desktop) - ui_id: rtx5090 - memory_type: dedicated - memory_gb: 32 + l4-24gb: + name: NVIDIA L4 24 GB + vendor: nvidia + kind: gpu + family: nvidia-l4 + variant: PCIe 24 GB + form_factor: pcie + memory: + type: dedicated + nominal: { value: 24, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [l4] + sources: + - url: https://www.nvidia.com/en-us/data-center/l4/ + checked: 2026-10-19 + + rtx-5090-32gb: + name: NVIDIA GeForce RTX 5090 32 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-5090 + variant: Desktop 32 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 32, unit: GB } backend: cuda supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [rtx5090, rtx-5090, "5090"] sources: - - https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/ - - https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf - - https://developer.nvidia.com/cuda/gpus - - https://docs.nvidia.com/cuda/archive/13.1.1/parallel-thread-execution/index.html#warp-level-matrix-instructions-mma + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-5080-16gb: + name: NVIDIA GeForce RTX 5080 16 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-5080 + variant: Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [rtx5080, rtx-5080, "5080"] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5080/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-5070-ti-16gb: + name: NVIDIA GeForce RTX 5070 Ti 16 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-5070-ti + variant: Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [rtx5070ti, rtx-5070-ti, 5070-ti] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5070-family/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-5070-12gb: + name: NVIDIA GeForce RTX 5070 12 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-5070 + variant: Desktop 12 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 12, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [rtx5070, rtx-5070, "5070"] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5070-family/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-5060-ti-16gb: + name: NVIDIA GeForce RTX 5060 Ti 16 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-5060-ti + variant: Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [rtx5060ti-16, rtx-5060-ti-16, 5060-ti-16] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5060-family/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-5060-ti-8gb: + name: NVIDIA GeForce RTX 5060 Ti 8 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-5060-ti + variant: Desktop 8 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 8, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [rtx5060ti-8, rtx-5060-ti-8, 5060-ti-8] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5060-family/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-4090-24gb: + name: NVIDIA GeForce RTX 4090 24 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-4090 + variant: Desktop 24 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 24, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx4090, rtx-4090, "4090"] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/rtx-4090/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-4080-super-16gb: + name: NVIDIA GeForce RTX 4080 Super 16 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-4080 + variant: Super Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx4080super, rtx-4080-super, 4080-super] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/rtx-4080/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-4070-ti-super-16gb: + name: NVIDIA GeForce RTX 4070 Ti Super 16 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-4070-ti + variant: Super Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx4070tisuper, rtx-4070-ti-super, 4070-ti-super] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/rtx-4070-family/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-4060-ti-16gb: + name: NVIDIA GeForce RTX 4060 Ti 16 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-4060-ti + variant: Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx4060ti-16, rtx-4060-ti-16, 4060-ti-16] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-4060-ti-8gb: + name: NVIDIA GeForce RTX 4060 Ti 8 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-4060-ti + variant: Desktop 8 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 8, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx4060ti-8, rtx-4060-ti-8, 4060-ti-8] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/40-series/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-3090-24gb: + name: NVIDIA GeForce RTX 3090 24 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-3090 + variant: Desktop 24 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 24, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16] + aliases: [rtx3090, rtx-3090, "3090"] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3090/ + checked: 2026-10-19 + + rtx-3080-12gb: + name: NVIDIA GeForce RTX 3080 12 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-3080 + variant: Desktop 12 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 12, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16] + aliases: [rtx3080-12, rtx-3080-12, 3080-12] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3080/ + checked: 2026-10-19 + + rtx-3080-10gb: + name: NVIDIA GeForce RTX 3080 10 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-3080 + variant: Desktop 10 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 10, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16] + aliases: [rtx3080-10, rtx-3080-10, 3080-10] + sources: + - url: https://www.nvidia.com/en-us/geforce/graphics-cards/30-series/rtx-3080/ + checked: 2026-10-19 rtx-pro-6000-blackwell-workstation-96gb: name: NVIDIA RTX PRO 6000 Blackwell Workstation Edition 96 GB - ui_id: rtx - memory_type: dedicated - memory_gb: 96 + vendor: nvidia + kind: gpu + family: nvidia-rtx-pro-6000-blackwell + variant: Workstation Edition 96 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 96, unit: GB } backend: cuda supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [rtx, rtx-pro-6000, rtx-pro-6000-blackwell] sources: - - https://www.nvidia.com/en-us/products/workstations/professional-desktop-gpus/rtx-pro-6000/ - - https://www.nvidia.com/content/dam/en-zz/Solutions/design-visualization/quadro-product-literature/NVIDIA-RTX-Blackwell-PRO-GPU-Architecture-v1.0.pdf - - https://developer.nvidia.com/cuda/gpus - - https://docs.nvidia.com/cuda/archive/13.1.1/parallel-thread-execution/index.html#warp-level-matrix-instructions-mma - notes: Workstation Edition variant with dedicated GDDR7 memory. + - url: https://www.nvidia.com/en-us/products/workstations/professional-desktop-gpus/rtx-pro-6000/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/blackwell/nvidia-rtx-blackwell-gpu-architecture.pdf + checked: 2026-10-19 + notes: Workstation Edition; the Max-Q and Server Edition are separate variants. + + rtx-6000-ada-48gb: + name: NVIDIA RTX 6000 Ada Generation 48 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-6000-ada + variant: Desktop 48 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 48, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx-6000-ada, rtx6000ada] + sources: + - url: https://www.nvidia.com/en-us/products/workstations/rtx-6000/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-a6000-48gb: + name: NVIDIA RTX A6000 48 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-a6000 + variant: Desktop 48 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 48, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16] + aliases: [rtx-a6000, rtxa6000] + sources: + - url: https://www.nvidia.com/en-us/design-visualization/rtx-a6000/ + checked: 2026-10-19 + + rtx-a5000-24gb: + name: NVIDIA RTX A5000 24 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-a5000 + variant: Desktop 24 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 24, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16] + aliases: [rtx-a5000, rtxa5000] + sources: + - url: https://www.nvidia.com/en-us/design-visualization/rtx-a5000/ + checked: 2026-10-19 + + rtx-a4000-16gb: + name: NVIDIA RTX A4000 16 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-a4000 + variant: Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16] + aliases: [rtx-a4000, rtxa4000] + sources: + - url: https://www.nvidia.com/en-us/design-visualization/rtx-a4000/ + checked: 2026-10-19 + + rtx-5000-ada-32gb: + name: NVIDIA RTX 5000 Ada Generation 32 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-5000-ada + variant: Desktop 32 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 32, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx-5000-ada, rtx5000ada] + sources: + - url: https://www.nvidia.com/en-us/products/workstations/rtx-5000/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + rtx-4000-ada-20gb: + name: NVIDIA RTX 4000 Ada Generation 20 GB + vendor: nvidia + kind: gpu + family: nvidia-rtx-4000-ada + variant: Desktop 20 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 20, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rtx-4000-ada, rtx4000ada] + sources: + - url: https://www.nvidia.com/en-us/products/workstations/rtx-4000/ + checked: 2026-10-19 + - url: https://images.nvidia.com/aem-dam/Solutions/geforce/ada/nvidia-ada-gpu-architecture.pdf + checked: 2026-10-19 + + mi300x-192gb: + name: AMD Instinct MI300X 192 GB + vendor: amd + kind: accelerator + family: amd-instinct-mi300x + variant: OAM 192 GB + form_factor: oam + memory: + type: dedicated + nominal: { value: 192, unit: GB } + backend: rocm + partitionable: true + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [mi300x] + sources: + - url: https://www.amd.com/en/products/accelerators/instinct/mi300/mi300x.html + checked: 2026-10-19 + + mi325x-256gb: + name: AMD Instinct MI325X 256 GB + vendor: amd + kind: accelerator + family: amd-instinct-mi325x + variant: OAM 256 GB + form_factor: oam + memory: + type: dedicated + nominal: { value: 256, unit: GB } + backend: rocm + partitionable: true + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [mi325x] + sources: + - url: https://www.amd.com/en/products/accelerators/instinct/mi300/mi325x.html + checked: 2026-10-19 + + mi355x-288gb: + name: AMD Instinct MI355X 288 GB + vendor: amd + kind: accelerator + family: amd-instinct-mi355x + variant: OAM 288 GB + form_factor: oam + memory: + type: dedicated + nominal: { value: 288, unit: GB } + backend: rocm + partitionable: true + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [mi355x] + sources: + - url: https://www.amd.com/en/products/accelerators/instinct/mi350/mi355x.html + checked: 2026-10-19 + + radeon-rx-7900-xtx-24gb: + name: AMD Radeon RX 7900 XTX 24 GB + vendor: amd + kind: gpu + family: amd-radeon-rx-7900-xtx + variant: Desktop 24 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 24, unit: GB } + backend: rocm + supported_dtypes: [fp32, fp16, bf16] + aliases: [rx7900xtx, rx-7900-xtx, 7900-xtx] + sources: + - url: https://www.amd.com/en/products/graphics/desktops/radeon/7000-series/amd-radeon-rx-7900xtx.html + checked: 2026-10-19 + + radeon-rx-9070-xt-16gb: + name: AMD Radeon RX 9070 XT 16 GB + vendor: amd + kind: gpu + family: amd-radeon-rx-9070-xt + variant: Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: rocm + supported_dtypes: [fp32, fp16, bf16, fp8] + aliases: [rx9070xt, rx-9070-xt, 9070-xt] + sources: + - url: https://www.amd.com/en/products/graphics/desktops/radeon/9000-series/amd-radeon-rx-9070xt.html + checked: 2026-10-19 + + radeon-pro-w7900-48gb: + name: AMD Radeon PRO W7900 48 GB + vendor: amd + kind: gpu + family: amd-radeon-pro-w7900 + variant: Workstation 48 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 48, unit: GB } + backend: rocm + supported_dtypes: [fp32, fp16, bf16] + aliases: [w7900, radeon-pro-w7900] + sources: + - url: https://www.amd.com/en/products/graphics/workstations/radeon-pro/w7900.html + checked: 2026-10-19 + + intel-arc-a770-16gb: + name: Intel Arc A770 16 GB + vendor: intel + kind: gpu + family: intel-arc-a770 + variant: Desktop 16 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 16, unit: GB } + backend: sycl + supported_dtypes: [fp32, fp16, bf16] + aliases: [arc-a770, a770] + sources: + - url: https://www.intel.com/content/www/us/en/products/sku/227956/intel-arc-a770-graphics-16gb/specifications.html + checked: 2026-10-19 + + intel-arc-b580-12gb: + name: Intel Arc B580 12 GB + vendor: intel + kind: gpu + family: intel-arc-b580 + variant: Desktop 12 GB + form_factor: desktop + memory: + type: dedicated + nominal: { value: 12, unit: GB } + backend: sycl + supported_dtypes: [fp32, fp16, bf16] + aliases: [arc-b580, b580] + sources: + - url: https://www.intel.com/content/www/us/en/products/sku/241643/intel-arc-b580-graphics/specifications.html + checked: 2026-10-19 + + intel-data-center-gpu-max-1550-128gb: + name: Intel Data Center GPU Max 1550 128 GB + vendor: intel + kind: accelerator + family: intel-data-center-gpu-max-1550 + variant: OAM 128 GB + form_factor: oam + memory: + type: dedicated + nominal: { value: 128, unit: GB } + backend: sycl + supported_dtypes: [fp32, fp16, bf16] + aliases: [max-1550, data-center-gpu-max-1550] + sources: + - url: https://www.intel.com/content/www/us/en/products/sku/232876/intel-data-center-gpu-max-1550/specifications.html + checked: 2026-10-19 + + apple-m1-pro-16gb: + name: Apple M1 Pro 16 GB + vendor: apple + kind: soc + family: apple-m1-pro + variant: 16 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 16, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m1-pro-16, m1pro] + sources: + - url: https://www.apple.com/newsroom/2021/10/introducing-m1-pro-and-m1-max-the-most-powerful-chips-apple-has-ever-built/ + checked: 2026-10-19 + notes: Unified memory is shared with the CPU and OS; 16 GB is not dedicated VRAM. + + apple-m2-8gb: + name: Apple M2 8 GB + vendor: apple + kind: soc + family: apple-m2 + variant: 8 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 8, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m2-8, m2] + sources: + - url: https://www.apple.com/newsroom/2022/06/apple-unveils-m2-with-breakthrough-performance-and-capability/ + checked: 2026-10-19 + notes: Unified memory is shared with the CPU and OS; 8 GB is not dedicated VRAM. + + apple-m4-max-36gb: + name: Apple M4 Max 36 GB + vendor: apple + kind: soc + family: apple-m4-max + variant: 36 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 36, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [apple, m4-max-36] + sources: + - url: https://support.apple.com/en-us/121553 + checked: 2026-10-19 + notes: Unified memory is shared with the CPU and OS; 36 GB is not dedicated VRAM. + + apple-m4-max-48gb: + name: Apple M4 Max 48 GB + vendor: apple + kind: soc + family: apple-m4-max + variant: 48 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 48, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m4-max-48] + sources: + - url: https://support.apple.com/en-us/121553 + checked: 2026-10-19 + + apple-m4-max-64gb: + name: Apple M4 Max 64 GB + vendor: apple + kind: soc + family: apple-m4-max + variant: 64 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 64, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m4-max-64] + sources: + - url: https://support.apple.com/en-us/121553 + checked: 2026-10-19 + + apple-m4-max-128gb: + name: Apple M4 Max 128 GB + vendor: apple + kind: soc + family: apple-m4-max + variant: 128 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 128, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m4-max-128] + sources: + - url: https://support.apple.com/en-us/121553 + checked: 2026-10-19 + + apple-m5-max-48gb: + name: Apple M5 Max 48 GB + vendor: apple + kind: soc + family: apple-m5-max + variant: 48 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 48, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m5-max-48] + sources: + - url: https://www.apple.com/macbook-pro/specs/ + checked: 2026-10-19 + + apple-m5-max-64gb: + name: Apple M5 Max 64 GB + vendor: apple + kind: soc + family: apple-m5-max + variant: 64 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 64, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m5-max-64] + sources: + - url: https://www.apple.com/macbook-pro/specs/ + checked: 2026-10-19 + + apple-m5-max-128gb: + name: Apple M5 Max 128 GB + vendor: apple + kind: soc + family: apple-m5-max + variant: 128 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 128, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m5-max-128] + sources: + - url: https://www.apple.com/macbook-pro/specs/ + checked: 2026-10-19 + + apple-m5-ultra-96gb: + name: Apple M5 Ultra 96 GB + vendor: apple + kind: soc + family: apple-m5-ultra + variant: 96 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 96, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m5-ultra-96] + sources: + - url: https://www.apple.com/mac-studio/specs/ + checked: 2026-10-19 + + apple-m5-ultra-256gb: + name: Apple M5 Ultra 256 GB + vendor: apple + kind: soc + family: apple-m5-ultra + variant: 256 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 256, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m5-ultra-256] + sources: + - url: https://www.apple.com/mac-studio/specs/ + checked: 2026-10-19 + + apple-m5-ultra-512gb: + name: Apple M5 Ultra 512 GB + vendor: apple + kind: soc + family: apple-m5-ultra + variant: 512 GB unified + form_factor: soc + memory: + type: unified + nominal: { value: 512, unit: GB } + backend: metal + supported_dtypes: [fp32, fp16, bf16] + aliases: [m5-ultra-512] + sources: + - url: https://www.apple.com/mac-studio/specs/ + checked: 2026-10-19 dgx-spark-gb10-128gb: name: NVIDIA DGX Spark GB10 128 GB - ui_id: spark - memory_type: unified - memory_gb: 128 + vendor: nvidia + kind: system + family: nvidia-dgx-spark-gb10 + variant: 128 GB unified + form_factor: system + memory: + type: unified + nominal: { value: 128, unit: GB } backend: cuda supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [spark, dgx-spark] sources: - - https://www.nvidia.com/en-us/products/workstations/dgx-spark/ - - https://developer.nvidia.com/cuda/gpus - - https://docs.nvidia.com/cuda/archive/13.0.2/cuda-toolkit-release-notes/index.html#cublas-release-13-0-update-2 - - https://docs.nvidia.com/cuda/archive/13.1.1/parallel-thread-execution/index.html#warp-level-matrix-instructions-mma - notes: >- - One 128 GB system with coherent memory shared by the CPU and GPU. - This entry does not describe the separate 64 GB OEM configuration or - the aggregate memory of a multi-system deployment. + - url: https://www.nvidia.com/en-us/products/workstations/dgx-spark/ + checked: 2026-10-19 + notes: One GB10 system with coherent unified memory shared by CPU and GPU. + + dgx-spark-gb10-64gb: + name: NVIDIA DGX Spark GB10 64 GB + vendor: nvidia + kind: system + family: nvidia-dgx-spark-gb10 + variant: 64 GB unified OEM + form_factor: system + memory: + type: unified + nominal: { value: 64, unit: GB } + backend: cuda + supported_dtypes: [fp32, fp16, bf16, fp8, nvfp4] + aliases: [spark-64] + sources: + - url: https://www.nvidia.com/en-us/products/workstations/dgx-spark/ + checked: 2026-10-19 + notes: OEM-only 64 GB configuration; keep separate from the 128 GB system. diff --git a/docs/cookbook/design.md b/docs/cookbook/design.md index 1473d4bb7..26b027fc4 100644 --- a/docs/cookbook/design.md +++ b/docs/cookbook/design.md @@ -74,9 +74,10 @@ to documentation, not embedded HTML. ## Shared JavaScript API -`docs/js/cookbook-config.mjs` exports the six functions below. The generated -`assets/cookbook-config/cookbook-config.js` bundles them with Ajv and exposes -the browser global `FastVideoConfigCookbook` and CommonJS exports: +`docs/js/cookbook-config.mjs` exports the recipe, capacity, and assessment +helpers below. The generated `assets/cookbook-config/cookbook-config.js` bundles +them with Ajv and exposes the browser global `FastVideoConfigCookbook` and +CommonJS exports: | Function | Result | | --- | --- | @@ -85,12 +86,22 @@ the browser global `FastVideoConfigCookbook` and CommonJS exports: | `getOptions(catalog, config = catalog.base_config, edits = {})` | `[{path, schema, value}]`, including GPU-dependent degree limits | | `resolveConfig(catalog, edits = {})` | `config`, `yaml`, `argv`, `command`, `clientRequest`, `clientCommand`, optional `websocketUrl` | | `sampleRequest(metadata, config)` | `{body, command}` and optional `websocketUrl` for a native serving baseline | -| `estimateMemory({config, workload, model, hardware, headroom})` | Analytical one-GPU memory estimate; see [memory estimates](../contributing/cookbook_configuration.md#memory-estimates) | +| `WORKLOADS` | Frozen `t2v`/`i2v`/`t2i`/`t2a`/`v2a` input/output/endpoint/media metadata used by the builder and sample client | +| `estimateMemory({config, workload, model, hardware, memoryBudgetGiB, partitionId, hostMemoryGb, headroom})` | Analytical one-device estimate with separate device and host budgets | +| `assessCapacity({model, example, hardware, config, request, env, revisions, ...})` | `{status, basis, analytical, evidence, compatibility, notes}` combining analysis and applicable evidence | +| `matchEvidence({model, example, config, request, env, hardware, revisions})` | `{applicable, unverified, rejected}` measured runs | +| `recommendCapacity({models, hardware, task, request, env, revisions, limit})` | Candidates with model/example, assessment, reasons, and lower-memory alternatives; unknown remains explicit | +| `resolveExecution({model, config, env})` | Effective topology, offload, precision, attention, and unsupported switches | +| `loadCapacityIndex(url, options)` | Promise of the model-summary index and resolved source URL | +| `loadCapacityModel(index, id, options)` | Promise of one expanded model, examples, and reference runs | +| `loadCapacityHardware(index, options)` | Promise of `{version, devices, url}` for the source-backed hardware lookup | `sampleRequest()` is also used by model pages backed by a checked-in serving YAML rather than a full editable catalog. Its metadata supplies `model.id`, `workload`, and `runtime.interface`/`runtime.server_defaults`. It builds client instructions only; it does not validate the configuration or run a request. +The shared workload map selects the canonical REST endpoint and output +extension; it does not establish that a model supports that workload. For WebSocket deployments, `clientRequest` is `null` and `websocketUrl` is present. REST deployments return a request object and omit `websocketUrl`. @@ -217,9 +228,12 @@ shared hardware/component facts and optional recorded runs through separate static loaders. Optional `capacity` sections extend existing model manifests; model identity and example configurations are derived from their deployments and native baseline YAML. `estimateMemory()` is analytical and never verifies a -recipe. Measured-hardware UI, format versioning and HTTP cache freshness remain -separate follow-ups. No general arithmetic validator or CLI override workflow is -introduced here. +recipe. `assessCapacity()` keeps analytical fit, measured device peaks, and +applicable evidence distinct; allocator-only peaks and successful generations +do not establish device fit or a minimum-VRAM requirement. The hardware asset is +versioned and source-backed; the UI owns catalog search and the custom-device +form, while the shared API owns lookup, normalization, and estimation. No general +arithmetic validator or CLI override workflow is introduced here. ## Cookbook interface diff --git a/docs/cookbook/options.yaml b/docs/cookbook/options.yaml index 4f51ce35c..627ac1e79 100644 --- a/docs/cookbook/options.yaml +++ b/docs/cookbook/options.yaml @@ -97,6 +97,36 @@ runtimes: title: Compile transformer type: boolean default: false + # MiniMax-H3 only (fastvideo_args.video_decode_backend). TAEH3 fetches its own SHA-pinned weights. + generator.pipeline.experimental.video_decode_backend: + title: Video decoder + description: >- + h3-vae is the full decoder. taeh3 is the tiny preview decoder: faster and lighter, lower quality, + text-to-video only. + type: string + enum: [h3-vae, taeh3] + default: h3-vae + # MiniMax-H3 only (fastvideo_args.h3_vae_nvfp4_decoder). Files come from FastVideo/FastH3-NVFP4-VAE-Decoder. + generator.pipeline.experimental.h3_vae_nvfp4_decoder: + title: NVFP4 video decoder + description: >- + Replaces the video decoder with an NVFP4 decoder on Blackwell GPUs. The 36-block quality tier needs a + full-depth VAE; the 8-block fast tier works with any H3 VAE. + type: [string, "null"] + enum: + - null + - ./fasth3-nvfp4-vae/quality-36L/decoder-nvfp4.safetensors + - ./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors + default: null + # A local text-encoder folder; a serialized NVFP4 encoder selects its quantization from its own config. + generator.pipeline.components.text_encoder_weights: + title: Text encoder + description: Loads a different text-encoder checkpoint. The NVFP4 encoder needs a Blackwell GPU. + type: [string, "null"] + enum: + - null + - ./fasth3-nvfp4-encoder/text_encoder + default: null default_request.sampling.num_frames: title: Frames type: integer diff --git a/docs/cookbook/recipes/fasth3-4step.yaml b/docs/cookbook/recipes/fasth3-4step.yaml new file mode 100644 index 000000000..1be84c090 --- /dev/null +++ b/docs/cookbook/recipes/fasth3-4step.yaml @@ -0,0 +1,47 @@ +title: FastH3 4-Step V1 +summary: Four-step video and audio on one DGX Spark, with lazy module loading and offloaded encoders. +guide: docs/getting_started/installation/spark.md +deployments: + spark-rest: + label: DGX Spark REST + runtime: fastvideo-cuda-rest + workload: t2v + defaults: examples/serving/openai_fasth3_spark.yaml + env: + FASTVIDEO_VSA_PTX_BLACKWELL: "0" + FASTVIDEO_FA4: "0" + FASTVIDEO_ATTENTION_BACKEND: VIDEO_SPARSE_ATTN_H3 + requirements: + - Install FastVideo using the DGX Spark guide before running these commands. + - GB10 has no FA4 or ptx_blackwell VSA kernel; the environment above selects the Triton path. + - A 345-frame request on one Spark can run out of memory. Prefer 124 or 243 frames. + - For two Sparks, use the inference recipe examples/inference/basic/basic_fasth3_spark_pair.yaml; it has no serving baseline yet. + - "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae" + - "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder" + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.tp_size + - generator.engine.parallelism.sp_size + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - generator.pipeline.experimental.video_decode_backend + - generator.pipeline.experimental.h3_vae_nvfp4_decoder + - generator.pipeline.components.text_encoder_weights + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.seed + overrides: + default_request.sampling.num_frames: + enum: [124, 243, 345] + default: 124 + description: Native H3 frame counts. 345 frames can exceed one Spark's memory. + default_request.sampling.height: + minimum: 32 + multipleOf: 32 + default: 768 + default_request.sampling.width: + minimum: 32 + multipleOf: 32 + default: 1344 diff --git a/docs/cookbook/recipes/fasth3-8step.yaml b/docs/cookbook/recipes/fasth3-8step.yaml index 90be2fe06..c014c4987 100644 --- a/docs/cookbook/recipes/fasth3-8step.yaml +++ b/docs/cookbook/recipes/fasth3-8step.yaml @@ -13,6 +13,8 @@ deployments: # Existing FastH3 V2 installation command: docs/cookbook/openai-api.md. requirements: - 'UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"' + - "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae" + - "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder" options: - server.* - generator.engine.num_gpus @@ -20,6 +22,9 @@ deployments: - "!generator.engine.parallelism.hsdp_*" - generator.engine.offload.text_encoder - generator.engine.offload.vae + - generator.pipeline.experimental.video_decode_backend + - generator.pipeline.experimental.h3_vae_nvfp4_decoder + - generator.pipeline.components.text_encoder_weights - default_request.sampling.num_frames - default_request.sampling.height - default_request.sampling.width diff --git a/docs/cookbook/recipes/fasth3-omniref.yaml b/docs/cookbook/recipes/fasth3-omniref.yaml new file mode 100644 index 000000000..53bf13793 --- /dev/null +++ b/docs/cookbook/recipes/fasth3-omniref.yaml @@ -0,0 +1,141 @@ +title: FastH3 OmniRef +summary: Video and synchronized audio from a prompt plus ordered image, video, and audio references, in eight forwards. +guide: docs/inference/fasth3_omniref.md +deployments: + rtx5090-rest: + label: RTX 5090 REST + runtime: fastvideo-cuda-rest + workload: i2v + defaults: examples/serving/openai_fasth3_omniref_rtx5090.yaml + env: &omniref_env + FASTVIDEO_ATTENTION_BACKEND: VIDEO_SPARSE_ATTN_H3 + FASTVIDEO_H3_VSA_FP4: "1" + FASTVIDEO_H3_VSA_FP4_PREFIX: fp8 + FASTVIDEO_H3_VSA_FP4_TWO_LEVEL_P: "1" + FASTVIDEO_H3_VSA_FP4_K_SMOOTH: "1" + FASTVIDEO_H3_GATE_CHUNK_ROWS: "8192" + FASTVIDEO_ULYSSES_A2A: auto + FASTVIDEO_MINIMAX_H3_EXACT_KERNELS: all + FASTVIDEO_H3_VSA_HEADS_FIRST_TILE: "1" + FASTVIDEO_H3_VAE_TILE_PARALLEL: "1" + FASTVIDEO_H3_REF2VA_MEMO_ENTRIES: "16" + FASTVIDEO_MINIMAX_H3_FUSIONS: all + FASTVIDEO_VSA_TRITON: "1" + FASTVIDEO_VSA_PTX_BLACKWELL: "0" + FASTVIDEO_FA4: "0" + FASTVIDEO_H3_ADALN_TABLE: ./adaln_table.pt + FASTVIDEO_H3_VAE_TILE_BATCH: "4" + requirements: &omniref_requirements + - 'UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"' + - >- + Compose the model directory once: python examples/inference/basic/basic_fasth3_omniref_pdd.py + --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --composed-dir ./FastH3-OmniRef-Composed + --image assets/images/couple.jpg --prompt "a person speaking to camera" + - >- + The 32 GB card needs ./adaln_table.pt. Write it once with FASTVIDEO_H3_ADALN_DUMP=./adaln_table.pt + on a run that loads the AdaLN projections, such as the RTX PRO 6000 or DGX Spark profile. + - Requests carry ordered image_reference, video_reference, and audio references; list local media + directories in server.media_roots. + - Based on the maintained inference recipe; this serving adaptation has not been GPU-validated. + - "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae" + options: &omniref_options + - server.* + - generator.engine.offload.text_encoder + - generator.pipeline.experimental.h3_vae_nvfp4_decoder + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.seed + overrides: &omniref_overrides + default_request.sampling.num_frames: + enum: [124, 243, 362] + default: 124 + description: Five, ten, or fifteen seconds at 24 fps. + default_request.sampling.height: + minimum: 32 + multipleOf: 32 + default: 480 + default_request.sampling.width: + minimum: 32 + multipleOf: 32 + default: 832 + + rtx-pro6000-rest: + label: RTX PRO 6000 REST + runtime: fastvideo-cuda-rest + workload: i2v + defaults: examples/serving/openai_fasth3_omniref_rtx_pro6000.yaml + env: + FASTVIDEO_ATTENTION_BACKEND: VIDEO_SPARSE_ATTN_H3 + FASTVIDEO_H3_VSA_FP4: "1" + FASTVIDEO_H3_VSA_FP4_PREFIX: fp8 + FASTVIDEO_H3_VSA_FP4_TWO_LEVEL_P: "1" + FASTVIDEO_H3_VSA_FP4_K_SMOOTH: "1" + FASTVIDEO_H3_GATE_CHUNK_ROWS: "8192" + FASTVIDEO_MINIMAX_H3_FUSIONS: all + FASTVIDEO_VSA_TRITON: "1" + FASTVIDEO_VSA_PTX_BLACKWELL: "0" + FASTVIDEO_FA4: "0" + FASTVIDEO_H3_VAE_TILE_BATCH: "8" + requirements: + - 'UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"' + - >- + Compose the model directory once: python examples/inference/basic/basic_fasth3_omniref_pdd.py + --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --composed-dir ./FastH3-OmniRef-Composed + --image assets/images/couple.jpg --prompt "a person speaking to camera" + - Requests carry ordered image_reference, video_reference, and audio references; list local media + directories in server.media_roots. + - Based on the maintained inference recipe; this serving adaptation has not been GPU-validated. + - "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae" + options: *omniref_options + overrides: + default_request.sampling.num_frames: + enum: [124, 243, 362] + default: 124 + description: Five, ten, or fifteen seconds at 24 fps. + default_request.sampling.height: + minimum: 32 + multipleOf: 32 + default: 768 + default_request.sampling.width: + minimum: 32 + multipleOf: 32 + default: 1344 + + spark-rest: + label: DGX Spark REST + runtime: fastvideo-cuda-rest + workload: i2v + defaults: examples/serving/openai_fasth3_omniref_spark.yaml + env: + FASTVIDEO_ATTENTION_BACKEND: VIDEO_SPARSE_ATTN_H3 + FASTVIDEO_H3_VSA_FP4: "1" + FASTVIDEO_H3_VSA_FP4_PREFIX: fp8 + FASTVIDEO_H3_VSA_FP4_TWO_LEVEL_P: "1" + FASTVIDEO_H3_VSA_FP4_K_SMOOTH: "1" + FASTVIDEO_H3_GATE_CHUNK_ROWS: "8192" + FASTVIDEO_ULYSSES_A2A: auto + FASTVIDEO_MINIMAX_H3_EXACT_KERNELS: all + FASTVIDEO_H3_VSA_HEADS_FIRST_TILE: "1" + FASTVIDEO_H3_VAE_TILE_PARALLEL: "1" + FASTVIDEO_H3_REF2VA_MEMO_ENTRIES: "16" + FASTVIDEO_MINIMAX_H3_FUSIONS: all + FASTVIDEO_VSA_TRITON: "1" + FASTVIDEO_VSA_PTX_BLACKWELL: "0" + FASTVIDEO_FA4: "0" + FASTVIDEO_NVFP4_MM_BACKEND: cudnn + FASTVIDEO_NVFP4_MM_ROW_SPLIT: table + FASTVIDEO_H3_VAE_TILE_BATCH: "12" + FASTVIDEO_H3_VAE_INT8_OVERLAY: "0" + requirements: + - Install FastVideo using the DGX Spark guide before running these commands. + - >- + Compose the model directory once: python examples/inference/basic/basic_fasth3_omniref_pdd.py + --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --composed-dir ./FastH3-OmniRef-Composed + --image assets/images/couple.jpg --prompt "a person speaking to camera" + - Requests carry ordered image_reference, video_reference, and audio references; list local media + directories in server.media_roots. + - Based on the maintained inference recipe; this serving adaptation has not been GPU-validated. + - "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae" + options: *omniref_options + overrides: *omniref_overrides diff --git a/docs/cookbook/recipes/fasth3-preview.yaml b/docs/cookbook/recipes/fasth3-preview.yaml new file mode 100644 index 000000000..3df4e110d --- /dev/null +++ b/docs/cookbook/recipes/fasth3-preview.yaml @@ -0,0 +1,64 @@ +title: FastH3 Preview V0.2 +summary: Few-step video and audio from the FastH3 preview checkpoint on CUDA or native Apple Silicon MLX. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + guide: docs/cookbook/openai-api.md + workload: t2v + defaults: examples/serving/openai_fasth3.yaml + requirements: + - 'UV_TORCH_BACKEND=cu130 uv pip install -e ".[fasth3]"' + - "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae" + - "Optional NVFP4 text encoder (Blackwell only): hf download FastVideo/FastVideo-FastH3-8-Step-V2-NVFP4-Consumer --include 'text_encoder/*' --local-dir fasth3-nvfp4-encoder" + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.tp_size + - generator.engine.parallelism.sp_size + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - generator.pipeline.experimental.video_decode_backend + - generator.pipeline.experimental.h3_vae_nvfp4_decoder + - generator.pipeline.components.text_encoder_weights + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.seed + overrides: + default_request.sampling.num_frames: + minimum: 108 + maximum: 362 + description: Configured frame counts align upward to the next 17n+5 value, up to 362. + default_request.sampling.height: + minimum: 32 + multipleOf: 32 + default: 768 + description: H3 canvas dimensions use multiples of 32; the runtime's canvas-area limit also applies. + default_request.sampling.width: + minimum: 32 + multipleOf: 32 + + mlx-rest: + runtime: fastvideo-mlx-rest + guide: docs/cookbook/openai-api.md + workload: t2v + defaults: examples/serving/mlx_fasth3.yaml + requirements: + - 'uv pip install -e ".[mlx]"' + - Download the preview checkpoint and convert its transformer to an MLX int6 export before starting the server. + - Paths are relative to the directory where you launch the server. + options: + - server.* + - generator.model_root + - generator.mlx_checkpoint + - generator.prompt_cache_dir + - generator.vae_dtype + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.seed + overrides: + generator.vae_dtype: + title: VAE compute precision + description: Precision used by the native MLX VAE; the serving baseline uses fp32. diff --git a/docs/cookbook/recipes/fasth3-trim.yaml b/docs/cookbook/recipes/fasth3-trim.yaml index 8ec8e242f..5e69ebacb 100644 --- a/docs/cookbook/recipes/fasth3-trim.yaml +++ b/docs/cookbook/recipes/fasth3-trim.yaml @@ -20,6 +20,7 @@ deployments: - Install FastVideo using the DGX Spark guide before running these commands. - Based on the released one-Spark inference recipe. This serving adaptation and edited hardware combinations have not been GPU-validated. - The checkpoint includes NVFP4 weights and encoder, a lightweight video VAE, and its trained eight-forward schedule. No local CompactH3 assembly is needed. + - "Optional NVFP4 decoder (Blackwell only): hf download FastVideo/FastH3-NVFP4-VAE-Decoder --include 'quality-36L/decoder-nvfp4.safetensors' 'fast-8L/decoder-nvfp4.safetensors' --local-dir fasth3-nvfp4-vae" options: - server.* - generator.engine.num_gpus @@ -27,12 +28,19 @@ deployments: - generator.engine.parallelism.sp_size - generator.engine.offload.text_encoder - generator.engine.offload.vae + - generator.pipeline.experimental.video_decode_backend + - generator.pipeline.experimental.h3_vae_nvfp4_decoder - default_request.prompt - default_request.sampling.num_frames - default_request.sampling.height - default_request.sampling.width - default_request.sampling.seed overrides: + generator.pipeline.experimental.h3_vae_nvfp4_decoder: + enum: + - null + - ./fasth3-nvfp4-vae/fast-8L/decoder-nvfp4.safetensors + description: The bundled VAE has 26 blocks, so only the 8-block fast tier fits. default_request.prompt: title: Prompt type: string diff --git a/docs/cookbook/recipes/hunyuan15-i2v-480p.yaml b/docs/cookbook/recipes/hunyuan15-i2v-480p.yaml new file mode 100644 index 000000000..8175d6e70 --- /dev/null +++ b/docs/cookbook/recipes/hunyuan15-i2v-480p.yaml @@ -0,0 +1,30 @@ +title: HunyuanVideo 1.5 I2V 480p +summary: First-frame-conditioned video generation with the step-distilled HunyuanVideo 1.5 model. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + workload: i2v + defaults: examples/serving/openai_hunyuan15_i2v_480p.yaml + requirements: + - >- + Each request must supply its own first frame through image_reference or + input_reference; the server does not define a default image. + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.* + - "!generator.engine.parallelism.hsdp_*" + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + overrides: + default_request.sampling.height: + minimum: 16 + multipleOf: 16 + default_request.sampling.width: + minimum: 16 + multipleOf: 16 diff --git a/docs/cookbook/recipes/hunyuan15-t2v-480p.yaml b/docs/cookbook/recipes/hunyuan15-t2v-480p.yaml new file mode 100644 index 000000000..f7bb91bd9 --- /dev/null +++ b/docs/cookbook/recipes/hunyuan15-t2v-480p.yaml @@ -0,0 +1,26 @@ +title: HunyuanVideo 1.5 T2V 480p +summary: Text-to-video generation at 480p with HunyuanVideo 1.5. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + workload: t2v + defaults: examples/serving/openai_hunyuan15_t2v_480p.yaml + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.* + - "!generator.engine.parallelism.hsdp_*" + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + overrides: + default_request.sampling.height: + minimum: 16 + multipleOf: 16 + default_request.sampling.width: + minimum: 16 + multipleOf: 16 diff --git a/docs/cookbook/recipes/kandinsky5-t2v.yaml b/docs/cookbook/recipes/kandinsky5-t2v.yaml new file mode 100644 index 000000000..3019d2d23 --- /dev/null +++ b/docs/cookbook/recipes/kandinsky5-t2v.yaml @@ -0,0 +1,26 @@ +title: Kandinsky 5 T2V Lite +summary: Text-to-video generation with the Kandinsky-5.0 Lite 5-second model. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + workload: t2v + defaults: examples/serving/openai_kandinsky5_t2v.yaml + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.* + - "!generator.engine.parallelism.hsdp_*" + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + overrides: + default_request.sampling.height: + minimum: 16 + multipleOf: 16 + default_request.sampling.width: + minimum: 16 + multipleOf: 16 diff --git a/docs/cookbook/recipes/kandinsky6-ti2va-distilled.yaml b/docs/cookbook/recipes/kandinsky6-ti2va-distilled.yaml new file mode 100644 index 000000000..9271c36f2 --- /dev/null +++ b/docs/cookbook/recipes/kandinsky6-ti2va-distilled.yaml @@ -0,0 +1,26 @@ +title: Kandinsky 6 TI2VA Distilled +summary: Ten-step pi-Flow Kandinsky 6 Pro video with synchronized audio. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + workload: t2v + defaults: examples/serving/openai_kandinsky6_ti2va_distilled.yaml + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.* + - "!generator.engine.parallelism.hsdp_*" + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + overrides: + default_request.sampling.height: + minimum: 16 + multipleOf: 16 + default_request.sampling.width: + minimum: 16 + multipleOf: 16 diff --git a/docs/cookbook/recipes/kandinsky6-ti2va.yaml b/docs/cookbook/recipes/kandinsky6-ti2va.yaml new file mode 100644 index 000000000..d121dc718 --- /dev/null +++ b/docs/cookbook/recipes/kandinsky6-ti2va.yaml @@ -0,0 +1,26 @@ +title: Kandinsky 6 TI2VA +summary: Text- or image-conditioned video with synchronized audio from Kandinsky 6 Pro. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + workload: t2v + defaults: examples/serving/openai_kandinsky6_ti2va.yaml + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.* + - "!generator.engine.parallelism.hsdp_*" + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + overrides: + default_request.sampling.height: + minimum: 16 + multipleOf: 16 + default_request.sampling.width: + minimum: 16 + multipleOf: 16 diff --git a/docs/cookbook/recipes/ltx2-distilled.yaml b/docs/cookbook/recipes/ltx2-distilled.yaml index 694a1763f..634633762 100644 --- a/docs/cookbook/recipes/ltx2-distilled.yaml +++ b/docs/cookbook/recipes/ltx2-distilled.yaml @@ -2,7 +2,7 @@ # selected options, schema types/ranges, and baseline values for each deployment. # TODO: Recheck field paths and choices when the typed API in PR #1913 is finalized. title: LTX2 Distilled -summary: Incremental video delivery over the native streaming WebSocket interface. +summary: LTX2 video generation with synchronized audio over REST or incremental WebSocket streaming. deployments: cuda-streaming: @@ -33,3 +33,28 @@ deployments: default_request.sampling.width: minimum: 64 multipleOf: 64 + + cuda-rest: + runtime: fastvideo-cuda-rest + workload: t2v + defaults: examples/serving/openai_ltx2_distilled.yaml + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.sp_size + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + - default_request.sampling.seed + overrides: + default_request.sampling.height: + minimum: 64 + multipleOf: 32 + default: 1024 + default_request.sampling.width: + minimum: 64 + multipleOf: 32 + default: 1536 diff --git a/docs/cookbook/recipes/wan21-t2v-14b.yaml b/docs/cookbook/recipes/wan21-t2v-14b.yaml new file mode 100644 index 000000000..f79dd950f --- /dev/null +++ b/docs/cookbook/recipes/wan21-t2v-14b.yaml @@ -0,0 +1,26 @@ +title: Wan2.1 T2V 14B +summary: Text-to-video generation with the Wan2.1 14B base model at 720p. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + workload: t2v + defaults: examples/serving/openai_wan21_t2v_14b.yaml + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.* + - "!generator.engine.parallelism.hsdp_*" + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + overrides: + default_request.sampling.height: + minimum: 16 + multipleOf: 16 + default_request.sampling.width: + minimum: 16 + multipleOf: 16 diff --git a/docs/cookbook/recipes/wan22-i2v-a14b.yaml b/docs/cookbook/recipes/wan22-i2v-a14b.yaml new file mode 100644 index 000000000..7f791ce55 --- /dev/null +++ b/docs/cookbook/recipes/wan22-i2v-a14b.yaml @@ -0,0 +1,30 @@ +title: Wan2.2 I2V A14B +summary: Image-conditioned video generation with the Wan2.2 mixture-of-experts model. + +deployments: + cuda-rest: + runtime: fastvideo-cuda-rest + workload: i2v + defaults: examples/serving/openai_wan22_i2v_a14b.yaml + requirements: + - >- + Each request must supply its own first frame through image_reference or + input_reference; the server does not define a default image. + options: + - server.* + - generator.engine.num_gpus + - generator.engine.parallelism.* + - "!generator.engine.parallelism.hsdp_*" + - generator.engine.offload.text_encoder + - generator.engine.offload.vae + - default_request.sampling.num_frames + - default_request.sampling.height + - default_request.sampling.width + - default_request.sampling.fps + overrides: + default_request.sampling.height: + minimum: 16 + multipleOf: 16 + default_request.sampling.width: + minimum: 16 + multipleOf: 16 diff --git a/docs/cookbook/serving-coverage.md b/docs/cookbook/serving-coverage.md new file mode 100644 index 000000000..23280315f --- /dev/null +++ b/docs/cookbook/serving-coverage.md @@ -0,0 +1,134 @@ +# Serving coverage and hardware evidence + +This ledger tracks what the repository can actually launch today. A checked-in +configuration means the model path, workload and request fields resolve; it does +not mean the recipe has been run on a particular GPU. Hardware catalog entries +are owned by the shared cookbook catalog, not by this page. + +Last source audit: 2026-10-19. + +## Runtime surface + +| Runtime | Interface | Workloads accepted by the server | Output | +| --- | --- | --- | --- | +| `fastvideo-cuda-rest` | OpenAI-compatible REST | `t2v`, `i2v`; `t2i` through `/v1/images`; `t2a`, `v2a` through the media-job routes | `.mp4`, `.png`, `.wav` | +| `fastvideo-cuda-streaming` | WebSocket streaming | `t2v` | fragmented MP4 | +| `fastvideo-mlx-rest` | MLX REST | `t2v` | `.mp4` | + +Audio uses `/v1/videos` and `/v1/videos/sync` because the job store and polling +contract are already shared. The response `media_type` is `audio/wav` and the +downloaded file is WAV; clients must branch on `media_type`, not on the endpoint +name. There is no separate audio job store. + +Media references are limited by the serve config. Local paths must sit under +`server.media_roots` (empty by default), and private or loopback URLs need +`server.allow_private_media_urls`. The server downloads at most +`server.media_reference_max_bytes` (default 2 GiB) per reference and removes +its copy when the job finishes, fails, or is cancelled; caller-owned files are +never deleted. See `examples/serving/README.md`. + +## Maintained serving baselines + +| Baseline | Model | Workload | Runtime | Cookbook deployment | Evidence | +| --- | --- | --- | --- | --- | --- | +| `openai_fasth3.yaml` | FastH3 Preview v0.2 | `t2v` | CUDA REST | none (legacy catalog only) | Source-backed; H100/GB200 benchmark notes exist, not a per-GPU fit claim | +| `openai_fasth3_8step.yaml` | FastH3 8-Step V2 | `t2v` | CUDA REST | `fasth3-8step/cuda-rest` | Source-backed | +| `openai_fasth3_lora.yaml` | MiniMax-H3 + LoRA | `t2v` | CUDA REST | none | Source-backed | +| `openai_fasth3_spark.yaml` | FastH3 V1 | `t2v` | CUDA REST | none (legacy catalog only) | Source-backed; one GB10 | +| `openai_fasth3_trim_spark.yaml` | FastH3 Trim | `t2v` | CUDA REST | `fasth3-trim/cuda-rest` | Source-backed; baseline says not GPU-validated | +| `openai_compacth3_rtx5090.yaml` | CompactH3 NVFP4 | `t2v` | CUDA REST | none | Source-backed; one RTX 5090 | +| `openai_compacth3_rtx_pro6000.yaml` | CompactH3 NVFP4 | `t2v` | CUDA REST | none | Source-backed; one RTX PRO 6000 | +| `openai_fastwan21_1_3b.yaml` | FastWan2.1 1.3B | `t2v` | CUDA REST | `fastwan21` | Source-backed | +| `openai_wan21_i2v_14b.yaml` | Wan2.1 I2V 14B | `i2v` | CUDA REST | `wan21-i2v` | Source-backed | +| `openai_wan21_t2v_14b.yaml` | Wan2.1 T2V 14B | `t2v` | CUDA REST | `wan21-t2v-14b` | Source-backed; 720p base model | +| `openai_wan22_t2v_a14b.yaml` | Wan2.2 T2V A14B | `t2v` | CUDA REST | none | Source-backed | +| `openai_wan22_i2v_a14b.yaml` | Wan2.2 I2V A14B | `i2v` | CUDA REST | `wan22-i2v-a14b` | Source-backed | +| `openai_wan22_ti2v_5b.yaml` | Wan2.2 TI2V 5B | `t2v` | CUDA REST | none | Source-backed | +| `openai_hunyuan15_t2v_480p.yaml` | HunyuanVideo 1.5 T2V 480p | `t2v` | CUDA REST | `hunyuan15-t2v-480p` | Source-backed | +| `openai_kandinsky5_t2v.yaml` | Kandinsky-5.0 T2V Lite | `t2v` | CUDA REST | `kandinsky5-t2v` | Source-backed | +| `openai_hunyuan15_i2v_480p.yaml` | HunyuanVideo 1.5 I2V 480p | `i2v` | CUDA REST | `hunyuan15-i2v-480p` | Source-backed; preset renders 720x1280 | +| `openai_kandinsky6_ti2va.yaml` | Kandinsky 6 Pro TI2VA | `t2v` | CUDA REST | `kandinsky6-ti2va` | Source-backed | +| `openai_kandinsky6_ti2va_distilled.yaml` | Kandinsky 6 Pro-distill TI2VA | `t2v` | CUDA REST | `kandinsky6-ti2va-distilled` | Source-backed | +| `openai_ltx2_distilled.yaml` | LTX2 Distilled | `t2v` | CUDA REST | `ltx2-distilled/cuda-rest` | Source-backed | +| `streaming_demo.yaml` | LTX2 Distilled | `t2v` | CUDA streaming | `ltx2-distilled/cuda-streaming` | Source-backed | +| `mlx_fasth3.yaml` | FastH3 V1 converted | `t2v` | MLX REST | none (legacy catalog only) | Source runtime; T2VA only | +| `mlx_fasth3_8step.yaml` | FastH3 V2 converted | `t2v` | MLX REST | `fasth3-8step/mlx-rest` | Source runtime; T2VA only | +| `openai_flux1_dev.yaml` | FLUX.1-dev | `t2i` | CUDA REST `/v1/images` | none | Source-backed; no image manifest yet | +| `openai_flux2_dev.yaml` | FLUX.2-dev | `t2i` | CUDA REST `/v1/images` | none | Source-backed; no image manifest yet | +| `openai_flux2_klein.yaml` | FLUX.2-klein-4B | `t2i` | CUDA REST `/v1/images` | none | Source-backed; no image manifest yet | +| `openai_sd35.yaml` | SD3.5 Medium | `t2i` | CUDA REST `/v1/images` | none | Source-backed; no image manifest yet | +| `openai_stable_audio.yaml` | Stable Audio Open 1.0 | `t2a` | CUDA REST media jobs | none | Source-backed; optional audio dependencies | +| `openai_mmaudio.yaml` | MMAudio large-44k-v2 | `v2a` | CUDA REST media jobs | none | Source-backed; video reference required | + +## Registry families without a maintained baseline + +The registry recognizes more model IDs than this table lists. These families +still resolve through the typed API, but they do not have a checked-in serving +configuration in this checkout: + +- Kandinsky 5 I2V and other checkpoints, Kandinsky 6 VSR, Kandinsky 5 SR. +- Cosmos, Cosmos 2.5, GEN3C, Helios, HyWorld, LingBot Video/World/World2. +- MatrixGame 2/3 interactive generation. +- Wan Fun InP/Control, Wan Animate, Wan S2V, Lucy Edit, Self-Forcing variants. +- MMAudio small/medium variants, Stable Audio Open Small. +- MagiHuman pipelines have no registry entry. + +Adding a baseline for one of these is a code and evidence task, not a catalog +edit. The next reviewable batches are image manifests (after the shared runtime +workload contract admits `t2i`), audio manifests, then the interactive and +super-resolution families. + +## Hardware evidence found in recipes + +| Device identity in source | Memory type | Capacity stated by source | Recipe or test evidence | Source | +| --- | --- | --- | --- | --- | +| NVIDIA GB200 | Dedicated HBM | Not stated per device | FastH3 four-device benchmarks and measured latency table | `examples/inference/basic/README.md`, `docs/inference/support_matrix.md` | +| NVIDIA H100 (sm_90) | Dedicated HBM | 80 GB per device | FastH3 four- and eight-device benchmark rows | `examples/inference/basic/basic_fasth3_h100.yaml`, `examples/inference/basic/README.md` | +| NVIDIA B200 | Dedicated HBM | Not stated per device | Kandinsky 5 and Stable Audio recorded single-GPU runs | `docs/cookbook/kandinsky5.md`, `docs/cookbook/stable-audio.md` | +| NVIDIA RTX 5090 | Dedicated GDDR | 32 GB | CompactH3 and OmniRef profiles | `examples/serving/openai_compacth3_rtx5090.yaml`, `examples/inference/basic/basic_fasth3_omniref_rtx5090.yaml` | +| NVIDIA RTX PRO 6000 Blackwell | Dedicated GDDR | 96 GB | CompactH3, FastH3 OmniRef profiles | `examples/serving/openai_compacth3_rtx_pro6000.yaml`, `examples/inference/basic/basic_fasth3_omniref_rtx_pro6000.yaml` | +| NVIDIA RTX 4090 | Dedicated GDDR | 24 GB | FastH3 OmniRef FP8 profile, no FP4 | `examples/inference/basic/basic_fasth3_omniref_rtx4090.yaml` | +| NVIDIA DGX Spark (GB10) | Unified LPDDR | 128 GB system memory | FastH3 and OmniRef one- and two-Spark recipes | `examples/serving/openai_fasth3_spark.yaml`, `examples/inference/basic/basic_fasth3_omniref_pdd.py` | +| Apple M4 Max | Unified | 16 GB or 36 GB system memory | FastMetal and MLX support rows | `docs/inference/support_matrix.md` | +| Apple M1 Pro | Unified | 16 GB system memory | PR #1802 real 1.3B run, 256x256, 17 frames | PR #1802, not merged into this checkout | +| Apple M2 (MacBook Air) | Unified | 8 GB system memory | Local harness host only; no successful 1.3B run recorded | Session hardware, not a recipe | + +The source often records a GPU count but not a device model. Wan2.1 I2V, Wan2.2 +A14B, HunyuanVideo 1.5, Kandinsky 6, FLUX and SD3.5 currently fall in that +category. A GPU-count match is not evidence that a device fits. + +## Hardware handoff to the catalog and UI + +- Exact identities above need canonical catalog variants. `RTX 5090 32 GB`, + `RTX PRO 6000 Blackwell 96 GB`, `H100 80 GB`, `GB10 128 GB unified` and + `M4 Max 16/36 GB unified` must not collapse to marketing names without + capacity or memory type. +- Dedicated VRAM and unified memory are separate estimator inputs. The Spark + and Apple rows are system memory shared with the OS; none of them is a + dedicated VRAM budget. A usable unified budget needs a platform policy or a + user-entered application budget. +- Host RAM is a third input. FastH3 and CompactH3 offload text/image encoders + and pin CPU memory; no recipe records the host RAM used, so offload host + requirements remain unknown. +- Multi-GPU rows are topology, not pooled memory. FastH3 runs four or eight + devices with sequence parallelism; one device's capacity is the relevant + per-device fact. +- Restrictions such as MIG, vGPU or a user-set memory limit must not inherit + the full card capacity. + +## Verification + +The declaration checks run without model weights: + +```bash +pytest fastvideo/tests/entrypoints/test_openai_serving_engine.py \ + fastvideo/tests/entrypoints/test_serving_family_baselines.py +cd docs && npm run test:cookbook +``` + +Audio clients are in `examples/serving/clients/audio.py`, `audio.mjs` and +`audio.sh`; they read `media_type` before choosing the `.wav` extension. + +Runtime evidence is recorded only when a generation actually completes with +the exact configuration, request, environment, revisions, hardware and metric +scope. A source-backed row above stays unverified until then. diff --git a/docs/design/inference_schema_parity_inventory.yaml b/docs/design/inference_schema_parity_inventory.yaml index 2c2eada72..088f11d27 100644 --- a/docs/design/inference_schema_parity_inventory.yaml +++ b/docs/design/inference_schema_parity_inventory.yaml @@ -88,6 +88,7 @@ surfaces: video_decode_backend: "MiniMax-H3 video decoder selection (full VAE vs TAEH3 preview); model-specific optimization not yet represented in the typed public schema." taeh3_checkpoint: "Optional local TAEH3 safetensors path; model-specific optimization not yet represented in the typed public schema." taeh3_chunk_size: "TAEH3 temporal chunk length; model-specific optimization not yet represented in the typed public schema." + h3_vae_nvfp4_decoder: "Exported NVFP4 MiniMax-H3 video decoder file that replaces the VAE decoder at load; model-specific optimization not yet represented in the typed public schema." vae_parallel_encode: "MiniMax-H3 sequence-parallel reference VAE encode opt-in; model-specific optimization not yet represented in the typed public schema." vae_parallel_decode_strategy: "Chunk-transport collective for vae_parallel_decode; model-specific optimization not yet represented in the typed public schema." attention_backend: "Process-wide default attention-backend request applied per component at load time; kernel-selection knob not yet represented in the typed public schema." @@ -620,6 +621,8 @@ surfaces: enable_teacache: request.runtime.enable_teacache max_sequence_length: request.sampling.max_sequence_length boundary_ratio: request.sampling.boundary_ratio + audio_start_in_s: request.sampling.audio_start_in_s + audio_end_in_s: request.sampling.audio_end_in_s extra_params: request.extensions compatibility_only: seconds: diff --git a/docs/inference/fasth3_omniref.md b/docs/inference/fasth3_omniref.md index c2a669383..c89e963c8 100644 --- a/docs/inference/fasth3_omniref.md +++ b/docs/inference/fasth3_omniref.md @@ -12,6 +12,18 @@ This page covers running the NVFP4 stack on one consumer Blackwell device (RTX 5090, RTX PRO 6000, DGX Spark) and the closest equivalent on an RTX 4090, which has no FP4 hardware. +## At a glance + +| | | +|---|---| +| Checkpoint | [FastVideo/FastH3-OmniRef-NVFP4-QAD](https://huggingface.co/FastVideo/FastH3-OmniRef-NVFP4-QAD) | +| Inputs | A prompt plus ordered references: up to 9 images, 3 videos and 3 audio clips, 12 in total | +| Output | Video with synchronized stereo audio at 24 fps | +| Clip length | 124, 243 or 362 frames (5, 10 or 15 seconds) | +| Sampling | 8 fused transformer forwards, guidance 1.0 | +| Local hardware | RTX 5090, RTX PRO 6000 and DGX Spark (NVFP4); RTX 4090 (FP8) | +| Cookbook | [MiniMax H3 cookbook](../cookbook/minimax-h3.md) | + ## Composing the model directory The NVFP4 checkpoint is @@ -114,6 +126,28 @@ sits 4.3 dB farther from the bf16 attention reference on every frame (20.96 vs Use the 36L decoder for the best-quality clip and the 8L decoder on memory-bound devices; both run the FP8 split. +## Serving + +`examples/serving/openai_fasth3_omniref_{rtx5090,rtx_pro6000,spark}.yaml` serve +the same stacks through the OpenAI-compatible API; their `generator` blocks match +the inference recipes above. Start one with the environment from its header: + +```bash +fastvideo serve --config examples/serving/openai_fasth3_omniref_spark.yaml +``` + +Requests send ordered `image_reference`, `video_reference` and audio references. +The server reads local paths only under `server.media_roots`. These serving +configurations follow the maintained inference recipes and have not yet been +validated as servers on each device. + +Select the NVFP4 decoder per configuration with +`generator.pipeline.experimental.h3_vae_nvfp4_decoder`, pointing at a file from +[FastVideo/FastH3-NVFP4-VAE-Decoder](https://huggingface.co/FastVideo/FastH3-NVFP4-VAE-Decoder); +`FASTVIDEO_H3_VAE_NVFP4_DECODER` remains the fallback. The composed directory +holds base MiniMax-H3's full 36-block VAE, so both the 36-block quality tier and +the 8-block fast tier apply. + ## Environment The profiles set these; set them yourself when driving the YAML recipes. diff --git a/docs/js/build-cookbook-capacity.mjs b/docs/js/build-cookbook-capacity.mjs index 6118589f8..4d3dd1b6a 100644 --- a/docs/js/build-cookbook-capacity.mjs +++ b/docs/js/build-cookbook-capacity.mjs @@ -57,22 +57,114 @@ function count(value, context) { return value; } +const HARDWARE_VENDORS = new Set(["nvidia", "amd", "intel", "apple"]); +const HARDWARE_KINDS = new Set(["gpu", "accelerator", "soc", "system"]); +const HARDWARE_BACKENDS = new Set(["cuda", "rocm", "metal", "sycl", "unknown"]); +const FORM_FACTORS = new Set(["pcie", "sxm", "oam", "desktop", "laptop", "soc", "system", "unknown"]); +const MEMORY_TYPES = new Set(["dedicated", "unified"]); +const MEMORY_UNITS = { GB: 1, GiB: 1.073741824 }; + +function memoryQuantity(value, context) { + keys(value, ["value", "unit"], [], context); + requireValue(positive(value.value), `${context}: value must be positive and finite`); + requireValue(Object.hasOwn(MEMORY_UNITS, value.unit), `${context}: unit must be GB or GiB`); + return value; +} + +function memorySizeGb(value, context) { + memoryQuantity(value, context); + return value.value * MEMORY_UNITS[value.unit]; +} + +function checkedSources(values, context) { + requireValue(Array.isArray(values) && values.length > 0, `${context}: provide source URLs`); + const seen = new Set(); + for (const source of values) { + keys(source, ["url", "checked"], [], context); + url(source.url, `${context}.url`); + requireValue(!seen.has(source.url), `${context}: duplicate source ${source.url}`); + seen.add(source.url); + const date = + typeof source.checked === "string" && + /^\d{4}-\d{2}-\d{2}$/.test(source.checked) && + Date.parse(`${source.checked}T00:00:00Z`); + requireValue( + date !== false && Number.isFinite(date) && new Date(date).toISOString().slice(0, 10) === source.checked, + `${context}.checked: expected a YYYY-MM-DD date`, + ); + } + return values; +} + function hardwareRecords(document, context) { keys(document, ["hardware"], [], context); requireValue(object(document.hardware) && Object.keys(document.hardware).length > 0, `${context}: no hardware`); + const names = new Set(); + const identities = new Map(); + const families = new Map(); + const records = {}; for (const [id, hardware] of Object.entries(document.hardware)) { const at = `${context}: hardware.${id}`; identifier(id, at); keys( hardware, - ["name", "memory_type", "memory_gb", "backend", "sources"], - ["ui_id", "supported_dtypes", "notes"], + ["name", "vendor", "kind", "memory", "backend", "sources"], + [ + "aliases", + "family", + "variant", + "form_factor", + "gpu_count", + "host_memory", + "partitionable", + "partitions", + "supported_dtypes", + "notes", + "ui_id", + ], at, ); - requireValue(text(hardware.name) && text(hardware.backend), `${at}: name and backend must be text`); - requireValue(["dedicated", "unified"].includes(hardware.memory_type), `${at}: invalid memory_type`); - requireValue(positive(hardware.memory_gb), `${at}: memory_gb must be positive and finite`); - if (hardware.ui_id !== undefined) identifier(hardware.ui_id, `${at}.ui_id`); + requireValue(text(hardware.name), `${at}: name must be text`); + const foldedName = hardware.name.toLocaleLowerCase(); + requireValue(!names.has(foldedName), `${at}: duplicate hardware name ${hardware.name}`); + names.add(foldedName); + requireValue(HARDWARE_VENDORS.has(hardware.vendor), `${at}: unknown vendor`); + requireValue(HARDWARE_KINDS.has(hardware.kind), `${at}: unknown kind`); + requireValue(HARDWARE_BACKENDS.has(hardware.backend), `${at}: unknown backend`); + if (hardware.form_factor !== undefined) + requireValue(FORM_FACTORS.has(hardware.form_factor), `${at}: unknown form_factor`); + keys(hardware.memory, ["type", "nominal"], ["usable", "policy"], `${at}.memory`); + requireValue(MEMORY_TYPES.has(hardware.memory.type), `${at}.memory: invalid type`); + const nominalGb = memorySizeGb(hardware.memory.nominal, `${at}.memory.nominal`); + if (hardware.memory.usable !== undefined) { + const usableGb = memorySizeGb(hardware.memory.usable, `${at}.memory.usable`); + requireValue(usableGb <= nominalGb, `${at}.memory.usable: exceeds nominal capacity`); + requireValue(text(hardware.memory.policy), `${at}.memory.policy: explain how the usable budget is derived`); + } else requireValue(hardware.memory.policy === undefined, `${at}.memory.policy: needs a usable budget`); + if (hardware.host_memory !== undefined) memorySizeGb(hardware.host_memory, `${at}.host_memory`); + if (hardware.gpu_count !== undefined) requireValue(integer(hardware.gpu_count), `${at}.gpu_count: invalid`); + if (hardware.partitionable !== undefined) + requireValue(typeof hardware.partitionable === "boolean", `${at}.partitionable: expected a boolean`); + const partitions = []; + if (hardware.partitions !== undefined) { + requireValue( + Array.isArray(hardware.partitions) && hardware.partitions.length > 0, + `${at}.partitions: expected a list`, + ); + requireValue(hardware.memory.type === "dedicated", `${at}.partitions: only dedicated devices can be partitioned`); + requireValue(hardware.partitionable !== false, `${at}.partitions: conflicts with partitionable: false`); + const ids = new Set(); + for (const partition of hardware.partitions) { + keys(partition, ["id", "label", "memory"], [], `${at}.partitions`); + identifier(partition.id, `${at}.partitions.id`); + requireValue(!ids.has(partition.id), `${at}.partitions: duplicate partition ${partition.id}`); + ids.add(partition.id); + requireValue(text(partition.label), `${at}.partitions.${partition.id}.label: expected text`); + const partitionGb = memorySizeGb(partition.memory, `${at}.partitions.${partition.id}.memory`); + requireValue(partitionGb <= nominalGb, `${at}.partitions.${partition.id}: must not exceed the full device`); + partitions.push({ ...structuredClone(partition), memory_gb: partitionGb }); + } + } if (hardware.notes !== undefined) requireValue(text(hardware.notes), `${at}: notes must be text`); if (hardware.supported_dtypes !== undefined) requireValue( @@ -82,9 +174,59 @@ function hardwareRecords(document, context) { new Set(hardware.supported_dtypes).size === hardware.supported_dtypes.length, `${at}: supported_dtypes must be a nonempty unique list of ${[...WEIGHT_FORMATS].join(", ")}`, ); - sources(hardware.sources, `${at}.sources`); + if (hardware.family !== undefined) { + identifier(hardware.family, `${at}.family`); + requireValue(text(hardware.variant), `${at}.variant: required with family`); + const variants = families.get(hardware.family) ?? new Set(); + const foldedVariant = hardware.variant.toLocaleLowerCase(); + requireValue(!variants.has(foldedVariant), `${at}: duplicate variant in family ${hardware.family}`); + variants.add(foldedVariant); + families.set(hardware.family, variants); + } else requireValue(hardware.variant === undefined, `${at}.variant: needs a family`); + requireValue(hardware.aliases === undefined || Array.isArray(hardware.aliases), `${at}.aliases: expected a list`); + const aliases = [...(hardware.aliases ?? [])]; + if (hardware.ui_id !== undefined) { + identifier(hardware.ui_id, `${at}.ui_id`); + aliases.push(hardware.ui_id); + } + requireValue( + aliases.every((alias) => text(alias)), + `${at}.aliases: expected nonempty text`, + ); + const record = { + id, + ...structuredClone(hardware), + aliases, + gpu_count: hardware.gpu_count ?? 1, + memory_gb: nominalGb, + }; + delete record.ui_id; + if (partitions.length) record.partitions = partitions; + if (hardware.memory.usable !== undefined) + record.usable_memory_gb = memorySizeGb(hardware.memory.usable, `${at}.memory.usable`); + if (hardware.host_memory !== undefined) + record.host_memory_gb = memorySizeGb(hardware.host_memory, `${at}.host_memory`); + checkedSources(hardware.sources, `${at}.sources`); + records[id] = record; } - return document.hardware; + for (const [id, hardware] of Object.entries(records)) { + const local = new Set([id.toLocaleLowerCase()]); + for (const identity of [id, ...hardware.aliases]) { + const folded = identity.toLocaleLowerCase(); + const owner = identities.get(folded); + requireValue( + owner === undefined || owner === id, + `${context}: hardware ${id} reuses identity ${identity} from ${owner}`, + ); + requireValue( + !(identity !== id && local.has(folded) && folded !== id), + `${context}: hardware ${id} repeats its own identity ${identity}`, + ); + local.add(folded); + identities.set(folded, id); + } + } + return records; } function componentRecords(root, catalogDir) { @@ -260,7 +402,10 @@ export function exportCapacityCatalog({ write(join(outputDir, "models", filename), model); index.models.push({ id: model.id, name: model.name, model_id: model.model_id, catalog_url: `models/${filename}` }); } - write(join(outputDir, "hardware.json"), hardware); + write(join(outputDir, "hardware.json"), { + version: 1, + devices: Object.values(hardware).sort((left, right) => left.id.localeCompare(right.id)), + }); write(join(outputDir, "index.json"), index); for (const name of readdirSync(join(outputDir, "models"))) { if (name.endsWith(".json") && ID.test(name.slice(0, -5)) && !expected.has(name)) diff --git a/docs/js/build-cookbook-config.mjs b/docs/js/build-cookbook-config.mjs index 5b3a11414..2ae7aa5b0 100644 --- a/docs/js/build-cookbook-config.mjs +++ b/docs/js/build-cookbook-config.mjs @@ -15,6 +15,7 @@ import { fileURLToPath } from "node:url"; import { parseDocument, stringify } from "yaml"; import { createValidator } from "./cookbook-validator.mjs"; import { resolveConfig } from "./cookbook-config.mjs"; +import { WORKLOAD_IDS } from "./cookbook-workloads.mjs"; import { validateCapacityMetadata } from "./cookbook-capacity-schema.mjs"; export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); @@ -348,9 +349,8 @@ export function buildCatalogs({ validateRuntime(runtime, id, `${context}.metadata`); expected( definition.workloads, - (value) => - Array.isArray(value) && value.length > 0 && value.every((workload) => ["t2v", "i2v"].includes(workload)), - "nonempty list of t2v or i2v workloads", + (value) => Array.isArray(value) && value.length > 0 && value.every((workload) => WORKLOAD_IDS.includes(workload)), + "nonempty list of supported workloads", `${context}.workloads`, ); const options = mergeOptions(declarations.options, definition.options, context); diff --git a/docs/js/cookbook-capacity-schema.mjs b/docs/js/cookbook-capacity-schema.mjs index 5e76cae9d..7728690d4 100644 --- a/docs/js/cookbook-capacity-schema.mjs +++ b/docs/js/cookbook-capacity-schema.mjs @@ -103,7 +103,7 @@ function referenceRuns(model, hardware, examples) { keys( run, ["id", "date", "hardware", "gpu_count", "outcome", "execution", "evidence_url", "context"], - ["memory"], + ["memory", "hardware_partition", "hardware_allocation_gb"], at, ); identifier(run.id, at); @@ -120,6 +120,9 @@ function referenceRuns(model, hardware, examples) { hardware === undefined || Object.hasOwn(hardware, run.hardware), `${at}: unknown hardware ${run.hardware}`, ); + if (run.hardware_partition !== undefined) identifier(run.hardware_partition, `${at}.hardware_partition`); + if (run.hardware_allocation_gb !== undefined) + requireValue(positive(run.hardware_allocation_gb), `${at}.hardware_allocation_gb must be positive`); requireValue(integer(run.gpu_count), `${at}: gpu_count must be a positive safe integer`); requireValue(run.outcome === "completed", `${at}: outcome must describe a completed generation`); requireValue(["first_request", "warm_request"].includes(run.execution), `${at}: unknown execution condition`); diff --git a/docs/js/cookbook-capacity.mjs b/docs/js/cookbook-capacity.mjs index e5851cea5..dbb31f482 100644 --- a/docs/js/cookbook-capacity.mjs +++ b/docs/js/cookbook-capacity.mjs @@ -53,20 +53,68 @@ export async function loadCapacityModel(index, id, options = {}) { return data; } +function validateHardwareDevice(hardware) { + requireValue( + object(hardware) && + text(hardware.id) && + text(hardware.name) && + object(hardware.memory) && + ["dedicated", "unified"].includes(hardware.memory.type) && + object(hardware.memory.nominal) && + Number.isFinite(hardware.memory.nominal.value) && + ["GB", "GiB"].includes(hardware.memory.nominal.unit) && + Number.isFinite(hardware.memory_gb) && + hardware.memory_gb > 0 && + Number.isSafeInteger(hardware.gpu_count) && + hardware.gpu_count > 0 && + ["cuda", "rocm", "metal", "sycl", "unknown"].includes(hardware.backend) && + Array.isArray(hardware.aliases) && + hardware.aliases.every(text), + `Invalid capacity hardware ${hardware?.id ?? "(missing id)"}`, + ); + if (hardware.usable_memory_gb !== undefined) + requireValue( + Number.isFinite(hardware.usable_memory_gb) && + hardware.usable_memory_gb > 0 && + hardware.usable_memory_gb <= hardware.memory_gb, + `Invalid usable memory for ${hardware.id}`, + ); + if (hardware.host_memory_gb !== undefined) + requireValue( + Number.isFinite(hardware.host_memory_gb) && hardware.host_memory_gb > 0, + `Invalid host memory for ${hardware.id}`, + ); + if (hardware.partitions !== undefined) { + requireValue( + Array.isArray(hardware.partitions) && + hardware.partitions.length > 0 && + hardware.partitions.every( + (partition) => + object(partition) && + text(partition.id) && + text(partition.label) && + Number.isFinite(partition.memory_gb) && + partition.memory_gb > 0 && + // A full-card profile such as A100 7g.80gb equals the card; none may exceed it. + partition.memory_gb <= hardware.memory_gb, + ), + `Invalid partitions for ${hardware.id}`, + ); + } +} + /** Hardware specifications are separate from model-specific successful run evidence. */ export async function loadCapacityHardware(index, options = {}) { validateIndex(index); requireValue(text(index.url), "Capacity index requires its source URL"); - const { data } = await fetchJson(new URL(index.hardware_url, index.url).href, options); - requireValue(object(data) && Object.keys(data).length > 0, "Invalid capacity hardware catalog"); - for (const [id, hardware] of Object.entries(data)) - requireValue( - object(hardware) && - text(hardware.name) && - ["dedicated", "unified"].includes(hardware.memory_type) && - Number.isFinite(hardware.memory_gb) && - hardware.memory_gb > 0, - `Invalid capacity hardware ${id}`, - ); - return data; + const { data, url } = await fetchJson(new URL(index.hardware_url, index.url).href, options); + requireValue(object(data) && data.version === 1 && Array.isArray(data.devices), "Invalid capacity hardware catalog"); + const ids = new Set(); + for (const hardware of data.devices) { + validateHardwareDevice(hardware); + requireValue(!ids.has(hardware.id), `Duplicate capacity hardware ${hardware.id}`); + ids.add(hardware.id); + } + requireValue(data.devices.length > 0, "Invalid capacity hardware catalog"); + return { version: data.version, devices: data.devices, url }; } diff --git a/docs/js/cookbook-config.mjs b/docs/js/cookbook-config.mjs index e6fbba698..e9a850c06 100644 --- a/docs/js/cookbook-config.mjs +++ b/docs/js/cookbook-config.mjs @@ -3,6 +3,8 @@ * Native baselines and manifest-selected JSON Schema controls come from the authored YAML catalog. */ import { createValidator } from "./cookbook-validator.mjs"; +import { WORKLOADS } from "./cookbook-workloads.mjs"; +export { WORKLOADS }; export { loadCapacityIndex, loadCapacityModel, loadCapacityHardware } from "./cookbook-capacity.mjs"; const scope = globalThis; @@ -63,6 +65,7 @@ function validateIndex(index, baseUrl) { for (const key of ["id", "label", "workload", "runtime", "catalog_url"]) { requireShape(text(deployment[key]), `${entry}.${key}`, "nonempty text"); } + requireShape(Object.hasOwn(WORKLOADS, deployment.workload), `${entry}.workload`, "a supported workload ID"); requireShape( !deploymentIds.has(deployment.id), `${entry}.id (${deployment.id})`, @@ -129,6 +132,7 @@ function validateCatalog(catalog, deployment) { } } requireShape(catalog.workload === deployment.workload, `${context}.workload`, deployment.workload); + requireShape(Object.hasOwn(WORKLOADS, catalog.workload), `${context}.workload`, "a supported workload ID"); const paths = new Set(); for (const [position, control] of catalog.controls.entries()) { const field = `${context}.controls[${position}]`; @@ -350,23 +354,28 @@ export function sampleRequest(catalog, config) { websocketUrl: `ws://${host}:${server.port}/v1/stream`, }; } + const media = Object.hasOwn(WORKLOADS, catalog.workload) ? WORKLOADS[catalog.workload] : undefined; + if (media === undefined) throw new CookbookValidationError(`Unsupported workload: ${catalog.workload}`); const components = config.generator?.pipeline?.components; const model = components?.lora_path ? components.lora_nickname : server.served_model_name || catalog.model.id; const body = { ...(model === undefined ? {} : { model }), prompt: config.default_request?.prompt ?? "A river flowing through a peaceful forest", }; - if (catalog.workload === "i2v") body.input_reference = "/absolute/path/to/first-frame.png"; + if (media.reference === "input_reference") body.input_reference = "/absolute/path/to/first-frame.png"; + if (media.reference === "video_reference") { + body.video_reference = { video_url: "/absolute/path/to/input.mp4" }; + } const argv = [ "curl", "--fail-with-body", - `http://${host}:${server.port}/v1/videos/sync`, + `http://${host}:${server.port}${media.endpoint}`, "-H", "Content-Type: application/json", "--data", JSON.stringify(body), ]; - argv.push("--output", "output.mp4"); + argv.push("--output", `output.${media.extension}`); return { body, command: argv.map(shellQuote).join(" ") }; } @@ -434,4 +443,10 @@ function getOptions(catalog, config = catalog.base_config, selections = {}) { } export { loadIndex, loadDeployment, getOptions, resolveConfig }; -export { estimateMemory } from "./cookbook-memory.mjs"; +export { + assessCapacity, + estimateMemory, + matchEvidence, + recommendCapacity, + resolveExecution, +} from "./cookbook-memory.mjs"; diff --git a/docs/js/cookbook-memory.mjs b/docs/js/cookbook-memory.mjs index 1286e477a..8990b6e79 100644 --- a/docs/js/cookbook-memory.mjs +++ b/docs/js/cookbook-memory.mjs @@ -1,15 +1,23 @@ /** - * Analytical single-GPU memory estimates for cookbook deployments. - * Estimates are not measurements and never mark a recipe as verified. + * Analytical memory estimates, evidence applicability and candidate recommendations for cookbook + * deployments. Analytical results are not measurements and never mark a recipe as verified. + * + * Inputs use the static capacity catalog exported by build-cookbook-capacity.mjs: + * model.components is a mapping by role, hardware uses memory.nominal/memory.type (the + * normalizer also accepts legacy memory_gb/memory_type for custom callers), and examples + * preserve the native baseline. Execution settings are resolved from the serving configuration. */ const GIB = 1024 ** 3; -const UNIT_BYTES = { GiB: GIB, GB: 1e9 }; +const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value); +const isCount = (value) => Number.isInteger(value) && value > 0; /** NVFP4 stores two 4-bit values per byte plus a 1-byte scale for every 16 values. */ export const BYTES_PER_WEIGHT = { fp32: 4, fp16: 2, bf16: 2, fp8: 1, nvfp4: 0.5 + 1 / 16 }; export const DEFAULT_HEADROOM = { fraction: 0.1, reserveGiB: 1 }; +/** Conservative application share for unified memory when the caller gives no budget. */ +export const DEFAULT_UNIFIED_BUDGET_FRACTION = 0.75; /** * Wan working-memory terms. Tensor counts follow WanTransformerBlock.forward and the chunked @@ -23,17 +31,81 @@ export const WAN_RULE = { decodeFrameFeatures: 48, }; -const PHASES = ["encoding", "denoising", "decoding"]; -const COMPONENT_PHASE = { text_encoder: "encoding", transformer: "denoising", vae: "decoding" }; +const PHASES = ["startup", "encoding", "denoising", "decoding"]; +const COMPONENT_PHASE = { + text_encoder: "encoding", + text_encoder_2: "encoding", + image_encoder: "encoding", + image_encoder_2: "encoding", + transformer: "denoising", + transformer_2: "denoising", + vae: "decoding", + audio_vae: "decoding", +}; +const COMPONENT_KIND = { + transformer: "transformer", + transformer_2: "transformer", + text_encoder: "encoder", + text_encoder_2: "encoder", + image_encoder: "encoder", + image_encoder_2: "encoder", + vae: "vae", + audio_vae: "vae", +}; const SAMPLING_FIELDS = ["num_frames", "height", "width"]; -const isCount = (value) => Number.isInteger(value) && value > 0; /** Unset switches default on, matching OffloadConfig in fastvideo/api/schema.py. */ -const OFFLOAD_DEFAULTS = { dit_layerwise: true, text_encoder: true, vae: true }; +const OFFLOAD_DEFAULTS = { + dit: true, + dit_layerwise: true, + text_encoder: true, + image_encoder: true, + vae: true, +}; /** Streaming offload holds the running block and one prefetched block. */ const STREAMED_BLOCKS = 2; +/** Native pipeline precision defaults for a modeled family; explicit config values override them. */ +export const PRECISION_DEFAULTS = { + wan: { + transformer: "bf16", + text_encoder: ["fp32"], + image_encoder: ["fp32"], + vae: "fp32", + vae_decode: "bf16", + vae_tiling: false, + }, +}; + +const DENSE_ATTENTION_BACKENDS = new Set(["FLASH_ATTN", "TORCH_SDPA", "SAGE_ATTN", "SAGE_ATTN_THREE"]); + +/** Settings that do not change a request's peak memory; other experimental keys do. */ +const HARMLESS_EXPERIMENTAL = new Set([ + "flow_shift", + "flow_shift_sr", + "dmd_denoising_steps", + "num_inference_steps", + "seed", + "fps", +]); + +const MEMORY_ENV_KEYS = new Set([ + "FASTVIDEO_ATTENTION_BACKEND", + "FASTVIDEO_LAYERWISE_OFFLOAD_BUFFERS", + "PYTORCH_CUDA_ALLOC_CONF", +]); +const MEMORY_ENV_PREFIXES = [ + "FASTVIDEO_VSA_", + "FASTVIDEO_H3_", + "FASTVIDEO_LAYERWISE_", + "FASTVIDEO_NVFP4", + "FASTVIDEO_FP8", + "FASTVIDEO_COMPILE", + "FASTVIDEO_OFFLOAD", + "FASTVIDEO_TORCH", +]; + const WORKING_MEMORY = { wan: { geometry: [ @@ -57,88 +129,328 @@ export function resolveTopology(engine = {}) { return { gpus: Math.max(gpus, tpSize, spSize), tpSize, spSize }; } -function componentBytes(component, quantization) { - const quantized = quantization ? component.quantized_parameter_count : 0; - const perWeight = BYTES_PER_WEIGHT[component.dtype]; - return { - quantized: quantization ? quantized * BYTES_PER_WEIGHT[quantization] : 0, - dense: (component.parameter_count - quantized) * perWeight, - denseBlocks: component.block_count ? (component.block_parameter_count - quantized) * perWeight : 0, - }; +function normalizeComponents(model) { + if (!isObject(model.components)) return null; + const components = []; + for (const [role, component] of Object.entries(model.components)) { + if (!isObject(component) || !COMPONENT_PHASE[role]) return null; + if (component.kind !== COMPONENT_KIND[role]) return null; + components.push({ role, ...component }); + } + return components.length ? components : null; +} + +function geometryFor(model, role) { + const geometry = model.components?.[role]?.geometry; + return isObject(geometry) ? geometry : null; +} + +function asPrecisionList(value, fallback) { + if (typeof value === "string" && value) return [value]; + if (Array.isArray(value) && value.length && value.every((item) => typeof item === "string")) return value; + return fallback; +} + +function experimentalPrecision(experimental, name, fallback) { + const value = experimental[name]; + return typeof value === "string" && value ? value : fallback; } /** - * Weights a component holds on the GPU during one phase. A component is complete during its own phase - * unless it streams blocks; offload only frees it during other phases. Without FSDP, whole-model DiT - * offload leaves the transformer on the GPU, and packed quantized weights are buffers that stay resident. + * Resolve the native execution settings that change memory. Values come from the serving config, + * then from family defaults keyed by memory_rules. The component catalog never fixes precision. */ -function residentBytes(component, phase, offload, bytes) { - const full = bytes.quantized + bytes.dense; - const withoutBlocks = full - bytes.denseBlocks; - const share = component.block_count ? Math.min(1, STREAMED_BLOCKS / component.block_count) : 1; - const streamed = withoutBlocks + bytes.denseBlocks * share; - const active = COMPONENT_PHASE[component.role] === phase; - if (component.role === "transformer") { - if (offload.dit_layerwise) return active ? streamed : withoutBlocks; +export function resolveExecution({ model, config, env = {} }) { + const family = model.memory_rules; + const defaults = PRECISION_DEFAULTS[family] ?? {}; + const engine = config.generator?.engine ?? {}; + const pipeline = config.generator?.pipeline ?? {}; + const experimental = isObject(pipeline.experimental) ? pipeline.experimental : {}; + const textEncoderPrecisions = asPrecisionList(experimental.text_encoder_precisions, defaults.text_encoder ?? []); + const imageEncoderPrecisions = asPrecisionList( + experimental.image_encoder_precisions, + experimental.image_encoder_precision ? [experimental.image_encoder_precision] : (defaults.image_encoder ?? []), + ); + const vae = experimentalPrecision(experimental, "vae_precision", defaults.vae); + const vaeDecode = experimentalPrecision(experimental, "vae_decode_precision", defaults.vae_decode ?? vae); + const transformer = experimentalPrecision(experimental, "dit_precision", defaults.transformer); + const offload = { ...OFFLOAD_DEFAULTS, ...engine.offload }; + const quantization = isObject(engine.quantization) ? engine.quantization : {}; + const attentionBackend = typeof env.FASTVIDEO_ATTENTION_BACKEND === "string" ? env.FASTVIDEO_ATTENTION_BACKEND : null; + const residentBlocks = Number.parseInt(env.FASTVIDEO_LAYERWISE_RESIDENT_BLOCKS ?? "0", 10); + const layerwiseResidentBlocks = Number.isInteger(residentBlocks) && residentBlocks >= 0 ? residentBlocks : 0; + const vsa = + (attentionBackend && attentionBackend.includes("SPARSE")) || + Object.keys(experimental).some((key) => key.startsWith("VSA_")) || + Object.keys(env).some((key) => key.startsWith("FASTVIDEO_VSA_")); + return { + family, + workload: pipeline.workload_type, + num_gpus: engine.num_gpus ?? 1, + topology: resolveTopology(engine), + use_fsdp: engine.use_fsdp_inference === true, + offload, + lazy_module_load: offload.lazy_module_load === true, + compile: + engine.compile?.enabled === true || + engine.compile?.text_encoder_enabled === true || + engine.compile?.vae_enabled === true, + quantization, + attentionBackend, + layerwise_resident_blocks: layerwiseResidentBlocks, + vae_feature_cache: experimental["vae_config.use_feature_cache"] !== false, + vsa, + vae_tiling: typeof pipeline.vae_tiling === "boolean" ? pipeline.vae_tiling : (defaults.vae_tiling ?? false), + vae_sp: pipeline.vae_sp === true, + precision: { + transformer, + text_encoder: textEncoderPrecisions, + image_encoder: imageEncoderPrecisions, + vae, + vae_decode: vaeDecode, + }, + experimental, + }; +} + +function componentPrecision(component, execution) { + const { role } = component; + if (role === "transformer" || role === "transformer_2") return execution.precision.transformer; + if (role === "vae" || role === "audio_vae") return execution.precision.vae; + if (role === "image_encoder" || role === "image_encoder_2") { + const index = role === "image_encoder_2" ? 1 : 0; + return execution.precision.image_encoder[index]; + } + const index = role === "text_encoder_2" ? 1 : 0; + return execution.precision.text_encoder[index]; +} + +function componentActivationPrecision(component, execution) { + if (component.role === "vae" || component.role === "audio_vae") return execution.precision.vae_decode; + return componentPrecision(component, execution); +} + +function componentBytes(component, execution) { + const storage = componentPrecision(component, execution); + const perWeight = BYTES_PER_WEIGHT[storage]; + return { storage, perWeight, full: component.parameter_count * perWeight }; +} + +/** Weights resident during a request phase after offload and layerwise streaming. */ +function offloadFlag(role) { + if (role.startsWith("text_encoder")) return "text_encoder"; + if (role.startsWith("image_encoder")) return "image_encoder"; + return role; +} + +function residentBytes(component, phase, execution, bytes) { + const full = bytes.full; + if (component.role === "transformer" || component.role === "transformer_2") { + const blocks = component.block_parameter_count * bytes.perWeight; + const outside = full - blocks; + if (execution.offload.dit_layerwise) { + const resident = execution.layerwise_resident_blocks ?? 0; + const share = component.block_count + ? Math.min(1, (phase === "denoising" ? resident + STREAMED_BLOCKS : resident) / component.block_count) + : 1; + return outside + blocks * share; + } return full; } - if (!active) return offload[component.role] ? 0 : full; - const streams = component.role === "text_encoder" && offload.text_encoder && component.streams_when_offloaded; - return streams ? streamed : full; + const offloaded = execution.offload[offloadFlag(component.role)]; + if (COMPONENT_PHASE[component.role] !== phase) return offloaded ? 0 : full; + return full; } -function activationBytes(components, role) { - const component = components.find((item) => item.role === role); - return BYTES_PER_WEIGHT[component.activation_dtype ?? component.dtype]; +/** Load-time residency: text/image encoders and the VAE rest on CPU when offloaded, the DiT loads on GPU. */ +function startupBytes(component, execution, bytes) { + if (component.role === "transformer" || component.role === "transformer_2") return bytes.full; + return execution.offload[offloadFlag(component.role)] ? 0 : bytes.full; } -function wanWorkingMemory(geometry, sampling, components) { - const { temporal_compression, spatial_compression, patch_size, hidden_size, ffn_size, vae_decoder_channels } = - geometry; +function activationBytes(component, execution) { + return BYTES_PER_WEIGHT[componentActivationPrecision(component, execution)]; +} + +function wanWorkingMemory({ transformerGeometry, vaeGeometry, vaeComponent, sampling, components, execution }) { + const { temporal_compression, spatial_compression, vae_decoder_channels, output_channels } = vaeGeometry; + const { hidden_size, ffn_size, patch_size } = transformerGeometry; const { num_frames, height, width } = sampling; const latentFrames = Math.floor((num_frames - 1) / temporal_compression) + 1; const tokens = Math.ceil(latentFrames / patch_size[0]) * Math.ceil(height / (spatial_compression * patch_size[1])) * Math.ceil(width / (spatial_compression * patch_size[2])); - const dit = activationBytes(components, "transformer"); - const vae = activationBytes(components, "vae"); + const transformer = components.find((component) => component.role === "transformer"); + const vae = components.find((component) => component.role === "vae"); + const dit = activationBytes(transformer, execution); + const vaeActivation = activationBytes(vae, execution); const hidden = tokens * hidden_size * dit; const ffn = tokens * ffn_size * dit; const pixels = height * width; const outputFrames = (latentFrames - 1) * temporal_compression + 1; + const decodeMultiplier = Number.isFinite(vaeComponent?.decode_feature_multiplier) + ? vaeComponent.decode_feature_multiplier + : WAN_RULE.decodeFrameFeatures; return { + startup: 0, encoding: WAN_RULE.encodingWorkspaceGiB * GIB, denoising: Math.max( WAN_RULE.attentionHiddenTensors * hidden, WAN_RULE.ffnHiddenTensors * hidden + WAN_RULE.ffnTensors * ffn, ), decoding: - WAN_RULE.decodeFrameFeatures * vae_decoder_channels * pixels * vae + - outputFrames * 3 * pixels * (2 * vae + BYTES_PER_WEIGHT.fp32), + decodeMultiplier * vae_decoder_channels * pixels * vaeActivation + + outputFrames * (output_channels ?? 3) * pixels * (2 * vaeActivation + BYTES_PER_WEIGHT.fp32), }; } -function checkCoverage({ engine, sampling, workload, model, hardware, quantization }) { +function quantityGiB(quantity) { + if (!isObject(quantity) || !Number.isFinite(quantity.value) || !["GB", "GiB"].includes(quantity.unit)) return null; + return (quantity.value * (quantity.unit === "GiB" ? 1.073741824 : 1) * 1e9) / GIB; +} + +/** + * Normalize catalog or custom hardware. Catalog capacities are nominal; a dedicated + * device uses its nominal (or selected partition) capacity, while a unified device + * uses an explicit application budget or a labeled 75% default and never exposes + * system memory as dedicated VRAM. + */ +export function normalizeHardware(hardware, { memoryBudgetGiB, allocationMemoryGb, partitionId } = {}) { + if (!isObject(hardware)) return null; + const memory = isObject(hardware.memory) ? hardware.memory : null; + const memoryType = memory?.type ?? hardware.memory_type; + if (!["dedicated", "unified"].includes(memoryType)) return null; + const nominalGiB = memory?.nominal + ? quantityGiB(memory.nominal) + : Number.isFinite(hardware.memory_gb) + ? (hardware.memory_gb * 1e9) / GIB + : null; + if (!(nominalGiB > 0)) return null; + let allocationGiB = Number.isFinite(allocationMemoryGb) ? (allocationMemoryGb * 1e9) / GIB : null; + let partition = null; + if (partitionId !== undefined) { + partition = (hardware.partitions ?? []).find((item) => item.id === partitionId); + if (!partition) return null; + allocationGiB = + quantityGiB(partition.memory) ?? + (Number.isFinite(partition.memory_gb) ? (partition.memory_gb * 1e9) / GIB : null); + if (!(allocationGiB > 0)) return null; + } + const catalogUsableGiB = memory?.usable + ? quantityGiB(memory.usable) + : Number.isFinite(hardware.usable_memory_gb) + ? (hardware.usable_memory_gb * 1e9) / GIB + : null; + const requestedBudget = Number.isFinite(memoryBudgetGiB) ? memoryBudgetGiB : null; + const catalogBudget = requestedBudget ?? catalogUsableGiB; + const assumedBudget = + memoryType === "unified" && catalogBudget === null ? nominalGiB * DEFAULT_UNIFIED_BUDGET_FRACTION : null; + const budgetGiB = catalogBudget ?? assumedBudget; + if (budgetGiB !== null && !(budgetGiB > 0)) return null; + if (budgetGiB !== null && budgetGiB > nominalGiB) return null; + if (allocationGiB !== null && allocationGiB > nominalGiB) return null; + const hostGiB = + quantityGiB(hardware.host_memory) ?? + (Number.isFinite(hardware.host_memory_gb) ? (hardware.host_memory_gb * 1e9) / GIB : null); + const capacity = Math.min(allocationGiB ?? nominalGiB, budgetGiB ?? nominalGiB); + return { + ...hardware, + id: hardware.id ?? (hardware.custom ? "custom" : "unknown"), + name: hardware.name ?? "Custom device", + memoryType, + nominalGiB, + catalogUsableGiB, + budgetGiB, + budgetSource: + requestedBudget !== null + ? "user" + : catalogUsableGiB !== null + ? "catalog_policy" + : assumedBudget !== null + ? "assumed_fraction" + : "nominal", + budgetAssumed: assumedBudget !== null, + allocationGiB, + partition, + capacityGiB: capacity, + hostGiB, + backend: hardware.backend ?? "unknown", + supported_dtypes: hardware.supported_dtypes, + custom: hardware.custom === true, + }; +} + +function applyUnifiedMemoryPolicy(execution, device) { + if (device?.memoryType !== "unified") return execution; + return { + ...execution, + offload: { + ...execution.offload, + dit: false, + dit_layerwise: false, + text_encoder: false, + image_encoder: false, + vae: false, + }, + }; +} + +function checkCoverage({ workload, model, device, execution, sampling, runtime = {}, components }) { const unknown = (reason) => ({ status: "unknown", reason }); - const components = model.components ?? []; - if (hardware.type !== "discrete_gpu") return unknown("Only discrete GPUs are modeled."); - if (!UNIT_BYTES[hardware.memory_per_device?.unit]) return unknown(`${hardware.name} has no known memory size.`); - if (!Array.isArray(hardware.supported_dtypes)) return unknown(`${hardware.name} lists no supported weight types.`); + if (runtime.return_trajectory_latents || runtime.return_trajectory_decoded) { + return unknown("Trajectory capture keeps a tensor per denoising step; it is not modeled."); + } + if (!device) return unknown("Select a catalogued hardware variant or enter a custom device with capacity."); + if (device.backend !== "cuda") return unknown(`${device.name} is not a CUDA device.`); + if (device.gpu_count > 1) { + return unknown("Multi-GPU hardware records are not modeled as one memory pool."); + } + if (!(device.capacityGiB > 0)) return unknown(`${device.name} has no known memory size.`); if (workload !== "t2v") return unknown(`${workload} workloads are not modeled yet.`); - if (resolveTopology(engine).gpus > 1 || engine.use_fsdp_inference) - return unknown("Multi-GPU and FSDP layouts are not modeled yet."); + if (execution.topology.gpus > 1 || execution.topology.tpSize > 1 || execution.topology.spSize > 1) { + return unknown("Multi-GPU layouts are not modeled as one memory pool yet."); + } + if (execution.use_fsdp) return unknown("FSDP inference sharding is not modeled yet."); + if (execution.lazy_module_load) return unknown("Lazy component loading is not modeled yet."); + if (execution.vsa) return unknown("VSA workspace is not modeled; a successful run alone is not a fit."); + if (execution.compile) return unknown("torch.compile buffers are not modeled yet."); + if (execution.vae_sp) return unknown("Parallel VAE decode is not modeled yet."); + if (execution.vae_feature_cache === false) { + return unknown("VAE feature caching is disabled; the full-clip decode path is not modeled."); + } + if (execution.quantization.transformer_quant) { + return unknown("Transformer quantization needs an explicit quantized parameter count."); + } + if (execution.quantization.transformer_retain_original_weights) { + return unknown("Retained original transformer weights are not modeled."); + } + if (execution.quantization.text_encoder_quant) return unknown("Text-encoder quantization is not modeled yet."); + if (execution.quantization.layer_profile) return unknown("Per-layer quantization profiles are not modeled."); + if (execution.attentionBackend && !DENSE_ATTENTION_BACKENDS.has(execution.attentionBackend)) { + return unknown(`Attention backend ${execution.attentionBackend} has no memory rule.`); + } const rule = WORKING_MEMORY[model.memory_rules]; if (!rule) return unknown(`No working-memory rule for ${model.memory_rules}.`); - const missing = rule.geometry.filter((field) => model.geometry?.[field] === undefined); + const transformerGeometry = geometryFor(model, "transformer"); + const vaeGeometry = geometryFor(model, "vae"); + const missing = rule.geometry.filter( + (field) => transformerGeometry?.[field] === undefined && vaeGeometry?.[field] === undefined, + ); if (missing.length) return unknown(`Model geometry lacks ${missing.join(", ")}.`); - if (!SAMPLING_FIELDS.every((field) => isCount(sampling[field]))) - return unknown("Frames, height and width must be set."); + if (!SAMPLING_FIELDS.every((field) => isCount(sampling[field]))) { + return unknown("Frames, height and width must be positive integers."); + } if ((sampling.num_videos_per_prompt ?? 1) !== 1) return unknown("Only one video per prompt is modeled."); + if (sampling.batch_cfg === true) return unknown("Batch CFG is not modeled."); + if (!components) return unknown("Components must be a mapping by role."); const unmodeled = components.find((component) => !COMPONENT_PHASE[component.role]); if (unmodeled) return unknown(`The ${unmodeled.role} component is not modeled yet.`); - if (!["transformer", "vae"].every((role) => components.some((component) => component.role === role))) - return unknown("The model needs a transformer and a VAE."); + for (const role of ["transformer", "vae", "text_encoder"]) { + if (!components.some((component) => component.role === role)) { + return unknown(`The model needs a ${role} component for this rule.`); + } + } const uncounted = components.find( (component) => !isCount(component.parameter_count) || @@ -146,70 +458,730 @@ function checkCoverage({ engine, sampling, workload, model, hardware, quantizati !(isCount(component.block_count) && isCount(component.block_parameter_count))), ); if (uncounted) return unknown(`The ${uncounted.role} parameter counts are incomplete.`); - const unsized = components.find( - (component) => - !BYTES_PER_WEIGHT[component.dtype] || !BYTES_PER_WEIGHT[component.activation_dtype ?? component.dtype], - ); + if (execution.offload.dit_layerwise) { + const missingBlocks = components.find( + (component) => + component.role.startsWith("transformer") && + (!isCount(component.block_count) || !isCount(component.block_parameter_count)), + ); + if (missingBlocks) { + return unknown(`Layerwise offload needs block counts for ${missingBlocks.role}.`); + } + } + const unsized = components.find((component) => { + const storage = componentPrecision(component, execution); + const activation = componentActivationPrecision(component, execution); + return !BYTES_PER_WEIGHT[storage] || !BYTES_PER_WEIGHT[activation]; + }); if (unsized) return unknown(`The ${unsized.role} precision is not modeled.`); - if (engine.quantization?.text_encoder_quant) return unknown("Text-encoder quantization is not modeled yet."); - if (quantization && !BYTES_PER_WEIGHT[quantization]) return unknown(`${quantization} weights are not modeled.`); - const dtypes = new Set([...components.map((component) => component.dtype), quantization].filter(Boolean)); - const unsupported = [...dtypes].find((dtype) => !hardware.supported_dtypes.includes(dtype)); - if (unsupported) - return { status: "unsupported", reason: `${hardware.name} does not support ${unsupported} weights.` }; - if (!quantization) return null; - if (engine.quantization.transformer_retain_original_weights) - return unknown("Retained original weights are not modeled."); - const transformer = components.find((component) => component.role === "transformer"); - if (!Number.isInteger(transformer.quantized_parameter_count)) - return unknown(`The transformer has no ${quantization} parameter count.`); + const dtypes = new Set(components.map((component) => componentPrecision(component, execution))); + if (!Array.isArray(device.supported_dtypes) || !device.supported_dtypes.length) { + const uncatalogued = [...dtypes].filter((dtype) => !["fp32", "fp16", "bf16"].includes(dtype)); + if (uncatalogued.length) { + return unknown(`${device.name} does not catalog ${uncatalogued.join(", ")} weight support.`); + } + } else { + const unsupported = [...dtypes].find((dtype) => !device.supported_dtypes.includes(dtype)); + if (unsupported) { + return { status: "unsupported", reason: `${device.name} does not support ${unsupported} weights.` }; + } + } return null; } +const HOST_STAGING_GIB = 2; + +/** Host bytes needed for CPU offload. Unified systems share one pool and are not double-counted. */ +function hostRequirement(components, bytes, execution) { + if (execution.topology.gpus > 1) return 0; + let used = 0; + for (const [index, component] of components.entries()) { + const flag = offloadFlag(component.role); + if (component.role === "transformer" || component.role === "transformer_2") { + if (execution.offload.dit_layerwise) used += bytes[index].full; + } else if (execution.offload[flag]) used += bytes[index].full; + } + return used ? used / GIB + HOST_STAGING_GIB : 0; +} + /** * Estimate one request's peak memory on one device from a resolved serving configuration. - * Each phase adds the weights resident on the GPU to that phase's working memory; the peak phase plus - * headroom is compared with the device's memory. Uncovered requests return `unknown` with a reason. + * Startup (load) and request phases are compared separately. Uncovered execution returns unknown. */ -export function estimateMemory({ config, workload, model, hardware, headroom = DEFAULT_HEADROOM }) { - const engine = config.generator?.engine ?? {}; - const sampling = config.default_request?.sampling ?? {}; - const quantization = engine.quantization?.transformer_quant?.toLowerCase() ?? null; - const gap = checkCoverage({ engine, sampling, workload, model, hardware, quantization }); +export function estimateMemory({ + config, + workload, + model, + hardware, + request, + env = {}, + headroom = DEFAULT_HEADROOM, + memoryBudgetGiB, + allocationMemoryGb, + partitionId, + hostMemoryGb, +}) { + const resolvedExecution = resolveExecution({ model, config, env }); + const sampling = request?.sampling ?? config.default_request?.sampling ?? {}; + const runtime = request?.runtime ?? config.default_request?.runtime ?? {}; + const components = normalizeComponents(model); + const device = normalizeHardware(hardware, { memoryBudgetGiB, allocationMemoryGb, partitionId }); + const execution = applyUnifiedMemoryPolicy(resolvedExecution, device); + const gap = checkCoverage({ + workload: workload ?? execution.workload, + model, + device, + execution, + sampling, + runtime, + components, + }); if (gap) return gap; - - const offload = { ...OFFLOAD_DEFAULTS, ...engine.offload }; - const working = WORKING_MEMORY[model.memory_rules].estimate(model.geometry, sampling, model.components); - const bytes = model.components.map((component) => - componentBytes(component, component.role === "transformer" ? quantization : null), - ); + const bytes = components.map((component) => componentBytes(component, execution)); + const transformerGeometry = geometryFor(model, "transformer"); + const vaeGeometry = geometryFor(model, "vae"); + const working = WORKING_MEMORY[model.memory_rules].estimate({ + transformerGeometry, + vaeGeometry, + vaeComponent: model.components.vae, + sampling, + components, + execution, + }); const phases = Object.fromEntries( PHASES.map((phase) => { - const weights = model.components.reduce( - (sum, component, index) => sum + residentBytes(component, phase, offload, bytes[index]), - 0, - ); + const weights = components.reduce((sum, component, index) => { + const resident = + phase === "startup" + ? startupBytes(component, execution, bytes[index]) + : residentBytes(component, phase, execution, bytes[index]); + return sum + resident; + }, 0); + const workingBytes = working[phase] ?? 0; return [ phase, - { weightsGiB: weights / GIB, workingGiB: working[phase] / GIB, totalGiB: (weights + working[phase]) / GIB }, + { + weightsGiB: weights / GIB, + workingGiB: workingBytes / GIB, + totalGiB: (weights + workingBytes) / GIB, + }, ]; }), ); - const peakPhase = PHASES.reduce((peak, phase) => (phases[phase].totalGiB > phases[peak].totalGiB ? phase : peak)); - const peakGiB = phases[peakPhase].totalGiB; + const peak = PHASES.reduce((best, phase) => (phases[phase].totalGiB > phases[best].totalGiB ? phase : best)); + const peakGiB = phases[peak].totalGiB; const requiredGiB = peakGiB * (1 + headroom.fraction) + headroom.reserveGiB; - const { value, unit } = hardware.memory_per_device; - const capacityGiB = (value * UNIT_BYTES[unit]) / GIB; + const capacity = device.capacityGiB; + const hostRequiredGiB = device.memoryType === "unified" ? 0 : hostRequirement(components, bytes, execution); + const hostCapacityGiB = + device.memoryType === "unified" + ? null + : Number.isFinite(hostMemoryGb) + ? (hostMemoryGb * 1e9) / GIB + : device.hostGiB; + const host = + device.memoryType === "unified" + ? { requiredGiB: 0, capacityGiB: null, status: "shared" } + : { + requiredGiB: hostRequiredGiB, + capacityGiB: hostCapacityGiB, + status: + hostRequiredGiB === 0 + ? "not_required" + : hostCapacityGiB === null + ? "unknown" + : hostRequiredGiB <= hostCapacityGiB + ? "fits" + : "exceeds", + }; + let status = requiredGiB <= capacity ? "fits" : "exceeds"; + if (host.status === "exceeds") status = "exceeds"; + const notes = []; + if (device.memoryType === "unified") { + notes.push( + `Unified memory: ${capacity.toFixed(1)} GiB application budget, shared with the CPU and OS; not dedicated VRAM.`, + ); + if (device.budgetAssumed) { + notes.push( + `Assumed ${Math.round(DEFAULT_UNIFIED_BUDGET_FRACTION * 100)}% of unified memory is available to the application; set a budget to override.`, + ); + } + } + if (device.allocationGiB !== null && device.budgetGiB !== null && device.budgetGiB < device.allocationGiB) { + notes.push( + `The ${device.budgetGiB.toFixed(1)} GiB application budget is smaller than the ${device.allocationGiB.toFixed(1)} GiB allocation.`, + ); + } + if (device.partition) { + notes.push( + device.allocationGiB < device.nominalGiB + ? `Using the ${device.partition.label} partition; the full ${device.nominalGiB.toFixed(1)} GiB is not available.` + : `Using the ${device.partition.label} partition, which spans the full ${device.nominalGiB.toFixed(1)} GiB.`, + ); + } else if (device.allocationGiB !== null) { + notes.push( + `Using a ${device.allocationGiB.toFixed(1)} GiB provisioned allocation; the full ${device.nominalGiB.toFixed(1)} GiB is not available.`, + ); + } + if (device.custom) notes.push("Custom hardware specification; not catalog-verified."); + if (!Array.isArray(device.supported_dtypes) || !device.supported_dtypes.length) { + notes.push("Weight-format support is not cataloged; the estimate assumes fp32, fp16, or bf16 storage."); + } + if (execution.vae_tiling) notes.push("Decode memory assumes untiled VAE decoding; tiling may lower it."); + if ( + device.memoryType === "unified" && + ["dit", "dit_layerwise", "text_encoder", "image_encoder", "vae"].some((key) => resolvedExecution.offload[key]) + ) { + notes.push("CPU offload is disabled on unified memory; selected components share one memory pool."); + } + if (host.status === "unknown") { + notes.push(`Host memory for offload is not specified; ${hostRequiredGiB.toFixed(1)} GiB is needed.`); + } else if (host.status === "exceeds") { + notes.push(`Host memory for offload exceeds the ${hostCapacityGiB.toFixed(1)} GiB available.`); + } + notes.push("Analytical estimate only; no memory was measured."); return { - status: requiredGiB <= capacityGiB ? "fits" : "exceeds", + status, basis: "analytical", phases, - peakPhase, + peakPhase: peak, peakGiB, requiredGiB, - capacityGiB, - notes: config.generator?.pipeline?.vae_tiling - ? ["Decode memory assumes untiled VAE decoding; tiling lowers it."] - : [], + capacityGiB: capacity, + headroom: { fraction: headroom.fraction, reserveGiB: headroom.reserveGiB }, + memory: { + type: device.memoryType, + nominalGiB: device.nominalGiB, + budgetGiB: device.budgetGiB ?? null, + budgetSource: device.budgetSource, + partition: device.partition?.id ?? null, + }, + host, + hardware: { + id: device.id, + name: device.name, + memory_type: device.memoryType, + capacity_gib: capacity, + custom: device.custom, + }, + execution: { + topology: execution.topology, + offload: execution.offload, + precision: execution.precision, + attentionBackend: execution.attentionBackend, + vae_tiling: execution.vae_tiling, + layerwise_resident_blocks: execution.layerwise_resident_blocks, + }, + notes, }; } + +function memoryEnv(env = {}) { + return Object.fromEntries( + Object.entries(env) + .filter(([key]) => MEMORY_ENV_KEYS.has(key) || MEMORY_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) + .sort(([left], [right]) => left.localeCompare(right)), + ); +} + +function normalizedExperimental(experimental = {}) { + return Object.fromEntries( + Object.entries(experimental) + .filter(([key]) => !HARMLESS_EXPERIMENTAL.has(key)) + .sort(([left], [right]) => left.localeCompare(right)), + ); +} + +function difference(path, recorded, current) { + if (JSON.stringify(recorded) === JSON.stringify(current)) return null; + return { path, recorded, current }; +} + +const EXPERIMENTAL_PRECISION_KEYS = new Set([ + "dit_precision", + "text_encoder_precisions", + "image_encoder_precision", + "image_encoder_precisions", + "vae_precision", + "vae_decode_precision", +]); + +function executionFingerprint({ model, config, env = {} }) { + const execution = resolveExecution({ model, config, env }); + const sampling = config.default_request?.sampling ?? {}; + const components = config.generator?.pipeline?.components ?? {}; + const quantization = Object.fromEntries( + Object.entries(execution.quantization).sort(([left], [right]) => left.localeCompare(right)), + ); + const experimental = Object.fromEntries( + Object.entries(normalizedExperimental(execution.experimental)) + .filter(([key]) => !EXPERIMENTAL_PRECISION_KEYS.has(key)) + .sort(([left], [right]) => left.localeCompare(right)), + ); + return { + "generator.model_path": config.generator?.model_path ?? null, + "generator.revision": config.generator?.revision ?? null, + "generator.engine.num_gpus": execution.num_gpus, + "generator.engine.use_fsdp_inference": execution.use_fsdp, + "generator.engine.parallelism.tp_size": execution.topology.tpSize, + "generator.engine.parallelism.sp_size": execution.topology.spSize, + "generator.engine.offload.dit": execution.offload.dit, + "generator.engine.offload.dit_layerwise": execution.offload.dit_layerwise, + "generator.engine.offload.text_encoder": execution.offload.text_encoder, + "generator.engine.offload.image_encoder": execution.offload.image_encoder, + "generator.engine.offload.vae": execution.offload.vae, + "generator.engine.offload.lazy_module_load": execution.lazy_module_load, + "generator.engine.compile": { + enabled: execution.compile, + text_encoder_enabled: config.generator?.engine?.compile?.text_encoder_enabled === true, + vae_enabled: config.generator?.engine?.compile?.vae_enabled === true, + }, + "generator.engine.quantization": quantization, + "generator.pipeline.workload_type": execution.workload ?? null, + "generator.pipeline.vae_tiling": execution.vae_tiling, + "generator.pipeline.vae_sp": execution.vae_sp, + "generator.pipeline.precision": execution.precision, + "generator.pipeline.experimental": experimental, + "generator.pipeline.components.transformer_weights": components.transformer_weights ?? null, + "generator.pipeline.components.vae_weights": components.vae_weights ?? null, + "generator.pipeline.components.text_encoder_weights": components.text_encoder_weights ?? null, + "default_request.sampling.height": sampling.height ?? null, + "default_request.sampling.width": sampling.width ?? null, + "default_request.sampling.num_frames": sampling.num_frames ?? null, + "default_request.sampling.num_videos_per_prompt": sampling.num_videos_per_prompt ?? null, + "default_request.sampling.batch_cfg": sampling.batch_cfg ?? false, + }; +} + +function configDifferences(model, recorded = {}, current = {}) { + const recordedFingerprint = executionFingerprint({ model, config: recorded }); + const currentFingerprint = executionFingerprint({ model, config: current }); + const differences = []; + for (const [path, recordedValue] of Object.entries(recordedFingerprint)) { + const item = difference(path, recordedValue, currentFingerprint[path]); + if (item) differences.push(item); + } + return differences; +} + +function revisionDifferences(recorded = {}, current) { + if (!isObject(current)) return { status: "unknown", differences: [] }; + const differences = []; + for (const key of ["fastvideo", "model"]) { + const value = current[key]; + if (value === undefined) return { status: "unknown", differences }; + if (value !== recorded[key]) + differences.push({ path: `revisions.${key}`, recorded: recorded[key], current: value }); + } + const recordedComponents = recorded.components ?? {}; + const currentComponents = current.components; + if (Object.keys(recordedComponents).length && currentComponents === undefined) { + return { status: "unknown", differences }; + } + if (currentComponents !== undefined) { + for (const role of new Set([...Object.keys(recordedComponents), ...Object.keys(currentComponents)])) { + if (recordedComponents[role] !== currentComponents[role]) { + differences.push({ + path: `revisions.components.${role}`, + recorded: recordedComponents[role], + current: currentComponents[role], + }); + } + } + } + return { status: differences.length ? "mismatched" : "matched", differences }; +} + +/** + * Match completed-run evidence to the current deployment, configuration, request, environment and + * hardware. Non-memory settings (server port, prompt, seed, fps) do not invalidate evidence. + */ +export function matchEvidence({ model, example, config, request, env = example?.env ?? {}, hardware, revisions } = {}) { + const applicable = []; + const unverified = []; + const rejected = []; + const currentConfig = config ?? example?.base_config; + const currentRequest = request ?? { sampling: currentConfig?.default_request?.sampling ?? {} }; + const currentEnv = env ?? example?.env ?? {}; + for (const run of model.reference_runs ?? []) { + const differences = []; + if (run.hardware !== hardware?.id) { + differences.push({ path: "hardware", recorded: run.hardware, current: hardware?.id }); + } + const partition = hardware?.partition?.id ?? hardware?.partition_id ?? null; + const recordedPartition = run.hardware_partition ?? null; + if (partition !== recordedPartition) { + differences.push({ path: "hardware_partition", recorded: recordedPartition, current: partition }); + } + const allocationGiB = Number.isFinite(hardware?.allocationGiB) + ? hardware.allocationGiB + : Number.isFinite(hardware?.allocationMemoryGb) + ? (hardware.allocationMemoryGb * 1e9) / GIB + : null; + const recordedAllocationGb = run.hardware_allocation_gb ?? null; + if (partition === null && (allocationGiB !== null || recordedAllocationGb !== null)) { + const allocationGb = allocationGiB === null ? null : (allocationGiB * GIB) / 1e9; + if ( + allocationGb === null || + recordedAllocationGb === null || + Math.abs(allocationGb - recordedAllocationGb) > 1e-6 + ) { + differences.push({ + path: "hardware_allocation_gb", + recorded: recordedAllocationGb, + current: allocationGb, + }); + } + } else if (partition !== null && recordedAllocationGb !== null && allocationGiB !== null) { + const allocationGb = (allocationGiB * GIB) / 1e9; + if (Math.abs(allocationGb - recordedAllocationGb) > 1e-6) { + differences.push({ + path: "hardware_allocation_gb", + recorded: recordedAllocationGb, + current: allocationGb, + }); + } + } + if (run.gpu_count !== (currentConfig?.generator?.engine?.num_gpus ?? 1)) { + differences.push({ + path: "gpu_count", + recorded: run.gpu_count, + current: currentConfig?.generator?.engine?.num_gpus ?? 1, + }); + } + if (example && run.context.deployment !== example.deployment_id) { + differences.push({ + path: "context.deployment", + recorded: run.context.deployment, + current: example.deployment_id, + }); + } + if (example && run.context.runtime !== example.runtime) { + differences.push({ path: "context.runtime", recorded: run.context.runtime, current: example.runtime }); + } + const recordedConfig = { ...run.context.config, default_request: { sampling: run.context.request.sampling } }; + const currentSamplingConfig = { + ...currentConfig, + default_request: { sampling: currentRequest.sampling ?? currentConfig?.default_request?.sampling }, + }; + differences.push(...configDifferences(model, recordedConfig, currentSamplingConfig)); + const trajectory = (runtime) => ({ + latents: Boolean(runtime?.return_trajectory_latents), + decoded: Boolean(runtime?.return_trajectory_decoded), + }); + const recordedTrajectory = trajectory(run.context.request.runtime); + const currentTrajectory = trajectory(currentRequest.runtime ?? currentConfig?.default_request?.runtime); + if ( + recordedTrajectory.latents !== currentTrajectory.latents || + recordedTrajectory.decoded !== currentTrajectory.decoded + ) { + differences.push({ + path: "request.runtime.trajectory", + recorded: recordedTrajectory, + current: currentTrajectory, + }); + } + const envDifference = difference("context.env", memoryEnv(run.context.env), memoryEnv(currentEnv)); + if (envDifference) differences.push(envDifference); + const revisionsMatch = revisionDifferences(run.context.revisions, revisions); + const entry = { + id: run.id, + date: run.date, + execution: run.execution, + evidence_url: run.evidence_url, + memory: run.memory ?? null, + revision_status: revisionsMatch.status, + revision_differences: revisionsMatch.differences, + applicable: differences.length === 0 && revisionsMatch.status === "matched", + differences, + }; + if (entry.applicable) applicable.push(entry); + else if (differences.length === 0) unverified.push(entry); + else rejected.push(entry); + } + return { applicable, unverified, rejected }; +} + +function measuredDevicePeak(evidence, device) { + const capacity = device?.capacityGiB; + if (!(capacity > 0)) return null; + const measurements = []; + for (const run of evidence.applicable) { + if (run.memory?.scope !== "device" || !Array.isArray(run.memory.per_gpu) || !run.memory.per_gpu.length) continue; + const startupMeasured = run.memory.per_gpu.every((gpu) => Number.isFinite(gpu.startup_peak)); + const requestPeak = Math.max(...run.memory.per_gpu.map((gpu) => gpu.request_peak)); + const peak = Math.max( + requestPeak, + ...run.memory.per_gpu.map((gpu) => (Number.isFinite(gpu.startup_peak) ? gpu.startup_peak : 0)), + ); + measurements.push({ + run: run.id, + metric: run.memory.metric, + scope: run.memory.scope, + execution: run.execution, + requestPeak, + peak, + startupMeasured, + }); + } + if (!measurements.length) return null; + const highest = measurements.reduce((best, item) => (item.peak > best.peak ? item : best)); + const base = { + basis: "measured", + peakGiB: highest.peak, + capacityGiB: capacity, + run: highest.run, + runs: measurements.map((item) => item.run), + metric: highest.metric, + scope: highest.scope, + execution: highest.execution, + }; + const unmeasured = measurements.filter((item) => !item.startupMeasured); + if (unmeasured.length) { + return { + ...base, + status: "unknown", + reason: `Startup peak was not measured for ${unmeasured.map((item) => item.run).join(", ")}; a request-only peak cannot establish device fit.`, + }; + } + return { + ...base, + status: highest.peak <= capacity ? "fits" : "exceeds", + spreadGiB: Math.max(...measurements.map((item) => item.peak)) - Math.min(...measurements.map((item) => item.peak)), + }; +} + +/** + * Combine the analytical estimate with applicable completed-run evidence. A successful generation + * without a device-wide measurement establishes compatibility, not a memory minimum or fit. + */ +export function assessCapacity({ + model, + example, + hardware, + config = example?.base_config, + request, + env = example?.env ?? {}, + revisions, + headroom, + memoryBudgetGiB, + allocationMemoryGb, + partitionId, + hostMemoryGb, +} = {}) { + const sampling = request?.sampling ?? config?.default_request?.sampling ?? {}; + const workload = example?.workload ?? config?.generator?.pipeline?.workload_type; + const device = normalizeHardware(hardware, { memoryBudgetGiB, allocationMemoryGb, partitionId }); + const analytical = estimateMemory({ + config, + workload, + model, + hardware: device ?? hardware, + request, + env, + headroom, + memoryBudgetGiB, + allocationMemoryGb, + partitionId, + hostMemoryGb, + }); + const evidence = matchEvidence({ + model, + example, + config, + request: { sampling, runtime: request?.runtime ?? config?.default_request?.runtime }, + env, + hardware: device ?? hardware, + revisions, + }); + const measured = analytical.status === "unsupported" ? null : measuredDevicePeak(evidence, device); + const compatibility = evidence.applicable.length + ? "completed" + : evidence.unverified.length + ? "completed_unverified_revision" + : "unverified"; + if (measured) { + const hostFailure = analytical.host?.status === "exceeds"; + return { + ...measured, + status: hostFailure ? "exceeds" : measured.status, + deviceStatus: measured.status, + reason: hostFailure + ? `Host memory for offload exceeds the ${analytical.host.capacityGiB.toFixed(1)} GiB available.` + : measured.reason, + analytical, + evidence, + compatibility, + hardware: analytical.hardware ?? device, + memory: analytical.memory ?? null, + host: analytical.host ?? null, + notes: [ + `Measured ${measured.metric} (${measured.scope}) on ${analytical.hardware?.name ?? hardware?.name ?? "the selected device"} for run ${measured.run}.`, + hostFailure + ? `Host memory for offload exceeds the ${analytical.host.capacityGiB.toFixed(1)} GiB available; a device measurement does not establish host fit.` + : null, + analytical.status === "unknown" + ? `Analytical coverage remains unknown: ${analytical.reason}` + : "The analytical estimate is retained for comparison.", + ].filter(Boolean), + }; + } + return { + ...analytical, + analytical, + evidence, + compatibility, + notes: [ + ...(analytical.notes ?? []), + ...(evidence.applicable.length + ? ["Applicable evidence records a completed generation but no device-wide memory peak."] + : []), + ...(evidence.unverified.length + ? ["A completed run matches the memory-relevant settings, but its revisions are not verified."] + : []), + ...(evidence.rejected.length ? ["No evidence matches every memory-relevant setting."] : []), + ], + }; +} + +function lowerMemoryConfigs({ model, example, hardware, config, request, env, revisions, headroom }) { + const alternatives = []; + const sampling = request?.sampling ?? config.default_request?.sampling ?? {}; + const assess = (mutate, edit) => { + const changed = structuredClone(config); + const changedRequest = request ? structuredClone(request) : undefined; + mutate(changed, changedRequest); + const result = assessCapacity({ + model, + example, + hardware, + config: changed, + request: changedRequest, + env, + revisions, + headroom, + }); + alternatives.push({ + changes: [edit], + status: result.status, + basis: result.basis ?? null, + required_gib: result.requiredGiB ?? null, + reason: result.reason ?? null, + }); + }; + const samplingChange = (key, value) => + assess( + (changed, changedRequest) => { + if (changedRequest) { + changedRequest.sampling = { ...sampling, [key]: value }; + } else { + changed.default_request = { + ...(changed.default_request ?? {}), + sampling: { ...sampling, [key]: value }, + }; + } + }, + { path: request ? `request.sampling.${key}` : `default_request.sampling.${key}`, value }, + ); + if (isCount(sampling.num_frames) && sampling.num_frames > 1) { + const frames = Math.max(1, Math.floor((sampling.num_frames - 1) / 2) + 1); + if (frames < sampling.num_frames) samplingChange("num_frames", frames); + } + for (const key of ["height", "width"]) { + if (isCount(sampling[key]) && sampling[key] > 16) { + const value = Math.max(16, Math.floor(sampling[key] / 2 / 16) * 16); + if (value < sampling[key]) samplingChange(key, value); + } + } + const offload = config.generator?.engine?.offload ?? {}; + const changes = {}; + for (const key of ["dit_layerwise", "text_encoder", "vae"]) { + if (offload[key] === false) changes[`generator.engine.offload.${key}`] = true; + } + if (Object.keys(changes).length) { + assess( + (changed) => { + for (const [key, value] of Object.entries(changes)) { + const parts = key.split("."); + let cursor = changed; + for (const part of parts.slice(0, -1)) cursor = cursor[part] ??= {}; + cursor[parts.at(-1)] = value; + } + }, + { path: "generator.engine.offload", value: changes }, + ); + } + return alternatives; +} +/** + * Rank maintained examples for one hardware/task choice. Ranking uses measured evidence first, then + * analytical memory fit. No speed or quality ordering is inferred from memory estimates. + */ +export function recommendCapacity({ models, hardware, task, request, env, revisions, headroom, limit = 3 } = {}) { + const candidates = []; + for (const model of models ?? []) { + for (const example of model.examples ?? []) { + if (task && example.workload !== task) continue; + const config = example.base_config; + const assessment = assessCapacity({ + model, + example, + hardware, + config, + request, + env: env ?? example.env, + revisions, + headroom, + }); + candidates.push({ model, example, assessment }); + } + } + const rank = (candidate) => { + const { status, basis } = candidate.assessment; + if (status === "fits" && basis === "measured") return 0; + if (status === "fits") return 1; + if (status === "unknown") return 2; + if (status === "exceeds") return 3; + return 4; + }; + candidates.sort((left, right) => { + const delta = rank(left) - rank(right); + if (delta) return delta; + const leftMemory = left.assessment.requiredGiB ?? left.assessment.peakGiB ?? Infinity; + const rightMemory = right.assessment.requiredGiB ?? right.assessment.peakGiB ?? Infinity; + return leftMemory - rightMemory; + }); + return candidates.slice(0, limit).map(({ model, example, assessment }) => ({ + model: { id: model.id, name: model.name, model_id: model.model_id }, + example: { + id: example.id, + deployment_id: example.deployment_id, + workload: example.workload, + runtime: example.runtime, + }, + assessment, + reasons: [ + assessment.status === "fits" + ? `${assessment.basis === "measured" ? "Measured" : "Estimated"} to fit ${hardware?.name ?? "the selected device"}.` + : assessment.status === "exceeds" + ? `Estimated to exceed ${hardware?.name ?? "the selected device"}.` + : `Fit is unknown: ${assessment.reason ?? "coverage is incomplete"}.`, + assessment.compatibility === "completed" + ? "Applicable evidence records a completed generation." + : "No applicable completed-run evidence.", + ], + lower_memory: + assessment.status === "fits" + ? [] + : lowerMemoryConfigs({ + model, + example, + hardware, + config: example.base_config, + request, + env: env ?? example.env, + revisions, + headroom, + }), + })); +} diff --git a/docs/js/cookbook-workloads.mjs b/docs/js/cookbook-workloads.mjs new file mode 100644 index 000000000..d2820a60a --- /dev/null +++ b/docs/js/cookbook-workloads.mjs @@ -0,0 +1,45 @@ +/** + * Shared workload IDs and media direction. Endpoint and request-field choices mirror the + * serving API contract; callers must not infer support from an ID alone. + */ +export const WORKLOADS = Object.freeze({ + t2v: Object.freeze({ + input: "text", + output: "video", + endpoint: "/v1/videos/sync", + extension: "mp4", + media_type: "video/mp4", + }), + i2v: Object.freeze({ + input: "image", + output: "video", + endpoint: "/v1/videos/sync", + extension: "mp4", + media_type: "video/mp4", + reference: "input_reference", + }), + t2i: Object.freeze({ + input: "text", + output: "image", + endpoint: "/v1/images/generations", + extension: "json", + media_type: "application/json", + }), + t2a: Object.freeze({ + input: "text", + output: "audio", + endpoint: "/v1/videos/sync", + extension: "wav", + media_type: "audio/wav", + }), + v2a: Object.freeze({ + input: "video", + output: "audio", + endpoint: "/v1/videos/sync", + extension: "wav", + media_type: "audio/wav", + reference: "video_reference", + }), +}); + +export const WORKLOAD_IDS = Object.freeze(Object.keys(WORKLOADS)); diff --git a/docs/node_modules b/docs/node_modules new file mode 120000 index 000000000..d3a232447 --- /dev/null +++ b/docs/node_modules @@ -0,0 +1 @@ +/Users/aryank/os/FastVideo-review-int/docs/node_modules \ No newline at end of file diff --git a/docs/tests/cookbook-capacity.test.mjs b/docs/tests/cookbook-capacity.test.mjs index abee72d38..5720cdb5a 100644 --- a/docs/tests/cookbook-capacity.test.mjs +++ b/docs/tests/cookbook-capacity.test.mjs @@ -46,11 +46,17 @@ function fixture(t) { const hardware = { hardware: { "example-gpu": { - name: "Fixture GPU", - memory_type: "dedicated", - memory_gb: 24, + name: "Fixture GPU 24 GB", + vendor: "nvidia", + kind: "gpu", + family: "fixture-gpu", + variant: "Fixture 24 GB", + form_factor: "desktop", + memory: { type: "dedicated", nominal: { value: 24, unit: "GB" } }, backend: "cuda", - sources: [SOURCE], + supported_dtypes: ["fp32", "fp16", "bf16"], + aliases: ["fixture"], + sources: [{ url: SOURCE, checked: "2026-10-19" }], }, }, }; @@ -196,7 +202,10 @@ function referenceRun(f) { test("capacity export resolves shared metadata and preserves native example configuration", (t) => { const f = fixture(t); const data = buildCapacityCatalog({ root: f.root }); - assert.deepEqual(data.hardware["example-gpu"], f.hardware.hardware["example-gpu"]); + assert.equal(data.hardware["example-gpu"].id, "example-gpu"); + assert.equal(data.hardware["example-gpu"].memory_gb, 24); + assert.deepEqual(data.hardware["example-gpu"].aliases, ["fixture"]); + assert.equal(data.hardware["example-gpu"].memory.type, "dedicated"); const model = data.models[0]; assert.equal(model.id, "example-model"); assert.equal(model.name, f.manifest.title); @@ -437,15 +446,82 @@ test("hardware and components require source URLs and do not fix a runtime dtype f.save(); assert.throws(() => buildCapacityCatalog({ root: f.root }), /source|URL/i); transformer(f).sources = [SOURCE]; - f.hardware.hardware["example-gpu"].sources = ["not-a-url"]; + f.hardware.hardware["example-gpu"].sources = [{ url: "not-a-url", checked: "2026-10-19" }]; f.save(); assert.throws(() => buildCapacityCatalog({ root: f.root }), /source|URL|https/i); - f.hardware.hardware["example-gpu"].sources = [SOURCE]; + f.hardware.hardware["example-gpu"].sources = [{ url: SOURCE, checked: "2026-10-19" }]; transformer(f).dtype = "bf16"; f.save(); assert.throws(() => buildCapacityCatalog({ root: f.root }), /dtype|unknown|unsupported/i); }); +test("the authored hardware inventory is sourced, unique and keeps unified memory explicit", () => { + const catalog = buildCapacityCatalog({ root: ROOT }).hardware; + const devices = Object.values(catalog); + assert.equal(devices.length, 52); + assert.equal(new Set(devices.map((device) => device.id)).size, devices.length); + const aliases = new Map(); + for (const device of devices) { + assert.ok(device.sources.length > 0, `${device.id}: missing source`); + assert.match(device.sources[0].checked, /^\d{4}-\d{2}-\d{2}$/); + assert.ok(["dedicated", "unified"].includes(device.memory.type), `${device.id}: memory type`); + assert.ok(device.memory.nominal.value > 0, `${device.id}: capacity`); + for (const alias of device.aliases) { + assert.equal(aliases.has(alias), false, `${alias} is assigned to ${aliases.get(alias)} and ${device.id}`); + aliases.set(alias, device.id); + } + for (const partition of device.partitions ?? []) { + assert.ok(partition.memory_gb > 0 && partition.memory_gb <= device.memory_gb); + assert.match(partition.id, /^[a-z0-9][a-z0-9.-]*$/); + } + } + const spark = catalog["dgx-spark-gb10-128gb"]; + assert.equal(spark.memory.type, "unified"); + assert.equal(spark.memory_gb, 128); + assert.equal(spark.memory_type, undefined); + assert.equal(catalog["h100-sxm-80gb"].partitions.length, 3); + assert.deepEqual( + catalog["a100-sxm-80gb"].partitions.map((partition) => partition.id), + ["1g.10gb", "2g.20gb", "3g.40gb", "7g.80gb"], + ); + assert.equal(catalog["gb200-186gb"].gpu_count, 1); +}); + +test("hardware aliases, budgets and partitions cannot contradict the device", (t) => { + const f = fixture(t); + f.hardware.hardware["example-gpu"].aliases = "fixture"; + f.save(); + assert.throws(() => buildCapacityCatalog({ root: f.root }), /aliases/i); + f.hardware.hardware["example-gpu"].aliases = ["fixture", "FIXTURE"]; + f.save(); + assert.throws(() => buildCapacityCatalog({ root: f.root }), /repeats its own identity/i); + f.hardware.hardware["example-gpu"].aliases = ["fixture"]; + f.hardware.hardware["other-gpu"] = { + ...clone(f.hardware.hardware["example-gpu"]), + name: "Other GPU", + aliases: ["fixture"], + }; + f.save(); + assert.throws(() => buildCapacityCatalog({ root: f.root }), /alias|duplicate/i); + delete f.hardware.hardware["other-gpu"]; + f.hardware.hardware["example-gpu"].memory.usable = { value: 25, unit: "GB" }; + f.save(); + assert.throws(() => buildCapacityCatalog({ root: f.root }), /usable/i); + f.hardware.hardware["example-gpu"].memory.usable = { value: 20, unit: "GB" }; + f.hardware.hardware["example-gpu"].memory.policy = "Fixture application budget"; + f.hardware.hardware["example-gpu"].partitions = [ + { id: "1g.25gb", label: "1g.25gb", memory: { value: 25, unit: "GB" } }, + ]; + f.save(); + assert.throws(() => buildCapacityCatalog({ root: f.root }), /partition/i); + delete f.hardware.hardware["example-gpu"].partitions; + delete f.hardware.hardware["example-gpu"].memory.usable; + delete f.hardware.hardware["example-gpu"].memory.policy; + f.hardware.hardware["example-gpu"].memory.type = "unified"; + f.save(); + assert.equal(buildCapacityCatalog({ root: f.root }).hardware["example-gpu"].memory.type, "unified"); +}); + test("hardware format names are known, unique and nonempty", (t) => { const f = fixture(t); const hardware = f.hardware.hardware["example-gpu"]; @@ -623,7 +699,12 @@ test("exports use relative URLs and browser loaders resolve them under a deploym assert.equal(model.components.vae.decode_feature_multiplier, f.vaes.vaes["example-vae"].decode_feature_multiplier); assert.deepEqual(model.examples[0].base_config, f.config); const hardware = await loadCapacityHardware(loadedIndex, { fetcher, signal }); - assert.deepEqual(hardware["example-gpu"], f.hardware.hardware["example-gpu"]); + assert.equal(hardware.version, 1); + assert.equal(hardware.devices.length, 1); + assert.equal(hardware.devices[0].id, "example-gpu"); + assert.equal(hardware.devices[0].memory_gb, 24); + assert.deepEqual(hardware.devices[0].aliases, ["fixture"]); + assert.equal(hardware.url, new URL(index.hardware_url, indexUrl).href); assert.deepEqual(requested, [ indexUrl, new URL(index.models[0].catalog_url, indexUrl).href, @@ -660,7 +741,7 @@ test("browser loaders reject malformed or mismatched catalog responses", async ( await assert.rejects( () => loadCapacityHardware(index, { - fetcher: fetcher({ gpu: { name: "Invalid capacity", memory_type: "dedicated", memory_gb: -1 } }), + fetcher: fetcher({ version: 1, devices: [{ id: "gpu", name: "Invalid capacity" }] }), }), /hardware/i, ); @@ -735,3 +816,18 @@ test( assert.equal(readFileSync(sentinel, "utf8"), "unchanged\n"); }, ); + +test("the shipped hardware catalog passes the browser loader", async () => { + // Same shape exportCapacityCatalog writes to hardware.json. + const hardware = Object.values(buildCapacityCatalog().hardware); + const files = { + "https://docs.example/capacity/index.json": { hardware_url: "hardware.json", models: [] }, + "https://docs.example/capacity/hardware.json": { version: 1, devices: hardware }, + }; + const fetcher = async (url) => ({ ok: url in files, status: 404, url, json: async () => files[url] }); + const index = await loadCapacityIndex("https://docs.example/capacity/index.json", { fetcher }); + const loaded = await loadCapacityHardware(index, { fetcher }); + assert.equal(loaded.devices.length, hardware.length); + const a100 = loaded.devices.find((device) => device.id === "a100-sxm-80gb"); + assert.ok(a100.partitions.some((partition) => partition.id === "7g.80gb" && partition.memory_gb === a100.memory_gb)); +}); diff --git a/docs/tests/cookbook-config.test.mjs b/docs/tests/cookbook-config.test.mjs index f051f8f2c..36c26869f 100644 --- a/docs/tests/cookbook-config.test.mjs +++ b/docs/tests/cookbook-config.test.mjs @@ -99,6 +99,8 @@ test("browser bundle exposes reusable methods and its error class with no DOM or ); assert.deepEqual(Object.keys(context.FastVideoConfigCookbook).sort(), [ "CookbookValidationError", + "WORKLOADS", + "assessCapacity", "estimateMemory", "getOptions", "loadCapacityHardware", @@ -106,7 +108,10 @@ test("browser bundle exposes reusable methods and its error class with no DOM or "loadCapacityModel", "loadDeployment", "loadIndex", + "matchEvidence", + "recommendCapacity", "resolveConfig", + "resolveExecution", "sampleRequest", ]); const api = context.FastVideoConfigCookbook; @@ -478,6 +483,30 @@ test("sample requests follow effective alias, host and port without repinning re assert.equal(resolveConfig(image).clientRequest.input_reference, "/absolute/path/to/first-frame.png"); }); +test("sample requests follow shared workload media", () => { + const expectations = { + t2i: { endpoint: /\/v1\/images\/generations/, output: /--output output\.json/, reference: null }, + t2a: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.wav/, reference: null }, + v2a: { endpoint: /\/v1\/videos\/sync/, output: /--output output\.wav/, reference: "video_reference" }, + }; + for (const [workload, expected] of Object.entries(expectations)) { + const data = catalog(); + data.workload = workload; + const result = resolveConfig(data); + assert.match(result.clientCommand, expected.endpoint); + assert.match(result.clientCommand, expected.output); + assert.equal(result.clientRequest.input_reference, undefined); + if (expected.reference) { + assert.deepEqual(result.clientRequest.video_reference, { video_url: "/absolute/path/to/input.mp4" }); + } else { + assert.equal(result.clientRequest.video_reference, undefined); + } + } + const invalid = catalog(); + invalid.workload = "t2x"; + assert.throws(() => resolveConfig(invalid), /Unsupported workload/); +}); + test("LoRA sample requests use the startup adapter instead of an edited server alias", () => { const data = catalog(); const components = { lora_path: "/models/adapter.safetensors", lora_nickname: "my-adapter" }; diff --git a/docs/tests/cookbook-memory.test.mjs b/docs/tests/cookbook-memory.test.mjs index 93e6066bc..d41c6f79a 100644 --- a/docs/tests/cookbook-memory.test.mjs +++ b/docs/tests/cookbook-memory.test.mjs @@ -1,18 +1,21 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { buildCatalogs } from "../js/build-cookbook-config.mjs"; -import { resolveConfig } from "../js/cookbook-config.mjs"; +import { buildCapacityCatalog } from "../js/build-cookbook-capacity.mjs"; import { BYTES_PER_WEIGHT, DEFAULT_HEADROOM, - WAN_RULE, + assessCapacity, estimateMemory, + matchEvidence, + recommendCapacity, + resolveExecution, resolveTopology, } from "../js/cookbook-memory.mjs"; const GIB = 1024 ** 3; -const NO_OFFLOAD = { dit_layerwise: false, text_encoder: false, vae: false }; +const NO_OFFLOAD = { dit: false, dit_layerwise: false, text_encoder: false, vae: false }; +const FASTWAN_OFFLOAD = { dit: false, dit_layerwise: true, text_encoder: true, vae: false }; const DIT = 1_418_996_800; const DIT_BLOCKS = 1_393_221_120; @@ -20,46 +23,102 @@ const TEXT = 5_680_910_336; const TEXT_BLOCKS = 4_630_757_376; const VAE = 73_296_659; -/** Wan2.1 T2V 1.3B; block totals follow WanTransformerBlock and the UMT5-XXL encoder layer shapes. */ -function wanModel() { - return { +function wanModel(change = () => {}) { + const model = { + id: "wan21", + name: "Wan2.1 T2V 1.3B", + model_id: "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", memory_rules: "wan", - geometry: { - temporal_compression: 4, - spatial_compression: 8, - patch_size: [1, 2, 2], - hidden_size: 1536, - ffn_size: 8960, - vae_decoder_channels: 96, - }, - components: [ - { - role: "text_encoder", + components: { + transformer: { + ref: "wan2.1-t2v-1.3b-dit", + kind: "transformer", + parameter_count: DIT, + block_count: 30, + block_parameter_count: DIT_BLOCKS, + geometry: { hidden_size: 1536, ffn_size: 8960, patch_size: [1, 2, 2], gated_ffn: false }, + }, + text_encoder: { + ref: "umt5-xxl-encoder", + kind: "encoder", parameter_count: TEXT, - dtype: "fp32", block_count: 24, block_parameter_count: TEXT_BLOCKS, - streams_when_offloaded: true, }, - { role: "transformer", parameter_count: DIT, dtype: "bf16", block_count: 30, block_parameter_count: DIT_BLOCKS }, - { role: "vae", parameter_count: VAE, dtype: "fp32", activation_dtype: "bf16" }, - ], + vae: { + ref: "wan2.1-vae-decoder", + kind: "vae", + parameter_count: VAE, + geometry: { + temporal_compression: 4, + spatial_compression: 8, + latent_channels: 16, + output_channels: 3, + vae_decoder_channels: 96, + }, + decode_feature_multiplier: 48, + }, + }, + examples: [], + reference_runs: [], + }; + change(model); + return model; +} + +function hardware({ + id = "test-80gb", + memory_gb = 80, + memory_type = "dedicated", + backend = "cuda", + dtypes = ["fp32", "fp16", "bf16", "fp8", "nvfp4"], + host_memory_gb, + partitions, +} = {}) { + return { + id, + name: `Test GPU ${memory_gb} GB`, + memory_type, + memory_gb, + backend, + supported_dtypes: dtypes, + ...(host_memory_gb === undefined ? {} : { host_memory_gb }), + ...(partitions === undefined ? {} : { partitions }), }; } -function gpu(value, { unit = "GiB", dtypes = ["fp32", "fp16", "bf16"] } = {}) { - return { name: "Test GPU", type: "discrete_gpu", memory_per_device: { value, unit }, supported_dtypes: dtypes }; +function config({ offload = FASTWAN_OFFLOAD, engine = {}, pipeline = {}, sampling, server = {} } = {}) { + return { + generator: { + model_path: "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", + engine: { num_gpus: 1, offload, ...engine }, + pipeline, + }, + server: { host: "0.0.0.0", port: 8000, ...server }, + default_request: { sampling: sampling ?? { num_frames: 81, height: 480, width: 832, seed: 1024, fps: 16 } }, + }; } -function config({ offload = NO_OFFLOAD, engine = {}, pipeline = {}, sampling } = {}) { +function example(base = config(), env = {}) { return { - generator: { engine: { num_gpus: 1, offload, ...engine }, pipeline }, - default_request: { sampling: sampling ?? { num_frames: 81, height: 480, width: 832 } }, + id: "cuda-rest", + deployment_id: "wan21/cuda-rest", + workload: "t2v", + runtime: "fastvideo-cuda-rest", + config: "examples/serving/openai_wan21.yaml", + env, + base_config: base, }; } function estimate(options = {}) { - return estimateMemory({ config: config(), workload: "t2v", model: wanModel(), hardware: gpu(80), ...options }); + return estimateMemory({ + config: config(), + workload: "t2v", + model: wanModel(), + hardware: hardware({ memory_gb: 80 }), + ...options, + }); } const gib = (parameters, dtype) => (parameters * BYTES_PER_WEIGHT[dtype]) / GIB; @@ -78,177 +137,721 @@ test("each storage format has its byte size per weight", () => { assert.deepEqual(BYTES_PER_WEIGHT, { fp32: 4, fp16: 2, bf16: 2, fp8: 1, nvfp4: 0.5625 }); }); -test("automatic parallel degrees resolve like FastVideoArgs", () => { +test("automatic topology follows the native -1 sentinels", () => { assert.deepEqual(resolveTopology(), { gpus: 1, tpSize: 1, spSize: 1 }); - assert.deepEqual(resolveTopology({ num_gpus: 4, parallelism: { tp_size: -1, sp_size: -1 } }), { + assert.deepEqual(resolveTopology({ num_gpus: 4 }), { gpus: 4, tpSize: 1, spSize: 4 }); + assert.deepEqual(resolveTopology({ num_gpus: 4, parallelism: { tp_size: 2, sp_size: -1 } }), { gpus: 4, - tpSize: 1, + tpSize: 2, spSize: 4, }); - assert.equal(resolveTopology({ num_gpus: 1, parallelism: { tp_size: 2, sp_size: 1 } }).gpus, 2); }); -test("component weights match the Wan 1.3B sizes in the VRAM design proposal", () => { - assert.equal(gib(DIT, "bf16").toFixed(2), "2.64"); - assert.equal(gib(TEXT, "bf16").toFixed(2), "10.58"); - assert.equal(gib(VAE, "fp32").toFixed(2), "0.27"); +test("native Wan precision defaults resolve without metadata dtypes", () => { + const execution = resolveExecution({ model: wanModel(), config: config({ offload: {} }) }); + assert.deepEqual(execution.precision, { + transformer: "bf16", + text_encoder: ["fp32"], + image_encoder: ["fp32"], + vae: "fp32", + vae_decode: "bf16", + }); + assert.equal(execution.offload.dit_layerwise, true); + assert.equal(execution.vae_tiling, false); }); -test("without offload every component stays resident in every phase", () => { - const total = gib(TEXT, "fp32") + gib(DIT, "bf16") + gib(VAE, "fp32"); - assertWeights(estimate(), { encoding: total, denoising: total, decoding: total }); +test("explicit serving precision and offload settings override family defaults", () => { + const execution = resolveExecution({ + model: wanModel(), + config: config({ + offload: { dit_layerwise: false, text_encoder: false, vae: false }, + pipeline: { + experimental: { + dit_precision: "fp16", + text_encoder_precisions: ["bf16"], + vae_precision: "bf16", + vae_decode_precision: "fp32", + }, + }, + }), + }); + assert.equal(execution.precision.transformer, "fp16"); + assert.deepEqual(execution.precision.text_encoder, ["bf16"]); + assert.equal(execution.precision.vae, "bf16"); + assert.equal(execution.precision.vae_decode, "fp32"); + assert.equal(execution.offload.dit_layerwise, false); }); -test("an offloaded component occupies the GPU only during its own phase", () => { - const model = wanModel(); - delete model.components[0].streams_when_offloaded; +test("startup, encoding, denoising and decoding keep only resident weights", () => { + const result = estimate(); + const ditOutside = gib(DIT - DIT_BLOCKS, "bf16"); + assert.equal(result.status, "fits"); + assert.equal(result.basis, "analytical"); + assertWeights(result, { + startup: gib(DIT, "bf16") + gib(VAE, "fp32"), + encoding: gib(TEXT, "fp32") + ditOutside + gib(VAE, "fp32"), + denoising: streamed(DIT, DIT_BLOCKS, 30, "bf16") + gib(VAE, "fp32"), + decoding: ditOutside + gib(VAE, "fp32"), + }); + assert.equal(result.peakPhase, "encoding"); + close(result.requiredGiB, result.peakGiB * (1 + DEFAULT_HEADROOM.fraction) + DEFAULT_HEADROOM.reserveGiB); +}); + +test("layerwise resident blocks stay on the device outside denoising", () => { + const result = estimate({ env: { FASTVIDEO_LAYERWISE_RESIDENT_BLOCKS: "4" } }); + const outside = gib(DIT - DIT_BLOCKS, "bf16"); + const blocks = gib(DIT_BLOCKS, "bf16"); + assertWeights(result, { + encoding: gib(TEXT, "fp32") + outside + (blocks * 4) / 30 + gib(VAE, "fp32"), + denoising: outside + (blocks * 6) / 30 + gib(VAE, "fp32"), + decoding: outside + (blocks * 4) / 30 + gib(VAE, "fp32"), + }); + assert.equal(result.execution.layerwise_resident_blocks, 4); +}); + +test("unified memory disables native CPU offload before counting weights", () => { + const unified = { + id: "unified", + name: "Fixture unified device", + memory: { type: "unified", nominal: { value: 128, unit: "GB" } }, + backend: "cuda", + supported_dtypes: ["fp32", "fp16", "bf16"], + }; const result = estimate({ - model, - config: config({ offload: { dit_layerwise: false, text_encoder: true, vae: true } }), + hardware: unified, + config: config({ + offload: { + dit: true, + dit_layerwise: true, + text_encoder: true, + image_encoder: true, + vae: true, + lazy_module_load: false, + }, + }), + }); + assert.equal(result.status, "fits"); + assert.deepEqual(result.execution.offload, { + dit: false, + dit_layerwise: false, + text_encoder: false, + image_encoder: false, + vae: false, + lazy_module_load: false, }); assertWeights(result, { - encoding: gib(TEXT, "fp32") + gib(DIT, "bf16"), - denoising: gib(DIT, "bf16"), + startup: gib(DIT, "bf16") + gib(TEXT, "fp32") + gib(VAE, "fp32"), + encoding: gib(DIT, "bf16") + gib(TEXT, "fp32") + gib(VAE, "fp32"), + denoising: gib(DIT, "bf16") + gib(TEXT, "fp32") + gib(VAE, "fp32"), + decoding: gib(DIT, "bf16") + gib(TEXT, "fp32") + gib(VAE, "fp32"), + }); + assert.ok(result.notes.some((note) => /offload is disabled/i.test(note))); +}); + +test("a disabled VAE feature cache returns unknown", () => { + const result = estimate({ + config: config({ pipeline: { experimental: { "vae_config.use_feature_cache": false } } }), + }); + assert.equal(result.status, "unknown"); + assert.match(result.reason, /feature caching/i); +}); + +test("trajectory capture returns unknown instead of a dense estimate", () => { + assert.notEqual(estimate().status, "unknown"); + for (const flag of ["return_trajectory_latents", "return_trajectory_decoded"]) { + const result = estimate({ request: { sampling: config().default_request.sampling, runtime: { [flag]: true } } }); + assert.equal(result.status, "unknown"); + assert.match(result.reason, /trajectory/i); + } +}); + +test("layerwise offload requires block counts", () => { + const model = wanModel((item) => { + delete item.components.transformer.block_count; + delete item.components.transformer.block_parameter_count; + }); + const result = estimate({ model }); + assert.equal(result.status, "unknown"); + assert.match(result.reason, /block counts/i); +}); + +test("a 24 GB device does not fit the fp32 UMT5 encoding phase", () => { + const result = estimate({ hardware: hardware({ memory_gb: 24 }) }); + assert.equal(result.status, "exceeds"); + assert.equal(result.peakPhase, "encoding"); + close(result.capacityGiB, 24e9 / GIB); +}); + +test("whole-model DiT offload keeps the transformer on the device in every phase", () => { + const result = estimate({ + config: config({ offload: { dit: true, dit_layerwise: false, text_encoder: true, vae: false } }), + }); + assertWeights(result, { + encoding: gib(DIT, "bf16") + gib(TEXT, "fp32") + gib(VAE, "fp32"), + denoising: gib(DIT, "bf16") + gib(VAE, "fp32"), decoding: gib(DIT, "bf16") + gib(VAE, "fp32"), }); }); -test("streaming offload keeps the weights outside the blocks plus two blocks", () => { - const result = estimate({ config: config({ offload: { dit_layerwise: true, text_encoder: true, vae: true } }) }); - const ditOutsideBlocks = gib(DIT - DIT_BLOCKS, "bf16"); +test("disabling every offload keeps complete components resident and counts load-time startup", () => { + const result = estimate({ config: config({ offload: NO_OFFLOAD }) }); + const complete = gib(DIT, "bf16") + gib(TEXT, "fp32") + gib(VAE, "fp32"); + assertWeights(result, { startup: complete, encoding: complete, denoising: complete, decoding: complete }); + assert.equal(result.peakPhase, "decoding"); +}); + +test("explicit precisions change storage and activation bytes", () => { + const result = estimate({ + config: config({ + pipeline: { + experimental: { + dit_precision: "fp16", + text_encoder_precisions: ["bf16"], + vae_precision: "bf16", + vae_decode_precision: "fp32", + }, + }, + }), + }); assertWeights(result, { - encoding: streamed(TEXT, TEXT_BLOCKS, 24, "fp32") + ditOutsideBlocks, - denoising: streamed(DIT, DIT_BLOCKS, 30, "bf16"), - decoding: ditOutsideBlocks + gib(VAE, "fp32"), + encoding: gib(TEXT, "bf16") + gib(DIT - DIT_BLOCKS, "fp16") + gib(VAE, "bf16"), + denoising: streamed(DIT, DIT_BLOCKS, 30, "fp16") + gib(VAE, "bf16"), + decoding: gib(DIT - DIT_BLOCKS, "fp16") + gib(VAE, "bf16"), }); -}); - -test("unset offload switches default on, so layerwise DiT offload replaces whole-model DiT offload", () => { - const result = estimate({ config: config({ offload: { dit: false, vae: false } }) }); - close(result.phases.denoising.weightsGiB, streamed(DIT, DIT_BLOCKS, 30, "bf16") + gib(VAE, "fp32")); -}); - -test("whole-model DiT offload without layerwise streaming keeps the transformer resident", () => { - const result = estimate({ config: config({ offload: { ...NO_OFFLOAD, dit: true, text_encoder: true, vae: true } }) }); - close(result.phases.decoding.weightsGiB, gib(DIT, "bf16") + gib(VAE, "fp32")); -}); - -test("lazy loading changes nothing for Wan because its pipeline defers no components", () => { - const lazy = estimate({ config: config({ offload: { ...NO_OFFLOAD, lazy_module_load: true } }) }); - assert.deepEqual(lazy.phases, estimate().phases); -}); - -test("Wan working memory scales with tokens, hidden size, FFN size and decoded pixels", () => { - const result = estimate(); - const tokens = 21 * 30 * 52; - const hidden = tokens * 1536 * 2; - const ffn = tokens * 8960 * 2; const pixels = 480 * 832; - assert.equal((hidden / GIB).toFixed(3), "0.094"); - close(result.phases.encoding.workingGiB, WAN_RULE.encodingWorkspaceGiB); - close( - result.phases.denoising.workingGiB, - Math.max(WAN_RULE.attentionHiddenTensors * hidden, WAN_RULE.ffnHiddenTensors * hidden + WAN_RULE.ffnTensors * ffn) / - GIB, - ); close( result.phases.decoding.workingGiB, - (WAN_RULE.decodeFrameFeatures * 96 * pixels * 2 + 81 * 3 * pixels * (2 * 2 + 4)) / GIB, + (48 * 96 * pixels * BYTES_PER_WEIGHT.fp32 + 81 * 3 * pixels * (2 * BYTES_PER_WEIGHT.fp32 + 4)) / GIB, ); - const longer = estimate({ config: config({ sampling: { num_frames: 161, height: 480, width: 832 } }) }); - assert.ok(longer.phases.denoising.workingGiB > result.phases.denoising.workingGiB); }); -test("the peak phase plus headroom decides fit against device memory", () => { - const result = estimate({ config: config({ offload: { dit_layerwise: true, text_encoder: true, vae: true } }) }); +test("the VAE decode multiplier comes from component metadata", () => { + const model = wanModel((item) => (item.components.vae.decode_feature_multiplier = 24)); + const result = estimate({ model }); + const pixels = 480 * 832; + const output = 81 * 3 * pixels * (2 * BYTES_PER_WEIGHT.bf16 + 4); + close(result.phases.decoding.workingGiB, (24 * 96 * pixels * BYTES_PER_WEIGHT.bf16 + output) / GIB); +}); + +test("VAE tiling is disclosed but never treated as a measured discount", () => { + const result = estimate({ config: config({ pipeline: { vae_tiling: true } }) }); + assert.ok(result.notes.some((note) => /tiling/.test(note))); assert.equal(result.status, "fits"); - assert.equal(result.basis, "analytical"); - assert.equal(result.peakPhase, "encoding"); - close(result.peakGiB, result.phases.encoding.totalGiB); - close(result.requiredGiB, result.peakGiB * (1 + DEFAULT_HEADROOM.fraction) + DEFAULT_HEADROOM.reserveGiB); - const offload = { dit_layerwise: true, text_encoder: true, vae: true }; - assert.equal(estimate({ config: config({ offload }), hardware: gpu(result.requiredGiB) }).status, "fits"); - assert.equal(estimate({ config: config({ offload }), hardware: gpu(result.requiredGiB - 0.01) }).status, "exceeds"); - close( - estimate({ config: config({ offload }), headroom: { fraction: 0, reserveGiB: 0 } }).requiredGiB, - result.peakGiB, - ); }); -test("device memory honors decimal and binary units", () => { - close(estimate({ hardware: gpu(180, { unit: "GB" }) }).capacityGiB, 180e9 / GIB); - close(estimate({ hardware: gpu(80) }).capacityGiB, 80); -}); - -test("VAE tiling adds a note because the decode estimate assumes untiled decoding", () => { - assert.deepEqual(estimate().notes, []); - assert.equal(estimate({ config: config({ pipeline: { vae_tiling: true } }) }).notes.length, 1); -}); - -test("requests outside the modeled coverage are unknown with a reason", () => { - const withModel = (change) => { - const model = wanModel(); - change(model); - return { model }; - }; - const cases = { - "multiple GPUs": { config: config({ engine: { num_gpus: 2 } }) }, - "FSDP inference": { config: config({ engine: { use_fsdp_inference: true } }) }, - "unified memory": { hardware: { ...gpu(128), type: "unified_memory" } }, - "unknown memory unit": { hardware: gpu(80, { unit: "TB" }) }, - "no supported weight types": { hardware: { ...gpu(80), supported_dtypes: undefined } }, - "image to video": { workload: "i2v" }, - "missing request size": { config: config({ sampling: {} }) }, - "several videos per prompt": { - config: config({ sampling: { num_frames: 81, height: 480, width: 832, num_videos_per_prompt: 2 } }), - }, - "unknown working-memory rule": withModel((model) => (model.memory_rules = "ltx2")), - "missing geometry": withModel((model) => delete model.geometry.ffn_size), - "unmodeled component": withModel((model) => - model.components.push({ role: "image_encoder", parameter_count: 1, dtype: "fp16" }), - ), - "missing VAE": withModel((model) => model.components.pop()), - "missing parameter count": withModel((model) => delete model.components[2].parameter_count), - "blocks without a block parameter count": withModel((model) => delete model.components[1].block_parameter_count), - "unknown precision": withModel((model) => (model.components[1].dtype = "int4")), - "text-encoder quantization": { config: config({ engine: { quantization: { text_encoder_quant: "fp8" } } }) }, - }; - for (const [name, options] of Object.entries(cases)) { +test("multi-GPU, FSDP and lazy loading are unknown rather than pooled", () => { + const cases = [ + ["multiple GPUs", { config: config({ engine: { num_gpus: 2 } }) }], + ["tensor parallel", { config: config({ engine: { num_gpus: 2, parallelism: { tp_size: 2 } } }) }], + ["FSDP", { config: config({ engine: { use_fsdp_inference: true } }) }], + ["lazy loading", { config: config({ engine: { offload: { lazy_module_load: true } } }) }], + ]; + for (const [name, options] of cases) { const result = estimate(options); assert.equal(result.status, "unknown", name); assert.ok(result.reason.length > 0, name); } }); -test("NVFP4 weights need device support and a quantized parameter count", () => { - const quantized = 1_300_000_000; - const nvfp4 = config({ - offload: { dit_layerwise: true, text_encoder: true, vae: true }, - engine: { quantization: { transformer_quant: "NVFP4" } }, +test("VSA, compilation and quantization stop the analytical rule", () => { + const cases = [ + ["VSA", { env: { FASTVIDEO_ATTENTION_BACKEND: "VIDEO_SPARSE_ATTN" } }], + ["VSA experiment", { config: config({ pipeline: { experimental: { VSA_sparsity: 0.8 } } }) }], + ["compile", { config: config({ engine: { compile: { enabled: true } } }) }], + ["transformer quantization", { config: config({ engine: { quantization: { transformer_quant: "NVFP4" } } }) }], + [ + "retained weights", + { config: config({ engine: { quantization: { transformer_retain_original_weights: true } } }) }, + ], + ["text quantization", { config: config({ engine: { quantization: { text_encoder_quant: "fp8" } } }) }], + ["unknown backend", { env: { FASTVIDEO_ATTENTION_BACKEND: "BSA_ATTN" } }], + ]; + for (const [name, options] of cases) { + const result = estimate(options); + assert.equal(result.status, "unknown", name); + assert.ok(result.reason.length > 0, name); + } +}); + +test("unsupported weight formats are distinct from unknown coverage", () => { + const result = estimate({ hardware: hardware({ dtypes: ["fp16", "bf16"] }) }); + assert.equal(result.status, "unsupported"); + assert.match(result.reason, /fp32/); +}); + +test("missing geometry, counts, request size and modeled components stay unknown", () => { + const cases = [ + ["unknown rule", wanModel((model) => (model.memory_rules = "ltx2"))], + ["missing geometry", wanModel((model) => delete model.components.transformer.geometry.ffn_size)], + ["missing count", wanModel((model) => delete model.components.vae.parameter_count)], + ["missing VAE", wanModel((model) => delete model.components.vae)], + ["unmodeled role", wanModel((model) => (model.components.audio_encoder = { kind: "encoder", parameter_count: 1 }))], + ["wrong kind", wanModel((model) => (model.components.vae.kind = "encoder"))], + ]; + for (const [name, model] of cases) { + assert.equal(estimate({ model }).status, "unknown", name); + } + assert.equal(estimate({ config: config({ sampling: {} }) }).status, "unknown"); + assert.equal( + estimate({ + config: config({ sampling: { num_frames: 81, height: 480, width: 832, num_videos_per_prompt: 2 } }), + }).status, + "unknown", + ); + assert.equal( + estimate({ config: config({ sampling: { num_frames: 81, height: 480, width: 832, batch_cfg: true } }) }).status, + "unknown", + ); + assert.equal(estimate({ workload: "i2v" }).status, "unknown"); +}); + +test("the FastWan catalog baseline is unknown because VSA and compilation are not modeled", () => { + const model = buildCapacityCatalog().models.find((item) => item.id === "fastwan21"); + const result = assessCapacity({ model, example: model.examples[0], hardware: hardware({ memory_gb: 32 }) }); + assert.equal(result.status, "unknown"); + assert.match(result.reason, /VSA/); + assert.equal(result.compatibility, "unverified"); +}); + +function referenceRun(base, { peak = 30, scope = "device" } = {}) { + return { + id: "run-1", + date: "2026-10-18", + hardware: "test-80gb", + gpu_count: 1, + outcome: "completed", + execution: "warm_request", + evidence_url: "https://example.test/run-1", + context: { + runtime: "fastvideo-cuda-rest", + deployment: "wan21/cuda-rest", + revisions: { fastvideo: "a".repeat(40), model: "b".repeat(40) }, + config: structuredClone(base), + request: { sampling: structuredClone(base.default_request.sampling) }, + env: { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }, + }, + memory: { + metric: scope === "device" ? "device_used" : "torch_peak_reserved", + scope, + unit: "GiB", + per_gpu: [{ rank: 0, request_peak: peak, startup_peak: peak - 2 }], + }, + }; +} + +const REVISIONS = { fastvideo: "a".repeat(40), model: "b".repeat(40) }; + +test("measured evidence on the full card does not apply to a selected partition", () => { + const partitioned = hardware({ + id: "h100", + memory_gb: 80, + partitions: [{ id: "1g.10gb", label: "1g.10gb", memory_gb: 10 }], }); - const blackwell = gpu(180, { unit: "GB", dtypes: ["fp32", "fp16", "bf16", "fp8", "nvfp4"] }); - - const unsupported = estimate({ config: nvfp4 }); - assert.equal(unsupported.status, "unsupported"); - assert.match(unsupported.reason, /nvfp4/); - assert.equal(estimate({ config: nvfp4, hardware: blackwell }).status, "unknown"); - + const base = config({ engine: { compile: { enabled: false } } }); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; const model = wanModel(); - model.components[1].quantized_parameter_count = quantized; - const result = estimate({ config: nvfp4, hardware: blackwell, model }); - const packed = (quantized * 0.5625) / GIB; - const outsideBlocks = gib(DIT - DIT_BLOCKS, "bf16"); - const denseBlocks = gib(DIT_BLOCKS - quantized, "bf16"); - close(result.phases.denoising.weightsGiB, packed + outsideBlocks + (denseBlocks * 2) / 30); - close(result.phases.decoding.weightsGiB, packed + outsideBlocks + gib(VAE, "fp32")); + const ex = example(base, env); + const run = referenceRun(base); + run.hardware = "h100"; + model.reference_runs = [run]; + const fullCard = assessCapacity({ + model, + example: ex, + hardware: partitioned, + env: ex.env, + revisions: REVISIONS, + partitionId: "1g.10gb", + }); + assert.equal(fullCard.evidence.applicable.length, 0); + assert.equal(fullCard.evidence.rejected[0].differences[0].path, "hardware_partition"); + assert.notEqual(fullCard.basis, "measured"); + run.hardware_partition = "1g.10gb"; + const partitionedRun = assessCapacity({ + model, + example: ex, + hardware: partitioned, + env: ex.env, + revisions: REVISIONS, + partitionId: "1g.10gb", + }); + assert.equal(partitionedRun.evidence.applicable.length, 1); + assert.equal(partitionedRun.basis, "measured"); + assert.equal(partitionedRun.status, "exceeds"); }); -test("the FastWan cookbook baseline produces a single-GPU estimate without changing its configuration", () => { - const catalog = buildCatalogs().find((item) => item.id === "fastwan21/cuda-rest"); - const { config: resolved } = resolveConfig(catalog); - const before = JSON.stringify(resolved); - const result = estimateMemory({ config: resolved, workload: catalog.workload, model: wanModel(), hardware: gpu(24) }); - assert.equal(JSON.stringify(resolved), before); +test("matchEvidence uses the example environment when the caller omits env", () => { + const base = config({ engine: { compile: { enabled: false } } }); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const model = wanModel((item) => (item.reference_runs = [referenceRun(base)])); + const match = matchEvidence({ + model, + example: example(base, env), + config: base, + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(match.applicable.length, 1); +}); + +test("a request-only measurement cannot establish an overall device fit", () => { + const base = config({ engine: { compile: { enabled: false } } }); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const run = referenceRun(base, { peak: 20 }); + delete run.memory.per_gpu[0].startup_peak; + const model = wanModel((item) => (item.reference_runs = [run])); + const result = assessCapacity({ + model, + example: example(base, env), + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(result.basis, "measured"); + assert.equal(result.status, "unknown"); + assert.match(result.reason, /Startup peak/i); +}); + +test("a measured device fit does not override a host-memory failure", () => { + const base = config({ engine: { compile: { enabled: false } } }); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const model = wanModel((item) => (item.reference_runs = [referenceRun(base, { peak: 20 })])); + const result = assessCapacity({ + model, + example: example(base, env), + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + hostMemoryGb: 8, + }); + assert.equal(result.basis, "measured"); + assert.equal(result.deviceStatus, "fits"); + assert.equal(result.status, "exceeds"); + assert.equal(result.host.status, "exceeds"); +}); + +test("measured fits use every applicable run rather than catalog order", () => { + const base = config({ engine: { compile: { enabled: false } } }); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const first = referenceRun(base, { peak: 20 }); + const second = referenceRun(base, { peak: 30 }); + second.id = "run-2"; + const model = wanModel((item) => (item.reference_runs = [first, second])); + const result = assessCapacity({ + model, + example: example(base, env), + hardware: hardware({ id: "test-80gb", memory_gb: 25 }), + revisions: REVISIONS, + }); + assert.equal(result.basis, "measured"); + assert.equal(result.status, "exceeds"); + close(result.peakGiB, 30); + assert.equal(result.run, "run-2"); + assert.deepEqual(result.runs, ["run-1", "run-2"]); +}); + +test("unified memory uses an explicit application budget and never nominal capacity", () => { + const spark = { + id: "spark-128", + name: "Fixture unified system", + memory: { type: "unified", nominal: { value: 128, unit: "GB" } }, + backend: "cuda", + supported_dtypes: ["fp32", "fp16", "bf16", "fp8"], + }; + const withoutBudget = estimate({ hardware: spark }); + assert.equal(withoutBudget.status, "fits"); + assert.equal(withoutBudget.memory.budgetSource, "assumed_fraction"); + assert.ok(withoutBudget.capacityGiB < 128); + assert.match(withoutBudget.notes.join(" "), /75% of unified memory/); + const withBudget = estimate({ hardware: spark, memoryBudgetGiB: 64 }); + assert.equal(withBudget.status, "fits"); + assert.equal(withBudget.capacityGiB, 64); + assert.equal(withBudget.memory.budgetGiB, 64); + assert.equal(withBudget.memory.type, "unified"); + assert.equal(withBudget.hardware.capacity_gib, 64); + assert.equal(withBudget.host.status, "shared"); + assert.match(withBudget.notes.join(" "), /shared with the CPU and OS/); + assert.equal(estimate({ hardware: spark, memoryBudgetGiB: 129 }).status, "unknown"); +}); + +test("a selected partition never inherits the full device capacity", () => { + const partitioned = hardware({ + id: "h100", + memory_gb: 80, + partitions: [{ id: "1g.10gb", label: "1g.10gb", memory_gb: 10 }], + }); + const partition = estimate({ hardware: partitioned, partitionId: "1g.10gb" }); + assert.ok(partition.capacityGiB < 10); + assert.equal(partition.status, "exceeds"); + assert.equal(partition.memory.partition, "1g.10gb"); + assert.equal(partition.hardware.capacity_gib, partition.capacityGiB); + assert.match(partition.notes.join(" "), /full 74\.5 GiB is not available/); + assert.equal(estimate({ hardware: partitioned, partitionId: "missing" }).status, "unknown"); +}); + +test("a selected partition is capped by the smaller application budget", () => { + const partitioned = hardware({ + id: "h100", + memory_gb: 80, + partitions: [{ id: "3g.40gb", label: "3g.40gb", memory: { value: 40, unit: "GB" } }], + }); + const result = estimate({ hardware: partitioned, partitionId: "3g.40gb", memoryBudgetGiB: 16 }); + assert.equal(result.capacityGiB, 16); + assert.equal(result.memory.budgetGiB, 16); + assert.match(result.notes.join(" "), /smaller than/i); + const allocation = estimate({ hardware: hardware({ memory_gb: 80 }), allocationMemoryGb: 40, memoryBudgetGiB: 16 }); + assert.equal(allocation.capacityGiB, 16); + assert.equal(allocation.memory.partition, null); +}); + +test("custom hardware normalizes authored partition, nominal and host quantities", () => { + const custom = { + id: "custom", + name: "Custom partitioned device", + custom: true, + memory: { type: "dedicated", nominal: { value: 80, unit: "GB" } }, + partitions: [{ id: "slice", label: "slice", memory: { value: 20, unit: "GB" } }], + host_memory: { value: 64, unit: "GB" }, + backend: "cuda", + supported_dtypes: ["fp32", "fp16", "bf16"], + }; + const result = estimate({ hardware: custom, partitionId: "slice" }); + close(result.capacityGiB, (20 * 1e9) / GIB); + close(result.host.capacityGiB, (64 * 1e9) / GIB); + assert.notEqual(result.status, "unknown"); + assert.equal( + estimate({ + hardware: { ...custom, partitions: [{ id: "bad", label: "bad" }] }, + partitionId: "bad", + }).status, + "unknown", + ); +}); + +test("custom hardware uses an explicit memory type and capacity", () => { + const custom = { + id: "custom", + name: "Custom accelerator", + custom: true, + memory: { type: "dedicated", nominal: { value: 48, unit: "GB" } }, + backend: "cuda", + supported_dtypes: ["fp32", "fp16", "bf16", "fp8"], + }; + const result = estimate({ hardware: custom }); assert.equal(result.status, "fits"); - close(result.phases.denoising.weightsGiB, streamed(DIT, DIT_BLOCKS, 30, "bf16") + gib(VAE, "fp32")); + assert.equal(result.hardware.custom, true); + assert.match(result.notes.join(" "), /not catalog-verified/); + assert.equal(estimate({ hardware: { ...custom, backend: "rocm" } }).status, "unknown"); + assert.equal(estimate({ hardware: { ...custom, memory: undefined, memory_type: undefined } }).status, "unknown"); + const unspecifiedFormats = { ...custom, supported_dtypes: undefined }; + const defaultFormats = estimate({ hardware: unspecifiedFormats }); + assert.equal(defaultFormats.status, "fits"); + assert.match(defaultFormats.notes.join(" "), /assumes fp32, fp16, or bf16/); + assert.equal( + estimate({ hardware: { ...unspecifiedFormats, supported_dtypes: ["fp32", "fp16", "bf16", "fp8"] } }).status, + "fits", + ); +}); + +test("host memory for offload is separate from device memory", () => { + const constrained = estimate({ hardware: hardware({ memory_gb: 80, host_memory_gb: 8 }) }); + assert.equal(constrained.status, "exceeds"); + assert.equal(constrained.host.status, "exceeds"); + close(constrained.host.capacityGiB, (8 * 1e9) / GIB); + assert.ok(constrained.host.requiredGiB > constrained.host.capacityGiB); + const sufficient = estimate({ hardware: hardware({ memory_gb: 80, host_memory_gb: 64 }) }); + assert.equal(sufficient.status, "fits"); + assert.equal(sufficient.host.status, "fits"); + const override = estimate({ hardware: hardware({ memory_gb: 80, host_memory_gb: 8 }), hostMemoryGb: 24 }); + const authored = estimate({ hardware: hardware({ memory_gb: 80, host_memory_gb: 24 }) }); + close(override.host.capacityGiB, (24 * 1e9) / GIB); + close(override.host.capacityGiB, authored.host.capacityGiB); + assert.equal(override.host.status, "exceeds"); + const unknown = estimate({ hardware: hardware({ memory_gb: 80 }) }); + assert.equal(unknown.status, "fits"); + assert.equal(unknown.host.status, "unknown"); + assert.match(unknown.notes.join(" "), /Host memory for offload is not specified/); + const noOffload = estimate({ + hardware: hardware({ memory_gb: 80, host_memory_gb: 8 }), + config: config({ offload: NO_OFFLOAD }), + }); + assert.equal(noOffload.host.status, "not_required"); +}); + +test("evidence applies only when memory-relevant settings and revisions match", () => { + const base = config({ engine: { compile: { enabled: false } } }); + const model = wanModel((item) => (item.reference_runs = [referenceRun(base)])); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const match = matchEvidence({ + model, + example: example(base, env), + config: base, + env, + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(match.applicable.length, 1); + assert.equal(match.unverified.length, 0); + assert.equal(match.rejected.length, 0); + + const harmless = structuredClone(base); + harmless.server.port = 9000; + harmless.default_request.sampling.seed = 7; + harmless.default_request.sampling.fps = 24; + const still = matchEvidence({ + model, + example: example(harmless, env), + config: harmless, + env, + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(still.applicable.length, 1); + + const changed = structuredClone(base); + changed.default_request.sampling.height = 720; + const rejected = matchEvidence({ + model, + example: example(changed, env), + config: changed, + env, + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(rejected.applicable.length, 0); + assert.equal(rejected.rejected.length, 1); + assert.ok(rejected.rejected[0].differences.some((item) => item.path === "default_request.sampling.height")); +}); + +test("evidence rejects another hardware layout or backend and reports unverified revisions", () => { + const base = config({ engine: { compile: { enabled: false } } }); + const model = wanModel((item) => (item.reference_runs = [referenceRun(base)])); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const otherHardware = matchEvidence({ + model, + example: example(base, env), + config: base, + env, + hardware: hardware({ id: "other", memory_gb: 80 }), + revisions: REVISIONS, + }); + assert.equal(otherHardware.rejected.length, 1); + + const otherEnv = matchEvidence({ + model, + example: example(base, { FASTVIDEO_ATTENTION_BACKEND: "TORCH_SDPA" }), + config: base, + env: { FASTVIDEO_ATTENTION_BACKEND: "TORCH_SDPA" }, + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(otherEnv.rejected.length, 1); + + const unverified = matchEvidence({ + model, + example: example(base, env), + config: base, + env, + hardware: hardware({ id: "test-80gb" }), + }); + assert.equal(unverified.applicable.length, 0); + assert.equal(unverified.unverified.length, 1); +}); + +test("a device-wide measurement can cover execution the analytical rule does not", () => { + const base = config({ engine: { compile: { enabled: true } } }); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const model = wanModel(); + const fitted = assessCapacity({ + model, + example: example(base, env), + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(fitted.status, "unknown"); + + model.reference_runs = [referenceRun(base, { peak: 20 })]; + const measured = assessCapacity({ + model, + example: example(base, env), + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(measured.status, "fits"); + assert.equal(measured.basis, "measured"); + assert.equal(measured.compatibility, "completed"); + close(measured.peakGiB, 20); + + const allocatorOnly = structuredClone(model); + allocatorOnly.reference_runs[0].memory = referenceRun(base, { peak: 20, scope: "allocator" }).memory; + const allocatorResult = assessCapacity({ + model: allocatorOnly, + example: example(base, env), + hardware: hardware({ id: "test-80gb" }), + revisions: REVISIONS, + }); + assert.equal(allocatorResult.status, "unknown"); + assert.equal(allocatorResult.basis, undefined); +}); + +test("recommendations prefer measured fits, keep unknown explicit and never rank speed or quality", () => { + const fittedConfig = config({ engine: { compile: { enabled: false } } }); + const env = { FASTVIDEO_ATTENTION_BACKEND: "FLASH_ATTN" }; + const fitted = wanModel((model) => { + model.id = "fitted"; + model.examples = [example(fittedConfig, env)]; + model.reference_runs = [referenceRun(fittedConfig, { peak: 20 })]; + }); + const unknown = wanModel((model) => { + model.id = "unknown"; + model.examples = [example(config({ engine: { compile: { enabled: true } } }), {})]; + }); + const recommendations = recommendCapacity({ + models: [unknown, fitted], + hardware: hardware({ id: "test-80gb" }), + task: "t2v", + revisions: REVISIONS, + }); + assert.equal(recommendations[0].model.id, "fitted"); + assert.equal(recommendations[0].assessment.basis, "measured"); + assert.equal(recommendations[1].model.id, "unknown"); + assert.match(recommendations[1].reasons[0], /unknown/i); + assert.doesNotMatch(JSON.stringify(recommendations), /speed|quality|benchmark/i); +}); + +test("a lower-memory alternative reports concrete native edits and their new status", () => { + const base = config({ offload: { dit_layerwise: true, text_encoder: true, vae: false } }); + const model = wanModel((item) => { + item.examples = [example(base)]; + }); + const [candidate] = recommendCapacity({ + models: [model], + hardware: hardware({ id: "test-24gb", memory_gb: 24 }), + task: "t2v", + }); + assert.equal(candidate.assessment.status, "exceeds"); + assert.ok(candidate.lower_memory.length > 0); + for (const alternative of candidate.lower_memory) { + assert.ok( + alternative.changes.every( + (change) => change.path.startsWith("default_request.sampling.") || change.path === "generator.engine.offload", + ), + ); + assert.equal(typeof alternative.required_gib, "number"); + } + + const request = { sampling: { num_frames: 121, height: 720, width: 1280 } }; + const [requestCandidate] = recommendCapacity({ + models: [model], + hardware: hardware({ id: "test-24gb", memory_gb: 24 }), + task: "t2v", + request, + }); + assert.equal(requestCandidate.assessment.status, "exceeds"); + assert.ok(requestCandidate.lower_memory.length > 0); + for (const alternative of requestCandidate.lower_memory) { + assert.ok( + alternative.changes.every( + (change) => change.path.startsWith("request.sampling.") || change.path === "generator.engine.offload", + ), + ); + assert.equal(typeof alternative.required_gib, "number"); + } }); diff --git a/examples/serving/README.md b/examples/serving/README.md index fcd339e45..b95910345 100644 --- a/examples/serving/README.md +++ b/examples/serving/README.md @@ -51,12 +51,13 @@ CUDA FastH3 8-Step V2 serving uses `examples/serving/openai_fasth3_8step.yaml`. ## Wan configs -Four Wan CUDA configs mirror the working generate config or example for each +Five Wan CUDA configs mirror the working generate config or example for each checkpoint: | Config | Model | GPUs | Served alias | | --- | --- | --- | --- | | `openai_fastwan21_1_3b.yaml` | FastWan2.1 T2V 1.3B (DMD, VSA) | 1 | `fastwan21-1.3b` | +| `openai_wan21_t2v_14b.yaml` | Wan2.1 T2V 14B 720P | 1 | `wan21-t2v-14b` | | `openai_wan21_i2v_14b.yaml` | Wan2.1 I2V 14B 480P | 2 | `wan21-i2v-14b` | | `openai_wan22_t2v_a14b.yaml` | Wan2.2 T2V A14B | 2 | `wan22-t2v-a14b` | | `openai_wan22_ti2v_5b.yaml` | Wan2.2 TI2V 5B | 1 | `wan22-ti2v-5b` | @@ -75,7 +76,81 @@ source image through the OpenAI-compatible reference fields, for example The I2V 14B server requires one on every request; the TI2V 5B server treats its absence as text-to-video. -Or submit, poll, and download with the OpenAI Python client: +## Other families + +These configs are source-backed declarations; none is a hardware-fit claim. +`docs/cookbook/serving-coverage.md` records the full baseline list and the +uncovered registry families. + +| Config | Model | Task | GPUs | +| --- | --- | --- | --- | +| `openai_wan22_i2v_a14b.yaml` | Wan2.2 I2V A14B | image-to-video | 1 | +| `openai_hunyuan15_t2v_480p.yaml` | HunyuanVideo 1.5 T2V 480p | text-to-video | 1 | +| `openai_kandinsky5_t2v.yaml` | Kandinsky-5.0 T2V Lite | text-to-video | 1 | +| `openai_hunyuan15_i2v_480p.yaml` | HunyuanVideo 1.5 I2V 480p | image-to-video | 1 | +| `openai_kandinsky6_ti2va.yaml` | Kandinsky 6 Pro TI2VA | text/image-to-video with audio | 1 | +| `openai_kandinsky6_ti2va_distilled.yaml` | Kandinsky 6 Pro-distill TI2VA | text/image-to-video with audio | 1 | +| `openai_ltx2_distilled.yaml` | LTX2 Distilled | text-to-video with audio | 4 | +| `openai_flux1_dev.yaml` | FLUX.1-dev | text-to-image | 1 | +| `openai_flux2_dev.yaml` | FLUX.2-dev | text-to-image | 1 | +| `openai_flux2_klein.yaml` | FLUX.2-klein-4B | text-to-image | 1 | +| `openai_sd35.yaml` | SD3.5 Medium | text-to-image | 1 | +| `openai_stable_audio.yaml` | Stable Audio Open 1.0 | text-to-audio | 1 | +| `openai_mmaudio.yaml` | MMAudio large-44k-v2 | video-to-audio | 1 | + +The HunyuanVideo 1.5 I2V 480p checkpoint's registered preset renders at +720x1280; "480p" is the checkpoint identity, not a claim that every output is +480p. + +Image servers use `/v1/images/generations` and `/v1/images/edits`. Send the +model's advertised alias as `model` and set `FASTVIDEO_MODEL` for the sample +clients. SD3.5 Medium is gated; accept its license and authenticate before +starting the server. + +## Audio jobs + +Audio uses the same asynchronous job routes as video so clients keep one +polling and deletion contract. The job's `media_type` is `audio/wav`, and +`/v1/videos/{id}/content` returns WAV bytes. Set `seconds` or +`audio_start_in_s`/`audio_end_in_s`; do not send `size`, `width`, `height`, or +`video_params`. + +MMAudio is video-to-audio and requires `video_reference` (a URL, or a local +path the server allows). Stable Audio is text-to-audio. + +Media references from HTTP callers follow three server settings: + +```yaml +server: + media_reference_max_bytes: 2147483648 # per downloaded reference (default 2 GiB) + media_roots: [/data/media] # local paths callers may use (default: none) + allow_private_media_urls: false # loopback/private/link-local URLs (default: off) +``` + +- Local paths are rejected unless they resolve inside a `media_roots` entry, + including through symlinks. +- URLs must use http(s). Unless `allow_private_media_urls` is set, every + redirect hop must resolve to a public address. Turn it on only for trusted + internal media services. +- Downloads count streamed bytes against the limit. The server removes its copy + when the job finishes, fails, or is cancelled; caller files are never deleted. + +Stable Audio needs the optional inference +dependencies listed in `examples/inference/basic/basic_stable_audio.py`. + +```bash +python -m pip install httpx +FASTVIDEO_MODEL=stable-audio FASTVIDEO_SECONDS=6 \ + python examples/serving/clients/audio.py +FASTVIDEO_MODEL=mmaudio-large-44k-v2 \ +FASTVIDEO_VIDEO_REFERENCE=/path/to/clip.mp4 \ + python examples/serving/clients/audio.py +``` + +Node and cURL clients are in `examples/serving/clients/audio.mjs` and +`audio.sh`. + +Or submit, poll, and download video with the OpenAI Python client: ```bash python -m pip install openai==3.6.0 @@ -84,9 +159,10 @@ python examples/serving/clients/video.py The `clients/` directory also contains cURL and OpenAI JavaScript examples. They default to model `fasth3`; set `FASTVIDEO_MODEL` to the advertised alias -when using the LoRA or Wan configs. Set `FASTVIDEO_BASE_URL` for another endpoint. +when using a LoRA or another family config. Set `FASTVIDEO_BASE_URL` for another endpoint. These clients call your FastVideo server, not OpenAI's cloud. The SDK key `local` is a placeholder, not authentication. Keep the server on loopback or use an authenticated proxy for remote access. -For a blocking call, `POST /v1/videos/sync` returns the MP4 body directly. +For a blocking call, `POST /v1/videos/sync` returns the generated media body +directly. Check its `Content-Type` before saving the response. diff --git a/examples/serving/clients/audio.mjs b/examples/serving/clients/audio.mjs new file mode 100644 index 000000000..c87000008 --- /dev/null +++ b/examples/serving/clients/audio.mjs @@ -0,0 +1,48 @@ +// Run in Node.js 22+ or a trusted backend, not in a browser with a private key. +import { writeFile } from "node:fs/promises"; + +const baseUrl = process.env.FASTVIDEO_BASE_URL || "http://127.0.0.1:8000/v1"; +const headers = { + Authorization: `Bearer ${process.env.FASTVIDEO_API_KEY || "local"}`, + "Content-Type": "application/json", +}; +const body = { + model: process.env.FASTVIDEO_MODEL || "stable-audio", + prompt: process.env.FASTVIDEO_PROMPT || "Rain on a metal roof, distant thunder.", + seconds: Number(process.env.FASTVIDEO_SECONDS || 6), +}; +if (process.env.FASTVIDEO_VIDEO_REFERENCE) { + body.video_reference = { video_url: process.env.FASTVIDEO_VIDEO_REFERENCE }; +} + +let response = await fetch(`${baseUrl}/videos`, { + method: "POST", + headers, + body: JSON.stringify(body), +}); +if (!response.ok) throw new Error(`Submit failed (${response.status}): ${await response.text()}`); +let job = await response.json(); +console.log(`Submitted ${job.id}`); + +const deadline = performance.now() + 1_800_000; +while (job.status === "queued" || job.status === "in_progress") { + const remaining = deadline - performance.now(); + if (remaining <= 0) throw new Error(`Polling timed out; job ${job.id} may still be running`); + await new Promise((resolve) => setTimeout(resolve, Math.min(2000, remaining))); + response = await fetch(`${baseUrl}/videos/${job.id}`, { headers }); + if (!response.ok) throw new Error(`Poll failed (${response.status}): ${await response.text()}`); + job = await response.json(); +} + +if (job.status !== "completed") { + throw new Error(`Audio job ${job.id} failed: ${job.error?.message || job.status}`); +} +if (job.media_type !== "audio/wav") { + throw new Error(`Expected audio/wav, got ${job.media_type}`); +} + +response = await fetch(`${baseUrl}/videos/${job.id}/content`, { headers }); +if (!response.ok) throw new Error(`Download failed (${response.status}): ${await response.text()}`); +const output = `${job.id}.wav`; +await writeFile(output, Buffer.from(await response.arrayBuffer())); +console.log(`Saved ${output}`); diff --git a/examples/serving/clients/audio.py b/examples/serving/clients/audio.py new file mode 100644 index 000000000..ab0181e93 --- /dev/null +++ b/examples/serving/clients/audio.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Generate audio through a local FastVideo media-job endpoint. + +Audio uses the same asynchronous job routes as video because the queue, +polling and deletion contracts are shared. Read ``media_type`` from the job: +successful audio jobs return ``audio/wav`` and a WAV download. + +Install HTTP dependencies with: python -m pip install httpx +""" + +import os +import time +from pathlib import Path + +import httpx + +BASE_URL = os.environ.get("FASTVIDEO_BASE_URL", "http://127.0.0.1:8000/v1") +MODEL = os.environ.get("FASTVIDEO_MODEL", "stable-audio") +PROMPT = os.environ.get("FASTVIDEO_PROMPT", "Rain on a metal roof, distant thunder.") + + +def main() -> None: + body = { + "model": MODEL, + "prompt": PROMPT, + "seconds": int(os.environ.get("FASTVIDEO_SECONDS", "6")), + } + video_reference = os.environ.get("FASTVIDEO_VIDEO_REFERENCE") + if video_reference: + body["video_reference"] = {"video_url": video_reference} + + headers = {"Authorization": f"Bearer {os.environ.get('FASTVIDEO_API_KEY', 'local')}"} + with httpx.Client(base_url=BASE_URL, headers=headers, timeout=60.0) as client: + response = client.post("/videos", json=body) + response.raise_for_status() + job = response.json() + job_id = job["id"] + print(f"Submitted {job_id}") + + deadline = time.monotonic() + 1800 + while job["status"] in {"queued", "in_progress"}: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError(f"Polling timed out; job {job_id} may still be running") + time.sleep(min(2, remaining)) + job = client.get(f"/videos/{job_id}", timeout=min(60, remaining)).json() + + if job["status"] != "completed": + message = (job.get("error") or {}).get("message", job["status"]) + raise RuntimeError(f"Audio job {job_id} failed: {message}") + + media_type = job.get("media_type") + if media_type != "audio/wav": + raise RuntimeError(f"Expected audio/wav, got {media_type!r}") + + output = Path(f"{job_id}.wav") + with client.stream("GET", f"/videos/{job_id}/content", timeout=300.0) as download: + download.raise_for_status() + with output.open("wb") as stream: + for chunk in download.iter_bytes(): + stream.write(chunk) + print(f"Saved {output}") + + +if __name__ == "__main__": + main() diff --git a/examples/serving/clients/audio.sh b/examples/serving/clients/audio.sh new file mode 100755 index 000000000..b890706df --- /dev/null +++ b/examples/serving/clients/audio.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Requires curl and jq. Run after starting the FastVideo audio server. +set -euo pipefail + +base_url="${FASTVIDEO_BASE_URL:-http://127.0.0.1:8000/v1}" +api_key="${FASTVIDEO_API_KEY:-local}" +model="${FASTVIDEO_MODEL:-stable-audio}" +prompt="${FASTVIDEO_PROMPT:-Rain on a metal roof, distant thunder.}" +seconds="${FASTVIDEO_SECONDS:-6}" +payload=$(jq -n --arg model "$model" --arg prompt "$prompt" --argjson seconds "$seconds" \ + '{model: $model, prompt: $prompt, seconds: $seconds}') +if [[ -n "${FASTVIDEO_VIDEO_REFERENCE:-}" ]]; then + payload=$(jq --arg video "$FASTVIDEO_VIDEO_REFERENCE" '. + {video_reference: {video_url: $video}}' <<< "$payload") +fi + +job=$(curl --fail-with-body --silent --show-error --max-time 60 \ + "$base_url/videos" \ + -H "Authorization: Bearer $api_key" \ + -H 'Content-Type: application/json' \ + -d "$payload") +job_id=$(jq -er '.id' <<< "$job") +printf 'Submitted %s\n' "$job_id" +deadline=$((SECONDS + 1800)) + +while true; do + status=$(jq -er '.status' <<< "$job") + case "$status" in + completed) break ;; + failed) jq -r '.error.message' <<< "$job" >&2; exit 1 ;; + queued|in_progress) ;; + *) printf 'Unexpected job status: %s\n' "$status" >&2; exit 1 ;; + esac + remaining=$((deadline - SECONDS)) + if (( remaining <= 0 )); then + printf 'Polling timed out; job %s may still be running\n' "$job_id" >&2 + exit 1 + fi + if (( remaining > 2 )); then sleep 2; else sleep "$remaining"; fi + remaining=$((deadline - SECONDS)) + if (( remaining <= 0 )); then continue; fi + if (( remaining > 60 )); then remaining=60; fi + job=$(curl --fail-with-body --silent --show-error --max-time "$remaining" \ + -H "Authorization: Bearer $api_key" "$base_url/videos/$job_id") +done + +media_type=$(jq -er '.media_type' <<< "$job") +if [[ "$media_type" != "audio/wav" ]]; then + printf 'Expected audio/wav, got %s\n' "$media_type" >&2 + exit 1 +fi +curl --fail-with-body --silent --show-error --max-time 300 \ + -H "Authorization: Bearer $api_key" \ + "$base_url/videos/$job_id/content" --output "$job_id.wav" +printf 'Saved %s.wav\n' "$job_id" diff --git a/examples/serving/openai_fasth3_omniref_rtx5090.yaml b/examples/serving/openai_fasth3_omniref_rtx5090.yaml new file mode 100644 index 000000000..bc3082e3b --- /dev/null +++ b/examples/serving/openai_fasth3_omniref_rtx5090.yaml @@ -0,0 +1,78 @@ +# OpenAI-compatible FastH3 OmniRef (Ref2VA) server on one RTX 5090 (32 GB). +# +# Serving adaptation of examples/inference/basic/basic_fasth3_omniref_rtx5090.yaml; the same NVFP4 stack and +# device policy, not yet GPU-validated as a server. +# +# Point model_path at a composed OmniRef directory. Build it once with +# python examples/inference/basic/basic_fasth3_omniref_pdd.py \ +# --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --profile rtx-5090 \ +# --composed-dir ./FastH3-OmniRef-Composed --image assets/images/couple.jpg --prompt "..." +# +# Requests carry ordered references: image_reference / input_reference for images, +# video_reference for clips. Local paths must sit under server.media_roots. +# +# FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 \ +# FASTVIDEO_H3_VSA_FP4=1 FASTVIDEO_H3_VSA_FP4_PREFIX=fp8 \ +# FASTVIDEO_H3_VSA_FP4_TWO_LEVEL_P=1 FASTVIDEO_H3_VSA_FP4_K_SMOOTH=1 \ +# FASTVIDEO_H3_GATE_CHUNK_ROWS=8192 \ +# FASTVIDEO_ULYSSES_A2A=auto FASTVIDEO_MINIMAX_H3_EXACT_KERNELS=all \ +# FASTVIDEO_H3_VSA_HEADS_FIRST_TILE=1 FASTVIDEO_H3_VAE_TILE_PARALLEL=1 \ +# FASTVIDEO_H3_REF2VA_MEMO_ENTRIES=16 \ +# FASTVIDEO_MINIMAX_H3_FUSIONS=all \ +# FASTVIDEO_VSA_TRITON=1 FASTVIDEO_VSA_PTX_BLACKWELL=0 FASTVIDEO_FA4=0 \ +# FASTVIDEO_H3_ADALN_TABLE=./adaln_table.pt \ +# FASTVIDEO_H3_VAE_TILE_BATCH=4 \ +# fastvideo serve --config examples/serving/openai_fasth3_omniref_rtx5090.yaml +# +# See docs/inference/fasth3_omniref.md. +generator: + model_path: ./FastH3-OmniRef-Composed + engine: + num_gpus: 1 + use_fsdp_inference: false + quantization: + transformer_quant: NVFP4 + layer_profile: h3_dit_vsa + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + dit_layerwise: false + text_encoder: true + vae: false + pin_cpu_memory: true + lazy_module_load: false + pipeline: + workload_type: i2v + components: + override_pipeline_cls_name: MiniMaxH3Ref2VAModularPipeline + experimental: + attention_backend: VIDEO_SPARSE_ATTN_H3 + h3_vsa_fp4: + enabled: true + prefix_precision: fp8 + two_level_p: true + smooth_k: true + video_decode_backend: h3-vae + h3_sequential_load: true + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_fasth3_omniref_rtx5090 + served_model_name: fasth3-omniref + +default_request: + negative_prompt: "" + sampling: + height: 480 + width: 832 + num_frames: 124 + fps: 24 + num_inference_steps: 8 # eight fused PDD blocks; the contract sets it + guidance_scale: 1.0 + batch_cfg: false + seed: 1234 + output: + return_frames: false diff --git a/examples/serving/openai_fasth3_omniref_rtx_pro6000.yaml b/examples/serving/openai_fasth3_omniref_rtx_pro6000.yaml new file mode 100644 index 000000000..eeb1771a3 --- /dev/null +++ b/examples/serving/openai_fasth3_omniref_rtx_pro6000.yaml @@ -0,0 +1,74 @@ +# OpenAI-compatible FastH3 OmniRef (Ref2VA) server on one RTX PRO 6000 Blackwell (96 GB). +# +# Serving adaptation of examples/inference/basic/basic_fasth3_omniref_rtx_pro6000.yaml; the same NVFP4 stack and +# device policy, not yet GPU-validated as a server. +# +# Point model_path at a composed OmniRef directory. Build it once with +# python examples/inference/basic/basic_fasth3_omniref_pdd.py \ +# --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --profile rtx-pro-6000 \ +# --composed-dir ./FastH3-OmniRef-Composed --image assets/images/couple.jpg --prompt "..." +# +# Requests carry ordered references: image_reference / input_reference for images, +# video_reference for clips. Local paths must sit under server.media_roots. +# +# FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 \ +# FASTVIDEO_H3_VSA_FP4=1 FASTVIDEO_H3_VSA_FP4_PREFIX=fp8 \ +# FASTVIDEO_H3_VSA_FP4_TWO_LEVEL_P=1 FASTVIDEO_H3_VSA_FP4_K_SMOOTH=1 \ +# FASTVIDEO_H3_GATE_CHUNK_ROWS=8192 \ +# FASTVIDEO_MINIMAX_H3_FUSIONS=all \ +# FASTVIDEO_VSA_TRITON=1 FASTVIDEO_VSA_PTX_BLACKWELL=0 FASTVIDEO_FA4=0 \ +# FASTVIDEO_H3_VAE_TILE_BATCH=8 \ +# fastvideo serve --config examples/serving/openai_fasth3_omniref_rtx_pro6000.yaml +# +# See docs/inference/fasth3_omniref.md. +generator: + model_path: ./FastH3-OmniRef-Composed + engine: + num_gpus: 1 + use_fsdp_inference: false + quantization: + transformer_quant: NVFP4 + layer_profile: h3_dit_vsa + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + dit_layerwise: false + text_encoder: false + vae: false + pin_cpu_memory: true + lazy_module_load: false + pipeline: + workload_type: i2v + components: + override_pipeline_cls_name: MiniMaxH3Ref2VAModularPipeline + experimental: + attention_backend: VIDEO_SPARSE_ATTN_H3 + h3_vsa_fp4: + enabled: true + prefix_precision: fp8 + two_level_p: true + smooth_k: true + video_decode_backend: h3-vae + h3_sequential_load: false + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_fasth3_omniref_rtx_pro6000 + served_model_name: fasth3-omniref + +default_request: + negative_prompt: "" + sampling: + height: 768 + width: 1344 + num_frames: 124 + fps: 24 + num_inference_steps: 8 # eight fused PDD blocks; the contract sets it + guidance_scale: 1.0 + batch_cfg: false + seed: 1234 + output: + return_frames: false diff --git a/examples/serving/openai_fasth3_omniref_spark.yaml b/examples/serving/openai_fasth3_omniref_spark.yaml new file mode 100644 index 000000000..15c026f5a --- /dev/null +++ b/examples/serving/openai_fasth3_omniref_spark.yaml @@ -0,0 +1,79 @@ +# OpenAI-compatible FastH3 OmniRef (Ref2VA) server on one DGX Spark (GB10, 128 GB unified). +# +# Serving adaptation of examples/inference/basic/basic_fasth3_omniref_spark.yaml; the same NVFP4 stack and +# device policy, not yet GPU-validated as a server. +# +# Point model_path at a composed OmniRef directory. Build it once with +# python examples/inference/basic/basic_fasth3_omniref_pdd.py \ +# --model-path FastVideo/FastH3-OmniRef-NVFP4-QAD --profile spark \ +# --composed-dir ./FastH3-OmniRef-Composed --image assets/images/couple.jpg --prompt "..." +# +# Requests carry ordered references: image_reference / input_reference for images, +# video_reference for clips. Local paths must sit under server.media_roots. +# +# FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN_H3 \ +# FASTVIDEO_H3_VSA_FP4=1 FASTVIDEO_H3_VSA_FP4_PREFIX=fp8 \ +# FASTVIDEO_H3_VSA_FP4_TWO_LEVEL_P=1 FASTVIDEO_H3_VSA_FP4_K_SMOOTH=1 \ +# FASTVIDEO_H3_GATE_CHUNK_ROWS=8192 \ +# FASTVIDEO_ULYSSES_A2A=auto FASTVIDEO_MINIMAX_H3_EXACT_KERNELS=all \ +# FASTVIDEO_H3_VSA_HEADS_FIRST_TILE=1 FASTVIDEO_H3_VAE_TILE_PARALLEL=1 \ +# FASTVIDEO_H3_REF2VA_MEMO_ENTRIES=16 \ +# FASTVIDEO_MINIMAX_H3_FUSIONS=all \ +# FASTVIDEO_VSA_TRITON=1 FASTVIDEO_VSA_PTX_BLACKWELL=0 FASTVIDEO_FA4=0 \ +# FASTVIDEO_NVFP4_MM_BACKEND=cudnn FASTVIDEO_NVFP4_MM_ROW_SPLIT=table \ +# FASTVIDEO_H3_VAE_TILE_BATCH=12 FASTVIDEO_H3_VAE_INT8_OVERLAY=0 \ +# fastvideo serve --config examples/serving/openai_fasth3_omniref_spark.yaml +# +# See docs/inference/fasth3_omniref.md. +generator: + model_path: ./FastH3-OmniRef-Composed + engine: + num_gpus: 1 + use_fsdp_inference: false + quantization: + transformer_quant: NVFP4 + layer_profile: h3_dit_vsa + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + dit_layerwise: false + text_encoder: false + image_encoder: false + vae: false + pin_cpu_memory: false + lazy_module_load: false + pipeline: + workload_type: i2v + components: + override_pipeline_cls_name: MiniMaxH3Ref2VAModularPipeline + experimental: + attention_backend: VIDEO_SPARSE_ATTN_H3 + h3_vsa_fp4: + enabled: true + prefix_precision: fp8 + two_level_p: true + smooth_k: true + video_decode_backend: h3-vae + h3_sequential_load: false + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_fasth3_omniref_spark + served_model_name: fasth3-omniref + +default_request: + negative_prompt: "" + sampling: + height: 480 + width: 832 + num_frames: 124 + fps: 24 + num_inference_steps: 8 # eight fused PDD blocks; the contract sets it + guidance_scale: 1.0 + batch_cfg: false + seed: 1234 + output: + return_frames: false diff --git a/examples/serving/openai_flux1_dev.yaml b/examples/serving/openai_flux1_dev.yaml new file mode 100644 index 000000000..30a8f0fc3 --- /dev/null +++ b/examples/serving/openai_flux1_dev.yaml @@ -0,0 +1,26 @@ +# OpenAI-compatible image server for FLUX.1-dev. +# fastvideo serve --config examples/serving/openai_flux1_dev.yaml +# curl http://127.0.0.1:8000/v1/images/generations -H 'Content-Type: application/json' \ +# -d '{"model":"flux1-dev","prompt":"a photo of a cat","size":"1024x1024","response_format":"b64_json"}' +generator: + model_path: black-forest-labs/FLUX.1-dev + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: false + text_encoder: false + pin_cpu_memory: false + + pipeline: + workload_type: t2i + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_flux1_dev + served_model_name: flux1-dev diff --git a/examples/serving/openai_flux2_dev.yaml b/examples/serving/openai_flux2_dev.yaml new file mode 100644 index 000000000..f215b65ef --- /dev/null +++ b/examples/serving/openai_flux2_dev.yaml @@ -0,0 +1,26 @@ +# OpenAI-compatible image server for FLUX.2-dev. +# fastvideo serve --config examples/serving/openai_flux2_dev.yaml +generator: + model_path: black-forest-labs/FLUX.2-dev + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: true + text_encoder: true + pin_cpu_memory: false + + pipeline: + workload_type: t2i + components: + override_pipeline_cls_name: Flux2Pipeline + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_flux2_dev + served_model_name: flux2-dev diff --git a/examples/serving/openai_flux2_klein.yaml b/examples/serving/openai_flux2_klein.yaml new file mode 100644 index 000000000..89ed3cdb8 --- /dev/null +++ b/examples/serving/openai_flux2_klein.yaml @@ -0,0 +1,24 @@ +# OpenAI-compatible image server for FLUX.2-klein (4-step distilled). +# fastvideo serve --config examples/serving/openai_flux2_klein.yaml +generator: + model_path: black-forest-labs/FLUX.2-klein-4B + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: true + text_encoder: true + pin_cpu_memory: false + + pipeline: + workload_type: t2i + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_flux2_klein + served_model_name: flux2-klein diff --git a/examples/serving/openai_hunyuan15_i2v_480p.yaml b/examples/serving/openai_hunyuan15_i2v_480p.yaml new file mode 100644 index 000000000..cf3accea6 --- /dev/null +++ b/examples/serving/openai_hunyuan15_i2v_480p.yaml @@ -0,0 +1,36 @@ +# OpenAI-compatible server for HunyuanVideo 1.5 I2V 480p step-distilled. +# fastvideo serve --config examples/serving/openai_hunyuan15_i2v_480p.yaml +# Send image_reference (or input_reference) with the first frame. +generator: + model_path: hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v_step_distilled + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: true + vae: true + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: i2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_hunyuan15_i2v_480p + served_model_name: hunyuan15-i2v-480p + +default_request: + negative_prompt: "" + sampling: + height: 720 + width: 1280 + num_frames: 121 + fps: 24 + num_inference_steps: 12 + guidance_scale: 1.0 + output: + return_frames: false diff --git a/examples/serving/openai_hunyuan15_t2v_480p.yaml b/examples/serving/openai_hunyuan15_t2v_480p.yaml new file mode 100644 index 000000000..6fc6b9cce --- /dev/null +++ b/examples/serving/openai_hunyuan15_t2v_480p.yaml @@ -0,0 +1,35 @@ +# OpenAI-compatible server for HunyuanVideo 1.5 T2V at 480p. +# fastvideo serve --config examples/serving/openai_hunyuan15_t2v_480p.yaml +generator: + model_path: hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: true + vae: true + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: t2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_hunyuan15_t2v_480p + served_model_name: hunyuan15-t2v-480p + +default_request: + negative_prompt: "" + sampling: + height: 480 + width: 848 + num_frames: 121 + fps: 24 + num_inference_steps: 50 + guidance_scale: 6.0 + output: + return_frames: false diff --git a/examples/serving/openai_kandinsky5_t2v.yaml b/examples/serving/openai_kandinsky5_t2v.yaml new file mode 100644 index 000000000..773b9d96a --- /dev/null +++ b/examples/serving/openai_kandinsky5_t2v.yaml @@ -0,0 +1,39 @@ +# OpenAI-compatible server for Kandinsky-5.0 T2V Lite (5 seconds, 50 steps). +# Use the distilled checkpoint or lower num_inference_steps for faster runs. +# fastvideo serve --config examples/serving/openai_kandinsky5_t2v.yaml +generator: + model_path: kandinskylab/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: false + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: t2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_kandinsky5_t2v + served_model_name: kandinsky5-t2v + +default_request: + negative_prompt: >- + Static, 2D cartoon, cartoon, 2d animation, paintings, images, worst quality, + low quality, ugly, deformed, walking backwards + sampling: + seed: 1024 + num_frames: 121 + height: 512 + width: 768 + fps: 24 + num_inference_steps: 50 + guidance_scale: 5.0 + output: + return_frames: false diff --git a/examples/serving/openai_kandinsky6_ti2va.yaml b/examples/serving/openai_kandinsky6_ti2va.yaml new file mode 100644 index 000000000..3f9d9a418 --- /dev/null +++ b/examples/serving/openai_kandinsky6_ti2va.yaml @@ -0,0 +1,35 @@ +# OpenAI-compatible server for Kandinsky 6 Pro TI2VA with synchronized audio. +# fastvideo serve --config examples/serving/openai_kandinsky6_ti2va.yaml +# Omit image_reference for text-to-video; send it for image conditioning. +generator: + model_path: kandinskylab/Kandinsky-6.0-Pro-5s-Diffusers + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: false + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: t2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_kandinsky6_ti2va + served_model_name: kandinsky6-ti2va + +default_request: + sampling: + height: 512 + width: 768 + num_frames: 121 + fps: 24 + num_inference_steps: 50 + guidance_scale: 5.0 + output: + return_frames: false diff --git a/examples/serving/openai_kandinsky6_ti2va_distilled.yaml b/examples/serving/openai_kandinsky6_ti2va_distilled.yaml new file mode 100644 index 000000000..6b2a29d14 --- /dev/null +++ b/examples/serving/openai_kandinsky6_ti2va_distilled.yaml @@ -0,0 +1,35 @@ +# OpenAI-compatible server for Kandinsky 6 Pro-distill TI2VA (pi-Flow, 10 steps). +# fastvideo serve --config examples/serving/openai_kandinsky6_ti2va_distilled.yaml +# Omit image_reference for text-to-video; send it for image conditioning. +generator: + model_path: kandinskylab/Kandinsky-6.0-Pro-distill-5s-Diffusers + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: false + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: t2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_kandinsky6_ti2va_distilled + served_model_name: kandinsky6-ti2va-distilled + +default_request: + sampling: + height: 512 + width: 768 + num_frames: 121 + fps: 24 + num_inference_steps: 10 + guidance_scale: 1.0 + output: + return_frames: false diff --git a/examples/serving/openai_ltx2_distilled.yaml b/examples/serving/openai_ltx2_distilled.yaml new file mode 100644 index 000000000..cd0d9920a --- /dev/null +++ b/examples/serving/openai_ltx2_distilled.yaml @@ -0,0 +1,36 @@ +# OpenAI-compatible REST server for LTX-2 distilled text-to-video with audio. +# fastvideo serve --config examples/serving/openai_ltx2_distilled.yaml +# The streaming variant remains in ltx2_distilled_streaming.yaml. +generator: + model_path: FastVideo/LTX2-Distilled-Diffusers + engine: + num_gpus: 4 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 4 + offload: + dit: false + vae: true + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: t2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_ltx2_distilled + served_model_name: ltx2-distilled + +default_request: + sampling: + height: 1024 + width: 1536 + num_frames: 121 + fps: 24 + num_inference_steps: 8 + guidance_scale: 1.0 + seed: 10 + output: + return_frames: false diff --git a/examples/serving/openai_mmaudio.yaml b/examples/serving/openai_mmaudio.yaml new file mode 100644 index 000000000..bf931b307 --- /dev/null +++ b/examples/serving/openai_mmaudio.yaml @@ -0,0 +1,37 @@ +# OpenAI-compatible media server for MMAudio large-44k-v2 video-to-audio. +# fastvideo serve --config examples/serving/openai_mmaudio.yaml +# Send video_reference (local path or URL). Audio uses the /v1/videos job +# endpoints and returns media_type audio/wav. +generator: + model_path: FastVideo/MMAudio-large-44k-v2-Diffusers + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: false + text_encoder: false + pin_cpu_memory: false + + pipeline: + workload_type: v2a + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_mmaudio + served_model_name: mmaudio-large-44k-v2 + +default_request: + negative_prompt: music + sampling: + audio_start_in_s: 0.0 + audio_end_in_s: 8.0 + num_inference_steps: 25 + guidance_scale: 4.5 + seed: 42 + output: + return_frames: false diff --git a/examples/serving/openai_sd35.yaml b/examples/serving/openai_sd35.yaml new file mode 100644 index 000000000..c41361b5e --- /dev/null +++ b/examples/serving/openai_sd35.yaml @@ -0,0 +1,24 @@ +# OpenAI-compatible image server for Stable Diffusion 3.5 Medium. +# fastvideo serve --config examples/serving/openai_sd35.yaml +generator: + model_path: stabilityai/stable-diffusion-3.5-medium + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: false + text_encoder: false + pin_cpu_memory: false + + pipeline: + workload_type: t2i + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_sd35 + served_model_name: sd35 diff --git a/examples/serving/openai_stable_audio.yaml b/examples/serving/openai_stable_audio.yaml new file mode 100644 index 000000000..8b7387197 --- /dev/null +++ b/examples/serving/openai_stable_audio.yaml @@ -0,0 +1,38 @@ +# OpenAI-compatible media server for Stable Audio Open 1.0 text-to-audio. +# fastvideo serve --config examples/serving/openai_stable_audio.yaml +# Audio uses the /v1/videos job endpoints and returns media_type audio/wav. +# Requires the optional Stable Audio inference dependencies listed in +# examples/inference/basic/basic_stable_audio.py. +generator: + model_path: FastVideo/stable-audio-open-1.0-Diffusers + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: false + vae: false + text_encoder: false + pin_cpu_memory: false + + pipeline: + workload_type: t2a + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_stable_audio + served_model_name: stable-audio + +default_request: + negative_prompt: "" + sampling: + audio_start_in_s: 0.0 + audio_end_in_s: 10.0 + num_inference_steps: 100 + guidance_scale: 7.0 + seed: 0 + output: + return_frames: false diff --git a/examples/serving/openai_wan21_t2v_14b.yaml b/examples/serving/openai_wan21_t2v_14b.yaml new file mode 100644 index 000000000..50b84181b --- /dev/null +++ b/examples/serving/openai_wan21_t2v_14b.yaml @@ -0,0 +1,43 @@ +# OpenAI-compatible server for Wan2.1 T2V 14B at 720p. This is the base +# 50-step model, not the distilled FastWan variant. +# fastvideo serve --config examples/serving/openai_wan21_t2v_14b.yaml +generator: + model_path: Wan-AI/Wan2.1-T2V-14B-Diffusers + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: true + vae: true + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: t2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_wan21_t2v_14b + served_model_name: wan21-t2v-14b + +default_request: + negative_prompt: >- + Bright tones, overexposed, static, blurred details, subtitles, style, + works, paintings, images, static, overall gray, worst quality, low + quality, JPEG compression residue, ugly, incomplete, extra fingers, + poorly drawn hands, poorly drawn faces, deformed, disfigured, misshapen + limbs, fused fingers, still picture, messy background, three legs, many + people in the background, walking backwards + sampling: + seed: 1024 + num_frames: 81 + height: 720 + width: 1280 + fps: 16 + num_inference_steps: 50 + guidance_scale: 5.0 + output: + return_frames: false diff --git a/examples/serving/openai_wan22_i2v_a14b.yaml b/examples/serving/openai_wan22_i2v_a14b.yaml new file mode 100644 index 000000000..f657aabe8 --- /dev/null +++ b/examples/serving/openai_wan22_i2v_a14b.yaml @@ -0,0 +1,33 @@ +# OpenAI-compatible server for Wan2.2 I2V A14B (mixture-of-experts DiT). +# fastvideo serve --config examples/serving/openai_wan22_i2v_a14b.yaml +# Send image_reference (or input_reference) with the first frame. +generator: + model_path: Wan-AI/Wan2.2-I2V-A14B-Diffusers + engine: + num_gpus: 1 + use_fsdp_inference: false + parallelism: + tp_size: 1 + sp_size: 1 + offload: + dit: true + vae: false + text_encoder: true + pin_cpu_memory: true + pipeline: + workload_type: i2v + +server: + host: 0.0.0.0 + port: 8000 + output_dir: outputs/openai_wan22_i2v_a14b + served_model_name: wan22-i2v-a14b + +default_request: + sampling: + height: 480 + width: 832 + num_frames: 81 + fps: 16 + output: + return_frames: false diff --git a/fastvideo/api/compat.py b/fastvideo/api/compat.py index c97604c82..e199bc8f0 100644 --- a/fastvideo/api/compat.py +++ b/fastvideo/api/compat.py @@ -395,7 +395,14 @@ def request_to_sampling_param( if request.plan is not None: raise NotImplementedError("GenerationRequest.plan is not wired into VideoGenerator yet") - sampling_param = SamplingParam.from_pretrained(model_path) + from fastvideo.registry import get_sampling_param_cls_for_name + + try: + sampling_param_cls = get_sampling_param_cls_for_name(model_path, allow_remote=False) or SamplingParam + except ValueError: + # A local directory without model_index.json has no registered family; use the base defaults. + sampling_param_cls = SamplingParam + sampling_param = sampling_param_cls.from_pretrained(model_path) if request.state is not None: _validate_continuation_state(request.state) sampling_param.continuation_state = request.state diff --git a/fastvideo/api/schema.py b/fastvideo/api/schema.py index 7bdcf40ac..0ece1ab24 100644 --- a/fastvideo/api/schema.py +++ b/fastvideo/api/schema.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Literal +DEFAULT_MEDIA_REFERENCE_MAX_BYTES = 2 * 1024**3 + @dataclass class ServerConfig: @@ -11,6 +13,17 @@ class ServerConfig: port: int = 8000 output_dir: str = "outputs/" served_model_name: str | None = None + media_reference_max_bytes: int = DEFAULT_MEDIA_REFERENCE_MAX_BYTES + # Directories whose files HTTP callers may reference by local path. Empty rejects local paths. + media_roots: list[str] = field(default_factory=list) + # Allows media URLs that resolve to loopback, private, or link-local addresses. + allow_private_media_urls: bool = False + + def __post_init__(self) -> None: + if self.media_reference_max_bytes <= 0: + raise ValueError("server.media_reference_max_bytes must be positive") + if any(not isinstance(root, str) or not root.strip() for root in self.media_roots): + raise ValueError("server.media_roots entries must be non-empty paths") @dataclass @@ -171,6 +184,8 @@ class SamplingConfig: height_sr: int = 1072 width_sr: int = 1920 fps: int = 24 + audio_start_in_s: float | None = None + audio_end_in_s: float | None = None num_inference_steps: int = 50 num_inference_steps_sr: int = 50 guidance_scale: float = 1.0 diff --git a/fastvideo/entrypoints/cli/serve.py b/fastvideo/entrypoints/cli/serve.py index 4ce9932ab..42eae20be 100644 --- a/fastvideo/entrypoints/cli/serve.py +++ b/fastvideo/entrypoints/cli/serve.py @@ -52,6 +52,7 @@ class ServeSubcommand(CLISubcommand): from fastvideo.entrypoints.openai.api_server import ( run_server, ) + from fastvideo.entrypoints.openai.media_policy import MediaPolicy logger.info( "Server will listen on %s:%d", @@ -67,6 +68,11 @@ class ServeSubcommand(CLISubcommand): output_dir=serve_config.server.output_dir, default_request=serve_config.default_request, served_model_name=serve_config.server.served_model_name, + media_reference_max_bytes=serve_config.server.media_reference_max_bytes, + media_policy=MediaPolicy( + roots=tuple(serve_config.server.media_roots), + allow_private_urls=serve_config.server.allow_private_media_urls, + ), ) def validate(self, args: argparse.Namespace) -> None: diff --git a/fastvideo/entrypoints/openai/api_server.py b/fastvideo/entrypoints/openai/api_server.py index ecda0a6f7..baea043e2 100644 --- a/fastvideo/entrypoints/openai/api_server.py +++ b/fastvideo/entrypoints/openai/api_server.py @@ -12,7 +12,8 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastvideo.api.presets import validate_preset_selection -from fastvideo.api.schema import GenerationRequest +from fastvideo.api.schema import DEFAULT_MEDIA_REFERENCE_MAX_BYTES, GenerationRequest +from fastvideo.entrypoints.openai.media_policy import MediaPolicy from fastvideo.entrypoints.openai.state import ( DEFAULT_OUTPUT_DIR, clear_state, @@ -61,6 +62,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: output_dir: str = app.state.output_dir served_model_name: str | None = app.state.served_model_name default_request: GenerationRequest | None = getattr(app.state, "default_request", None) + media_reference_max_bytes: int = getattr(app.state, "media_reference_max_bytes", DEFAULT_MEDIA_REFERENCE_MAX_BYTES) + media_policy: MediaPolicy = getattr(app.state, "media_policy", MediaPolicy()) logger.info("Initializing %s generation runtime for %s ...", app.state.runtime, args.model_path) factory = app.state.generator_factory @@ -75,6 +78,8 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: output_dir, default_request=default_request, served_model_name=served_model_name, + media_reference_max_bytes=media_reference_max_bytes, + media_policy=media_policy, ) try: @@ -98,6 +103,8 @@ def create_app( generator_factory: Callable[[], ServingGenerator] | None = None, video_request_validator: Callable[[VideoGenerationRequest], None] | None = None, runtime: str = "cuda", + media_reference_max_bytes: int = DEFAULT_MEDIA_REFERENCE_MAX_BYTES, + media_policy: MediaPolicy | None = None, ) -> FastAPI: """Build the FastAPI application with all routers mounted""" @@ -117,6 +124,8 @@ def create_app( app.state.output_dir = output_dir app.state.default_request = default_request app.state.served_model_name = served_model_name + app.state.media_reference_max_bytes = media_reference_max_bytes + app.state.media_policy = media_policy or MediaPolicy() app.state.generator_factory = generator_factory app.state.video_request_validator = video_request_validator app.state.runtime = runtime @@ -211,6 +220,8 @@ def run_server( output_dir: str = DEFAULT_OUTPUT_DIR, default_request: GenerationRequest | None = None, served_model_name: str | None = None, + media_reference_max_bytes: int = DEFAULT_MEDIA_REFERENCE_MAX_BYTES, + media_policy: MediaPolicy | None = None, ): """Create the app and run it with uvicorn""" os.environ.setdefault("FASTVIDEO_STAGE_LOGGING", "1") @@ -222,6 +233,8 @@ def run_server( output_dir=output_dir, default_request=default_request, served_model_name=served_model_name, + media_reference_max_bytes=media_reference_max_bytes, + media_policy=media_policy, ) logger.info("Starting FastVideo server on %s:%d", host, port) diff --git a/fastvideo/entrypoints/openai/media_policy.py b/fastvideo/entrypoints/openai/media_policy.py new file mode 100644 index 000000000..18c8e5aa1 --- /dev/null +++ b/fastvideo/entrypoints/openai/media_policy.py @@ -0,0 +1,113 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Rules for media references supplied by HTTP callers. + +Local paths are readable only inside operator-listed ``server.media_roots``. +URLs must use http(s) and, unless ``server.allow_private_media_urls`` is set, +must resolve to public addresses on every redirect hop. Downloads are capped +by streamed bytes and leave no partial file on failure or cancellation. +""" + +from __future__ import annotations + +import asyncio +import contextlib +import ipaddress +import os +import socket +from dataclasses import dataclass +from urllib.parse import urljoin, urlparse + +import aiofiles +import httpx + +MAX_REDIRECTS = 5 +READ_TIMEOUT_S = 60.0 + + +class MediaPolicyError(ValueError): + """A media reference is not allowed by the server's media policy.""" + + +@dataclass(frozen=True) +class MediaPolicy: + roots: tuple[str, ...] = () + allow_private_urls: bool = False + + +def resolve_local_media(source: str, policy: MediaPolicy) -> str: + """Return the real path of a caller-supplied local file inside a configured root.""" + path = os.path.realpath(os.path.expanduser(source)) + for root in policy.roots: + real_root = os.path.realpath(os.path.expanduser(root)) + if os.path.commonpath([path, real_root]) == real_root: + if not os.path.isfile(path): + raise MediaPolicyError(f"Media reference does not exist: {source}") + return path + # Checked before existence so callers cannot probe the server's filesystem. + raise MediaPolicyError("Local media paths are not accepted from HTTP callers. Provide an http(s) URL, or list " + "the directory in server.media_roots in the serve config.") + + +def _resolve_addresses(host: str, port: int) -> list[str]: + return [str(info[4][0]) for info in socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)] + + +async def _check_url(url: str, policy: MediaPolicy) -> None: + parsed = urlparse(url) + if parsed.scheme not in ("http", "https") or not parsed.hostname: + raise MediaPolicyError("Media URLs must use http or https.") + if policy.allow_private_urls: + return + port = parsed.port or (443 if parsed.scheme == "https" else 80) + try: + addresses = await asyncio.to_thread(_resolve_addresses, parsed.hostname, port) + except OSError as error: + raise MediaPolicyError(f"Unable to resolve media host {parsed.hostname}: {error}") from error + for address in addresses: + if not ipaddress.ip_address(address.split("%", 1)[0]).is_global: + raise MediaPolicyError(f"Media URL {parsed.hostname} resolves to a private or local address. Set " + "server.allow_private_media_urls for trusted internal media hosts.") + + +def discard(path: str) -> None: + with contextlib.suppress(OSError): + os.unlink(path) + + +async def fetch_media( + url: str, + target: str, + *, + max_bytes: int, + policy: MediaPolicy, + transport: httpx.AsyncBaseTransport | None = None, +) -> httpx.Headers: + """Download ``url`` to ``target`` under ``policy``; returns the final response headers.""" + os.makedirs(os.path.dirname(os.path.abspath(target)), exist_ok=True) + try: + async with httpx.AsyncClient(follow_redirects=False, timeout=READ_TIMEOUT_S, transport=transport) as client: + for _ in range(MAX_REDIRECTS + 1): + await _check_url(url, policy) + async with client.stream("GET", url) as response: + if response.is_redirect: + location = response.headers.get("location") + if not location: + raise MediaPolicyError("Media URL redirect has no location.") + url = urljoin(url, location) + continue + response.raise_for_status() + written = 0 + async with aiofiles.open(target, "wb") as output: + async for chunk in response.aiter_bytes(): + written += len(chunk) + if written > max_bytes: + raise MediaPolicyError( + f"Media reference is larger than the {max_bytes}-byte limit; raise " + "server.media_reference_max_bytes in the serve config or provide a smaller file.") + await output.write(chunk) + return response.headers + raise MediaPolicyError(f"Media URL redirected more than {MAX_REDIRECTS} times.") + except BaseException: + # Includes cancellation: a client disconnect must not leave a partial upload behind. + discard(target) + raise diff --git a/fastvideo/entrypoints/openai/protocol.py b/fastvideo/entrypoints/openai/protocol.py index 52ad00a2c..f23185ceb 100644 --- a/fastvideo/entrypoints/openai/protocol.py +++ b/fastvideo/entrypoints/openai/protocol.py @@ -145,6 +145,8 @@ class VideoGenerationRequest(BaseModel): # SGLang spelling retained as an alias-like input field. n: int | None = Field(default=None, ge=1, le=10) start_time_seconds: float | None = Field(default=None, ge=0.0) + audio_start_in_s: float | None = Field(default=None, ge=0.0) + audio_end_in_s: float | None = Field(default=None, gt=0.0) quality: Literal["auto", "default", "standard", "hd"] | None = None negative_prompt: str | None = None num_inference_steps: int | None = Field(default=None, ge=1, le=200) @@ -226,7 +228,7 @@ class VideoResponse(BaseModel): expires_at: int | None = None file_path: str | None = None file_name: str | None = None - media_type: Literal["video/mp4"] = "video/mp4" + media_type: Literal["video/mp4", "audio/wav"] = "video/mp4" completed_at: int | None = None error: VideoError | None = None peak_memory_mb: float | None = None diff --git a/fastvideo/entrypoints/openai/request_adapter.py b/fastvideo/entrypoints/openai/request_adapter.py index 94a9b0737..ed767e054 100644 --- a/fastvideo/entrypoints/openai/request_adapter.py +++ b/fastvideo/entrypoints/openai/request_adapter.py @@ -7,7 +7,16 @@ import asyncio import math import os from typing import Any +from urllib.parse import urlparse +from fastvideo.entrypoints.openai.media_policy import ( + MediaPolicy, + MediaPolicyError, + discard, + fetch_media, + resolve_local_media, +) +from fastvideo.entrypoints.openai.state import get_media_policy, get_media_reference_max_bytes from fastvideo.api.compat import ( REQUEST_BATCH_EXTRA_PASSTHROUGH_FIELDS, explicit_request_updates, @@ -19,9 +28,10 @@ from fastvideo.entrypoints.openai.protocol import ( FileImageReference, FileVideoReference, UrlImageReference, + UrlVideoReference, VideoGenerationRequest, ) -from fastvideo.entrypoints.openai.utils import save_image_to_path +from fastvideo.entrypoints.openai.utils import image_suffix, save_base64_image_to_path from fastvideo.fastvideo_args import FastVideoArgs from fastvideo.models.vision_utils import load_image from fastvideo.registry import get_preset_selection @@ -31,6 +41,9 @@ class RequestAdaptationError(ValueError): """The transport request cannot be represented by the loaded pipeline.""" +_VIDEO_SUFFIXES = frozenset({".avi", ".m4v", ".mkv", ".mov", ".mp4", ".mpeg", ".mpg", ".webm"}) + + def _as_list(value: Any | list[Any] | None) -> list[Any]: if value is None: return [] @@ -54,50 +67,158 @@ def _image_sources(request: VideoGenerationRequest) -> list[str]: return sources +def discard_materialized_media(paths: list[str]) -> None: + for path in paths: + discard(path) + + +async def _download_media(source: str, + target: str, + *, + max_bytes: int | None = None, + policy: MediaPolicy | None = None) -> None: + try: + await fetch_media(source, + target, + max_bytes=get_media_reference_max_bytes() if max_bytes is None else max_bytes, + policy=get_media_policy() if policy is None else policy) + except MediaPolicyError as error: + raise RequestAdaptationError(str(error)) from error + + +def _local_media(source: str, policy: MediaPolicy) -> str: + try: + return resolve_local_media(source, policy) + except MediaPolicyError as error: + raise RequestAdaptationError(str(error)) from error + + +async def _materialize_video(source: str, + target_base: str, + *, + max_bytes: int | None = None, + policy: MediaPolicy | None = None) -> str: + policy = get_media_policy() if policy is None else policy + source = source.strip() + if not source: + raise RequestAdaptationError("Video references must not be empty.") + if source.lower().startswith(("http://", "https://")): + suffix = os.path.splitext(urlparse(source).path)[1].lower() + if suffix not in _VIDEO_SUFFIXES: + suffix = ".mp4" + target = f"{target_base}{suffix}" + try: + await _download_media(source, target, max_bytes=max_bytes, policy=policy) + except RequestAdaptationError: + raise + except Exception as error: + raise RequestAdaptationError(f"Unable to fetch video reference: {error}") from error + return target + if source.lower().startswith("data:"): + raise RequestAdaptationError("Data video references are not supported; provide a URL or local path.") + return _local_media(source, policy) + + +async def _materialize_image_url(source: str, target_base: str, *, max_bytes: int | None, policy: MediaPolicy) -> str: + target = f"{target_base}{image_suffix(source)}" + try: + await _download_media(source, target, max_bytes=max_bytes, policy=policy) + except RequestAdaptationError: + raise + except Exception as error: + raise RequestAdaptationError(f"Unable to fetch image reference: {error}") from error + return target + + async def prepare_reference_media( request_id: str, request: VideoGenerationRequest, output_dir: str, -) -> None: - """Materialize and decode image references before a job reaches workers.""" + *, + max_bytes: int | None = None, + policy: MediaPolicy | None = None, +) -> list[str]: + """Materialize media references and return the files created for this request.""" + policy = get_media_policy() if policy is None else policy _image_sources(request) + _video_sources(request) uploads_dir = os.path.join(os.path.abspath(output_dir), "uploads") + created: list[str] = [] + + def track(path: str) -> str: + if path not in created: + created.append(path) + return path + + def is_remote(source: str) -> bool: + return source.lower().startswith(("http://", "https://", "data:image")) async def materialize(source: str, index: int) -> str: source = source.strip() if not source: raise RequestAdaptationError("Image references must not be empty.") - if source.lower().startswith(("http://", "https://", "data:image")): - target = os.path.join(uploads_dir, f"{request_id}_{index}") + target = os.path.join(uploads_dir, f"{request_id}_{index}") + if source.startswith("data:image"): try: - local_path = await save_image_to_path(source, target) + local_path = track(await save_base64_image_to_path(source, target)) except Exception as error: - raise RequestAdaptationError(f"Unable to fetch image reference: {error}") from error + raise RequestAdaptationError(f"Unable to decode image reference: {error}") from error + elif is_remote(source): + local_path = track(await _materialize_image_url(source, target, max_bytes=max_bytes, policy=policy)) else: - local_path = os.path.abspath(os.path.expanduser(source)) - if not os.path.isfile(local_path): - raise RequestAdaptationError(f"Image reference does not exist: {source}") + local_path = _local_media(source, policy) try: await asyncio.to_thread(load_image, local_path) except Exception as error: raise RequestAdaptationError(f"Unable to decode image reference: {error}") from error return local_path - index = 0 - references = _as_list(request.image_reference) - for reference in references: - if isinstance(reference, UrlImageReference): - reference.image_url = await materialize(reference.image_url, index) + try: + index = 0 + for reference in _as_list(request.image_reference): + if isinstance(reference, UrlImageReference): + reference.image_url = await materialize(reference.image_url, index) + index += 1 + if request.input_reference: + request.input_reference = await materialize(request.input_reference, index) index += 1 - if request.input_reference: - request.input_reference = await materialize(request.input_reference, index) - index += 1 - elif request.input_reference == "": - request.input_reference = None - if request.reference_url: - request.reference_url = await materialize(request.reference_url, index) - elif request.reference_url == "": - request.reference_url = None + elif request.input_reference == "": + request.input_reference = None + if request.reference_url: + request.reference_url = await materialize(request.reference_url, index) + elif request.reference_url == "": + request.reference_url = None + + for index, reference in enumerate(_as_list(request.video_reference)): + if isinstance(reference, UrlVideoReference): + remote = is_remote(reference.video_url) + reference.video_url = await _materialize_video(reference.video_url, + os.path.join(uploads_dir, f"{request_id}_video_{index}"), + max_bytes=max_bytes, + policy=policy) + if remote: + track(reference.video_url) + if request.video_path: + remote = is_remote(request.video_path) + request.video_path = await _materialize_video(request.video_path, + os.path.join(uploads_dir, f"{request_id}_video_path"), + max_bytes=max_bytes, + policy=policy) + if remote: + track(request.video_path) + if request.video_url: + remote = is_remote(request.video_url) + request.video_url = await _materialize_video(request.video_url, + os.path.join(uploads_dir, f"{request_id}_video_url"), + max_bytes=max_bytes, + policy=policy) + if remote: + track(request.video_url) + except BaseException: + # Includes cancellation, so a disconnected client leaves no downloaded inputs behind. + discard_materialized_media(created) + raise + return created def _video_sources(request: VideoGenerationRequest) -> list[str]: @@ -288,6 +409,9 @@ def build_generation_request( ) -> GenerationRequest: """Build one tracked FastVideo request using explicit-field precedence.""" validate_model_and_lora(request, args, served_model_name) + workload_type = getattr(args, "workload_type", "") + workload_value = str(getattr(workload_type, "value", workload_type) or "") + is_audio = workload_value.endswith("2a") kwargs: dict[str, Any] = {} if default_request is not None: kwargs.update(explicit_request_updates(default_request)) @@ -323,7 +447,7 @@ def build_generation_request( num_frames = request.num_frames if "num_frames" in body_set else request.video_params.num_frames if num_frames is not None: kwargs["num_frames"] = num_frames - elif "seconds" in body_set and request.seconds is not None: + elif not is_audio and "seconds" in body_set and request.seconds is not None: kwargs["num_frames"] = int(request.seconds) * int(kwargs["fps"]) direct_fields = ( @@ -336,12 +460,25 @@ def build_generation_request( "enable_teacache", "max_sequence_length", "boundary_ratio", + "audio_start_in_s", + "audio_end_in_s", ) for name in direct_fields: if name in body_set: value = getattr(request, name) if value is not None: kwargs[name] = value + if is_audio and "seconds" in body_set and request.seconds is not None and "audio_end_in_s" not in body_set: + start = float(kwargs.get("audio_start_in_s") or 0.0) + kwargs["audio_start_in_s"] = start + kwargs["audio_end_in_s"] = start + float(int(request.seconds)) + if is_audio: + if {"size", "width", "height", "video_params"} & body_set: + raise RequestAdaptationError("Audio generation does not accept size, width, height, or video_params.") + start = float(kwargs.get("audio_start_in_s") or 0.0) + end = kwargs.get("audio_end_in_s") + if end is not None and float(end) <= start: + raise RequestAdaptationError(f"audio_end_in_s ({end}) must be greater than audio_start_in_s ({start}).") if "n" in body_set or "num_outputs_per_prompt" in body_set: kwargs["num_videos_per_prompt"] = request.resolved_num_outputs @@ -355,6 +492,8 @@ def build_generation_request( raise RequestAdaptationError("short_edge requires aspect_ratio.") _apply_aspect_ratio(kwargs, request, model_family=model_family) _apply_reference_inputs(kwargs, request, args, model_family=model_family) + if model_family == "mmaudio" and not workload_value.endswith("t2a") and not kwargs.get("video_path"): + raise RequestAdaptationError("MMAudio video-to-audio generation requires video_reference or video_path.") extension_fields = ("flow_shift", "sound_duration", "start_time_seconds") for name in extension_fields: @@ -411,7 +550,9 @@ def build_generation_request( f"got {requested_num_frames}, next valid value is {aligned_num_frames}.") kwargs["num_frames"] = aligned_num_frames - output_path = os.path.join(os.path.abspath(output_dir), "videos", f"{request_id}.mp4") + media_dir = "audio" if is_audio else "videos" + extension = ".wav" if is_audio else ".mp4" + output_path = os.path.join(os.path.abspath(output_dir), media_dir, f"{request_id}{extension}") os.makedirs(os.path.dirname(output_path), exist_ok=True) kwargs.update({ "output_path": output_path, diff --git a/fastvideo/entrypoints/openai/state.py b/fastvideo/entrypoints/openai/state.py index 66fd39a45..aea04631a 100644 --- a/fastvideo/entrypoints/openai/state.py +++ b/fastvideo/entrypoints/openai/state.py @@ -9,6 +9,9 @@ from __future__ import annotations from typing import TYPE_CHECKING +from fastvideo.api.schema import DEFAULT_MEDIA_REFERENCE_MAX_BYTES +from fastvideo.entrypoints.openai.media_policy import MediaPolicy + if TYPE_CHECKING: from fastvideo.api.schema import GenerationRequest from fastvideo.entrypoints.openai.serving_engine import OpenAIServingEngine, ServingGenerator @@ -22,6 +25,8 @@ _fastvideo_args: FastVideoArgs | None = None _output_dir: str = DEFAULT_OUTPUT_DIR _served_model_name: str | None = None _default_request: GenerationRequest | None = None +_media_reference_max_bytes: int = DEFAULT_MEDIA_REFERENCE_MAX_BYTES +_media_policy: MediaPolicy = MediaPolicy() def get_generator() -> ServingGenerator: @@ -60,6 +65,16 @@ def get_default_request() -> GenerationRequest | None: return _default_request +def get_media_reference_max_bytes() -> int: + """Return the server's URL media-reference byte cap.""" + return _media_reference_max_bytes + + +def get_media_policy() -> MediaPolicy: + """Return the local-root and private-address rules for caller media references.""" + return _media_policy + + def set_state( generator: ServingGenerator, serving_engine: OpenAIServingEngine, @@ -67,22 +82,30 @@ def set_state( output_dir: str, default_request: GenerationRequest | None = None, served_model_name: str | None = None, + media_reference_max_bytes: int = DEFAULT_MEDIA_REFERENCE_MAX_BYTES, + media_policy: MediaPolicy | None = None, ) -> None: """Set all server state at once (called from lifespan).""" global _generator, _serving_engine, _fastvideo_args, _output_dir, _served_model_name, _default_request + global _media_reference_max_bytes, _media_policy _generator = generator _serving_engine = serving_engine _fastvideo_args = fastvideo_args _output_dir = output_dir _served_model_name = served_model_name _default_request = default_request + _media_reference_max_bytes = media_reference_max_bytes + _media_policy = media_policy or MediaPolicy() def clear_state() -> None: """Clear server state on shutdown.""" global _generator, _serving_engine, _fastvideo_args, _served_model_name, _default_request + global _media_reference_max_bytes, _media_policy _generator = None _serving_engine = None _fastvideo_args = None _served_model_name = None _default_request = None + _media_reference_max_bytes = DEFAULT_MEDIA_REFERENCE_MAX_BYTES + _media_policy = MediaPolicy() diff --git a/fastvideo/entrypoints/openai/utils.py b/fastvideo/entrypoints/openai/utils.py index f49bc6d72..dc476386d 100644 --- a/fastvideo/entrypoints/openai/utils.py +++ b/fastvideo/entrypoints/openai/utils.py @@ -7,7 +7,6 @@ import re from typing import Any import aiofiles -import httpx from fastapi import UploadFile from fastvideo.logger import init_logger @@ -59,41 +58,37 @@ async def _maybe_url_image(img_url: str, target_path: str) -> str | None: if img_url.lower().startswith(("http://", "https://")): return await _save_url_image_to_path(img_url, target_path) elif img_url.startswith("data:image"): - return await _save_base64_image_to_path(img_url, target_path) + return await save_base64_image_to_path(img_url, target_path) else: raise ValueError("Unsupported image url format") +_IMAGE_SUFFIXES = frozenset({".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"}) + + +def image_suffix(image_url: str) -> str: + """File suffix for a downloaded image; decoding inspects content, so this is only a hint.""" + suffix = os.path.splitext(image_url.split("?")[0])[1].lower() + if suffix not in _IMAGE_SUFFIXES: + return ".jpg" + return ".jpg" if suffix == ".jpeg" else suffix + + async def _save_url_image_to_path(image_url: str, target_path: str) -> str: - os.makedirs(os.path.dirname(target_path), exist_ok=True) + # Local imports: state imports serving modules that import these helpers. + from fastvideo.entrypoints.openai.media_policy import fetch_media + from fastvideo.entrypoints.openai.state import get_media_policy, get_media_reference_max_bytes + + if not os.path.splitext(target_path)[1]: + target_path = f"{target_path}{image_suffix(image_url)}" try: - async with httpx.AsyncClient(follow_redirects=True) as client: - response = await client.get(image_url, timeout=30.0) - response.raise_for_status() - - if not os.path.splitext(target_path)[1]: - content_type = response.headers.get("content-type", "").lower() - url_path = image_url.split("?")[0] - _, url_ext = os.path.splitext(url_path) - url_ext = url_ext.lower() - if url_ext in {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"}: - ext = ".jpg" if url_ext == ".jpeg" else url_ext - elif "png" in content_type: - ext = ".png" - elif "webp" in content_type: - ext = ".webp" - else: - ext = ".jpg" - target_path = f"{target_path}{ext}" - - async with aiofiles.open(target_path, "wb") as f: - await f.write(response.content) - return target_path + await fetch_media(image_url, target_path, max_bytes=get_media_reference_max_bytes(), policy=get_media_policy()) except Exception as e: raise RuntimeError(f"Failed to download image from URL: {e}") from e + return target_path -async def _save_base64_image_to_path(base64_data: str, target_path: str) -> str: +async def save_base64_image_to_path(base64_data: str, target_path: str) -> str: pattern = r"data:(.*?)(;base64)?,(.*)" match = re.match(pattern, base64_data) if not match or not match.group(2): diff --git a/fastvideo/entrypoints/openai/video_api.py b/fastvideo/entrypoints/openai/video_api.py index 5c1c7a235..92f481819 100644 --- a/fastvideo/entrypoints/openai/video_api.py +++ b/fastvideo/entrypoints/openai/video_api.py @@ -28,11 +28,13 @@ from fastvideo.entrypoints.openai.protocol import ( ) from fastvideo.entrypoints.openai.request_adapter import ( build_generation_request, + discard_materialized_media, prepare_reference_media, validate_model_and_lora, ) from fastvideo.entrypoints.openai.state import ( get_default_request, + get_media_reference_max_bytes, get_output_dir, get_served_model_name, get_server_args, @@ -113,6 +115,8 @@ def _build_generation_kwargs( "enable_teacache", "max_sequence_length", "boundary_ratio", + "audio_start_in_s", + "audio_end_in_s", ): if name in body_set and getattr(req, name) is not None: kwargs[name] = getattr(req, name) @@ -122,9 +126,11 @@ def _build_generation_kwargs( kwargs["image_path"] = req.input_reference kwargs.pop("output_path", None) - output_dir = os.path.join(os.path.abspath(get_output_dir()), "videos") + is_audio = _is_audio_workload() + output_dir = os.path.join(os.path.abspath(get_output_dir()), "audio" if is_audio else "videos") os.makedirs(output_dir, exist_ok=True) - kwargs["output_path"] = os.path.join(output_dir, f"{request_id}.mp4") + extension = ".wav" if is_audio else ".mp4" + kwargs["output_path"] = os.path.join(output_dir, f"{request_id}{extension}") kwargs["save_video"] = True return kwargs @@ -135,13 +141,27 @@ def _result_value(result: Any, name: str, default: Any = None) -> Any: return getattr(result, name, default) -def _remove_artifact(file_path: str | None) -> None: - if not file_path or not os.path.isfile(file_path): - return +def _is_audio_workload() -> bool: try: - os.unlink(file_path) - except OSError: - logger.warning("Failed to delete video artifact %s", file_path, exc_info=True) + args = get_server_args() + except AssertionError: + return False + workload_type = getattr(args, "workload_type", "") + return str(getattr(workload_type, "value", workload_type) or "").endswith("2a") + + +def _media_type_for_path(path: str) -> str: + return "audio/wav" if path.lower().endswith(".wav") else "video/mp4" + + +def _remove_artifact(file_path: str | None) -> None: + if file_path: + discard_materialized_media([file_path]) + + +def _remove_artifacts(file_path: str | None, materialized: list[str]) -> None: + _remove_artifact(file_path) + discard_materialized_media(materialized) def _stage_durations(result: Any) -> dict[str, float]: @@ -162,8 +182,14 @@ def _make_video_job( generation_request: GenerationRequest, ) -> dict[str, Any]: sampling = request_to_sampling_param(generation_request, model_path=get_server_args().model_path) - size = f"{sampling.width}x{sampling.height}" if sampling.width and sampling.height else None - seconds = int(round(sampling.num_frames / sampling.fps)) if sampling.fps else int(req.seconds or 4) + is_audio = _is_audio_workload() + size = None if is_audio else (f"{sampling.width}x{sampling.height}" if sampling.width and sampling.height else None) + if is_audio and sampling.audio_end_in_s is not None: + start = sampling.audio_start_in_s or 0.0 + seconds = max(1, int(round(sampling.audio_end_in_s - start))) + else: + seconds = int(round(sampling.num_frames / sampling.fps)) if sampling.fps else int(req.seconds or 4) + media_type = "audio/wav" if is_audio else "video/mp4" return { "id": request_id, "object": "video", @@ -175,6 +201,7 @@ def _make_video_job( "size": size, "seconds": str(max(1, seconds)), "quality": req.quality or "default", + "media_type": media_type, # ``file_path`` is a FastVideo compatibility extension. vLLM-Omni's # public job shape uses ``file_name`` after completion. "file_path": None, @@ -185,6 +212,7 @@ def _make_video_job( async def _run_generation( request_id: str, generation_request: GenerationRequest, + materialized_media: list[str] | None = None, ) -> None: started = 0.0 video_path = generation_request.output.output_path @@ -242,11 +270,8 @@ async def _run_generation( finally: if request_id in _DELETED_VIDEO_IDS: _DELETED_VIDEO_IDS.discard(request_id) - if video_path and os.path.isfile(video_path): - try: - os.unlink(video_path) - except OSError: - logger.warning("Failed to clean up deleted video artifact %s", video_path, exc_info=True) + _remove_artifact(video_path) + discard_materialized_media(materialized_media or []) def _track_video_job(request_id: str, task: asyncio.Task[None]) -> None: @@ -329,12 +354,16 @@ async def _parse_video_request(raw_request: Request) -> VideoGenerationRequest: raise HTTPException(status_code=400, detail=f"Invalid request body: {error}") from error -async def _adapt_request(request_id: str, request: VideoGenerationRequest) -> GenerationRequest: +async def _adapt_request(request_id: str, request: VideoGenerationRequest) -> tuple[GenerationRequest, list[str]]: + materialized_media: list[str] = [] try: get_serving_engine().validate_video_request(request) validate_model_and_lora(request, get_server_args(), get_served_model_name()) - await prepare_reference_media(request_id, request, get_output_dir()) - return await asyncio.to_thread( + materialized_media = await prepare_reference_media(request_id, + request, + get_output_dir(), + max_bytes=get_media_reference_max_bytes()) + generation_request = await asyncio.to_thread( build_generation_request, request_id, request, @@ -343,7 +372,9 @@ async def _adapt_request(request_id: str, request: VideoGenerationRequest) -> Ge output_dir=get_output_dir(), default_request=get_default_request(), ) + return generation_request, materialized_media except Exception as error: + discard_materialized_media(materialized_media) raise HTTPException(status_code=400, detail=str(error)) from error @@ -353,47 +384,59 @@ async def create_video(raw_request: Request) -> VideoResponse: """Create an asynchronous video generation job.""" request = await _parse_video_request(raw_request) request_id = f"video_gen_{generate_request_id()}" - generation_request = await _adapt_request(request_id, request) - job = _make_video_job(request_id, request, generation_request) - await VIDEO_STORE.upsert(request_id, job) - task = asyncio.create_task(_run_generation(request_id, generation_request), name=f"video-job-{request_id}") + generation_request, materialized_media = await _adapt_request(request_id, request) + try: + job = _make_video_job(request_id, request, generation_request) + await VIDEO_STORE.upsert(request_id, job) + except Exception: + discard_materialized_media(materialized_media) + raise + task = asyncio.create_task(_run_generation(request_id, generation_request, materialized_media), + name=f"video-job-{request_id}") + task.add_done_callback(lambda _: discard_materialized_media(materialized_media)) _track_video_job(request_id, task) return VideoResponse(**job) @router.post("/sync") async def create_video_sync(raw_request: Request) -> FileResponse: - """Generate synchronously and return raw MP4 bytes with vLLM headers.""" + """Generate synchronously and return the generated media with vLLM headers.""" request = await _parse_video_request(raw_request) request_id = f"video_sync-{generate_request_id()}" - generation_request = await _adapt_request(request_id, request) + generation_request, materialized_media = await _adapt_request(request_id, request) started = time.perf_counter() + response: FileResponse | None = None try: - result = await get_serving_engine().generate(generation_request) - except Exception as error: - logger.exception("Sync video generation failed for %s", request_id) - raise HTTPException(status_code=500, detail=f"Video generation failed: {error}") from error - if isinstance(result, list): - if not result: - raise HTTPException(status_code=500, detail="FastVideo returned no generation results") - result = result[0] - elapsed = time.perf_counter() - started - video_path = _result_value(result, "video_path") or generation_request.output.output_path - if not video_path or not os.path.exists(video_path): - raise HTTPException(status_code=500, detail="FastVideo did not produce an MP4 file") - return FileResponse( - video_path, - media_type="video/mp4", - filename=os.path.basename(video_path), - headers={ - "X-Request-Id": request_id, - "X-Model": get_served_model_name(), - "X-Inference-Time-S": f"{elapsed:.3f}", - "X-Stage-Durations": json.dumps(_stage_durations(result), separators=(",", ":")), - "X-Peak-Memory-MB": f"{float(_result_value(result, 'peak_memory_mb', 0.0) or 0.0):.3f}", - }, - background=BackgroundTask(_remove_artifact, video_path), - ) + try: + result = await get_serving_engine().generate(generation_request) + except Exception as error: + logger.exception("Sync video generation failed for %s", request_id) + raise HTTPException(status_code=500, detail=f"Video generation failed: {error}") from error + if isinstance(result, list): + if not result: + raise HTTPException(status_code=500, detail="FastVideo returned no generation results") + result = result[0] + elapsed = time.perf_counter() - started + video_path = _result_value(result, "video_path") or generation_request.output.output_path + if not video_path or not os.path.exists(video_path): + raise HTTPException(status_code=500, detail="FastVideo did not produce a media file") + response = FileResponse( + video_path, + media_type=_media_type_for_path(video_path), + filename=os.path.basename(video_path), + headers={ + "X-Request-Id": request_id, + "X-Model": get_served_model_name(), + "X-Inference-Time-S": f"{elapsed:.3f}", + "X-Stage-Durations": json.dumps(_stage_durations(result), separators=(",", ":")), + "X-Peak-Memory-MB": f"{float(_result_value(result, 'peak_memory_mb', 0.0) or 0.0):.3f}", + }, + background=BackgroundTask(_remove_artifacts, video_path, materialized_media), + ) + return response + finally: + if response is None: + discard_materialized_media(materialized_media) @router.get("", response_model=VideoListResponse) @@ -470,7 +513,9 @@ async def download_video_content(video_id: str = Path(...), variant: str | None raise HTTPException(status_code=404, detail="Generation is still in-progress") if not os.path.exists(file_path): raise HTTPException(status_code=404, detail="Generated video file not found on disk") - return FileResponse(path=file_path, media_type="video/mp4", filename=os.path.basename(file_path)) + return FileResponse(path=file_path, + media_type=_media_type_for_path(file_path), + filename=os.path.basename(file_path)) __all__ = [ diff --git a/fastvideo/fastvideo_args.py b/fastvideo/fastvideo_args.py index c52276cb8..818834e18 100644 --- a/fastvideo/fastvideo_args.py +++ b/fastvideo/fastvideo_args.py @@ -231,6 +231,9 @@ class FastVideoArgs: video_decode_backend: str = "h3-vae" taeh3_checkpoint: str | None = None taeh3_chunk_size: int = 5 + # Exported NVFP4 MiniMax-H3 decoder file (decoder-nvfp4.safetensors or decoder.safetensors) that replaces + # the VAE decoder at load. Overrides FASTVIDEO_H3_VAE_NVFP4_DECODER. Needs Blackwell FP4 hardware. + h3_vae_nvfp4_decoder: str | None = None # Load each heavy component on first use and free it once the last stage # that holds it has run, instead of keeping every component resident from diff --git a/fastvideo/models/loader/component_loader.py b/fastvideo/models/loader/component_loader.py index 35eacaebf..f5e5d0add 100644 --- a/fastvideo/models/loader/component_loader.py +++ b/fastvideo/models/loader/component_loader.py @@ -828,12 +828,15 @@ class VAELoader(ComponentLoader): return None @staticmethod - def _defers_h3_nvfp4_decoder(class_name: str | None) -> bool: - return class_name == "AutoencoderKLMiniMaxH3" and bool(envs.FASTVIDEO_H3_VAE_NVFP4_DECODER.get()) + def _h3_nvfp4_decoder_path(class_name: str | None, fastvideo_args: FastVideoArgs) -> str | None: + """The NVFP4 decoder to swap in: the serve-config field, else ``FASTVIDEO_H3_VAE_NVFP4_DECODER``.""" + if class_name != "AutoencoderKLMiniMaxH3": + return None + return getattr(fastvideo_args, "h3_vae_nvfp4_decoder", None) or envs.FASTVIDEO_H3_VAE_NVFP4_DECODER.get() @staticmethod def _apply_h3_nvfp4_decoder(vae: nn.Module, path: str, target_device: torch.device) -> None: - """Swap in an exported NVFP4 MiniMax-H3 decoder (``FASTVIDEO_H3_VAE_NVFP4_DECODER``).""" + """Swap in an exported NVFP4 MiniMax-H3 decoder (``h3_vae_nvfp4_decoder``).""" from fastvideo.models.vaes.minimax_h3_nvfp4_decoder import ( apply_nvfp4_decoder_checkpoint, freeze_nvfp4_linears, @@ -841,9 +844,9 @@ class VAELoader(ComponentLoader): ) if not os.path.isfile(path): - raise FileNotFoundError(f"FASTVIDEO_H3_VAE_NVFP4_DECODER={path} does not exist") + raise FileNotFoundError(f"NVFP4 H3 decoder {path} does not exist") loaded_blocks = len(vae.decoder.transformer_blocks) - # The swap runs where the VAE was built (the CPU, see ``_defers_h3_nvfp4_decoder``); a pre-packed + # The swap runs where the VAE was built (the CPU, see ``_h3_nvfp4_decoder_path``); a pre-packed # file never allocates the quantized linears' full-precision weights, a bf16 file packs on the GPU. device = next(vae.parameters()).device metadata = apply_nvfp4_decoder_checkpoint(vae, load_nvfp4_decoder_checkpoint(path, device), freeze=False) @@ -978,7 +981,7 @@ class VAELoader(ComponentLoader): vae = vae_cls(vae_config) # An NVFP4 H3 decoder is swapped in on the CPU first, so the dense decoder it replaces # never reaches the GPU; the VAE moves to the target device after the swap. - if not self._defers_h3_nvfp4_decoder(class_name): + if not self._h3_nvfp4_decoder_path(class_name, fastvideo_args): vae = vae.to(target_device) # Find all safetensors files @@ -990,9 +993,9 @@ class VAELoader(ComponentLoader): int8_convrot_overlay_to_apply, ) int8_convrot_path = int8_convrot_overlay_to_apply(model_path) - if int8_convrot_path is not None and envs.FASTVIDEO_H3_VAE_NVFP4_DECODER.get(): - logger.info("Skipping MiniMax-H3 INT8 ConvRot VAE overlay %s: FASTVIDEO_H3_VAE_NVFP4_DECODER " - "replaces the decoder", int8_convrot_path) + if int8_convrot_path is not None and self._h3_nvfp4_decoder_path(class_name, fastvideo_args): + logger.info("Skipping MiniMax-H3 INT8 ConvRot VAE overlay %s: the NVFP4 decoder replaces the " + "decoder", int8_convrot_path) int8_convrot_path = None safetensors_list = dense_vae_safetensors(safetensors_list) if not safetensors_list: @@ -1035,8 +1038,9 @@ class VAELoader(ComponentLoader): if class_name == "AutoencoderKLMiniMaxH3" and int8_convrot_path is not None: from fastvideo.models.vaes.minimax_h3_int8_convrot import overlay_minimax_h3_int8_convrot_decoder overlay_minimax_h3_int8_convrot_decoder(vae, int8_convrot_path) - if class_name == "AutoencoderKLMiniMaxH3" and envs.FASTVIDEO_H3_VAE_NVFP4_DECODER.get(): - self._apply_h3_nvfp4_decoder(vae, envs.FASTVIDEO_H3_VAE_NVFP4_DECODER.get(), target_device) + nvfp4_decoder = self._h3_nvfp4_decoder_path(class_name, fastvideo_args) + if nvfp4_decoder: + self._apply_h3_nvfp4_decoder(vae, nvfp4_decoder, target_device) if (class_name == "AutoencoderKLWan" and getattr(vae.config, "use_light_vae", False) and target_device.type == "cuda" and hasattr(vae, "optimize_memory_format")): vae.optimize_memory_format() diff --git a/fastvideo/registry.py b/fastvideo/registry.py index f9962885e..2df3914f0 100644 --- a/fastvideo/registry.py +++ b/fastvideo/registry.py @@ -220,6 +220,7 @@ def _get_config_info( *, raise_on_missing: bool = True, revision: str | None = None, + allow_remote: bool = True, ) -> ConfigInfo | None: # 1. Exact match if model_path in _MODEL_HF_PATH_TO_NAME: @@ -240,8 +241,10 @@ def _get_config_info( # 3. Use detectors (path or model_index pipeline name). if os.path.exists(model_path): config = verify_model_config_and_directory(model_path, required_component_dirs=[]) - else: + elif allow_remote: config = maybe_download_model_index(model_path, revision=revision) + else: + return None pipeline_name = (_resolve_class_name(config) or "").lower() scheduler = config.get("scheduler") @@ -1277,8 +1280,8 @@ def get_pipeline_config_cls_from_name(pipeline_name_or_path: str) -> type[Pipeli return config_info.pipeline_config_cls -def get_sampling_param_cls_for_name(pipeline_name_or_path: str) -> Any | None: - config_info = _get_config_info(pipeline_name_or_path, raise_on_missing=False) +def get_sampling_param_cls_for_name(pipeline_name_or_path: str, *, allow_remote: bool = True) -> Any | None: + config_info = _get_config_info(pipeline_name_or_path, raise_on_missing=False, allow_remote=allow_remote) if config_info is None: logger.warning("No match found for pipeline %s, using default sampling param.", pipeline_name_or_path) return None diff --git a/fastvideo/tests/api/test_cli_translation.py b/fastvideo/tests/api/test_cli_translation.py index b11d621f9..6226ff654 100644 --- a/fastvideo/tests/api/test_cli_translation.py +++ b/fastvideo/tests/api/test_cli_translation.py @@ -473,7 +473,14 @@ def test_serve_subcommand_dispatches_via_typed_config(tmp_path, monkeypatch): captured["config"] = config return SimpleNamespace(model_path=config.model_path) - def fake_run_server(fastvideo_args, host, port, output_dir, default_request, served_model_name=None): + def fake_run_server(fastvideo_args, + host, + port, + output_dir, + default_request, + served_model_name=None, + media_reference_max_bytes=None, + media_policy=None): captured["fastvideo_args"] = fastvideo_args captured["host"] = host captured["port"] = port @@ -512,8 +519,17 @@ def test_serve_subcommand_forwards_default_request(tmp_path, monkeypatch): def fake_generator_config_to_fastvideo_args(config): return SimpleNamespace(model_path=config.model_path) - def fake_run_server(fastvideo_args, host, port, output_dir, default_request, served_model_name=None): + def fake_run_server(fastvideo_args, + host, + port, + output_dir, + default_request, + served_model_name=None, + media_reference_max_bytes=None, + media_policy=None): captured["default_request"] = default_request + captured["media_reference_max_bytes"] = media_reference_max_bytes + captured["media_policy"] = media_policy monkeypatch.setattr( "fastvideo.entrypoints.cli.serve.generator_config_to_fastvideo_args", @@ -526,6 +542,9 @@ def test_serve_subcommand_forwards_default_request(tmp_path, monkeypatch): default_request = captured["default_request"] assert default_request.prompt == "hello" assert default_request.sampling.seed == 42 + # Without server.media_roots or allow_private_media_urls the CLI passes the closed policy. + assert captured["media_policy"].roots == () + assert captured["media_policy"].allow_private_urls is False def test_main_rejects_top_level_config_without_subcommand(tmp_path, monkeypatch): diff --git a/fastvideo/tests/api/test_configs.py b/fastvideo/tests/api/test_configs.py index 9cbbc15e2..8ebd3417f 100644 --- a/fastvideo/tests/api/test_configs.py +++ b/fastvideo/tests/api/test_configs.py @@ -37,6 +37,9 @@ def test_serve_config_includes_server_and_default_request_defaults() -> None: "port": 8000, "output_dir": "outputs/", "served_model_name": None, + "media_reference_max_bytes": 2 * 1024**3, + "media_roots": [], + "allow_private_media_urls": False, } assert dumped["default_request"]["sampling"]["fps"] == 24 assert dumped["default_request"]["runtime"]["enable_teacache"] is False diff --git a/fastvideo/tests/api/test_parser.py b/fastvideo/tests/api/test_parser.py index 27412cc0d..406528f58 100644 --- a/fastvideo/tests/api/test_parser.py +++ b/fastvideo/tests/api/test_parser.py @@ -226,6 +226,8 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None: "height_sr": 1072, "width_sr": 1920, "fps": 24, + "audio_start_in_s": None, + "audio_end_in_s": None, "num_inference_steps": 50, "num_inference_steps_sr": 50, "guidance_scale": 1.0, diff --git a/fastvideo/tests/entrypoints/test_media_policy.py b/fastvideo/tests/entrypoints/test_media_policy.py new file mode 100644 index 000000000..955f5f422 --- /dev/null +++ b/fastvideo/tests/entrypoints/test_media_policy.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Media references from HTTP callers: local roots, private addresses, size caps, cleanup.""" + +import asyncio +import os + +import httpx +import pytest + +from fastvideo.entrypoints.openai.media_policy import ( + MediaPolicy, + MediaPolicyError, + fetch_media, + resolve_local_media, +) + + +def test_local_paths_are_rejected_without_a_configured_root(tmp_path): + existing = tmp_path / "clip.mp4" + existing.write_bytes(b"x") + missing = tmp_path / "missing.mp4" + errors = [] + for path in (existing, missing): + with pytest.raises(MediaPolicyError) as error: + resolve_local_media(str(path), MediaPolicy()) + errors.append(str(error.value)) + # The same message for existing and missing files: no filesystem oracle. + assert errors[0] == errors[1] + assert "server.media_roots" in errors[0] + + +def test_local_paths_inside_a_root_resolve(tmp_path): + media = tmp_path / "media" + media.mkdir() + clip = media / "clip.mp4" + clip.write_bytes(b"x") + assert resolve_local_media(str(clip), MediaPolicy(roots=(str(media), ))) == os.path.realpath(clip) + + +def test_symlinks_cannot_escape_a_root(tmp_path): + media = tmp_path / "media" + media.mkdir() + secret = tmp_path / "secret.txt" + secret.write_text("x") + (media / "link.mp4").symlink_to(secret) + with pytest.raises(MediaPolicyError): + resolve_local_media(str(media / "link.mp4"), MediaPolicy(roots=(str(media), ))) + + +def test_missing_files_inside_a_root_are_reported(tmp_path): + with pytest.raises(MediaPolicyError, match="does not exist"): + resolve_local_media(str(tmp_path / "missing.mp4"), MediaPolicy(roots=(str(tmp_path), ))) + + +@pytest.mark.parametrize("url", [ + "http://127.0.0.1/clip.mp4", + "http://localhost/clip.mp4", + "http://10.0.0.5/clip.mp4", + "http://169.254.169.254/latest/meta-data", + "http://[::1]/clip.mp4", +]) +def test_private_addresses_are_blocked_by_default(tmp_path, url): + transport = httpx.MockTransport(lambda request: httpx.Response(200, content=b"x")) + with pytest.raises(MediaPolicyError, match="private"): + asyncio.run(fetch_media(url, str(tmp_path / "out"), max_bytes=10, policy=MediaPolicy(), transport=transport)) + assert not (tmp_path / "out").exists() + + +def test_private_addresses_can_be_allowed(tmp_path): + transport = httpx.MockTransport(lambda request: httpx.Response(200, content=b"abc")) + target = tmp_path / "out" + asyncio.run( + fetch_media("http://127.0.0.1/clip.mp4", + str(target), + max_bytes=10, + policy=MediaPolicy(allow_private_urls=True), + transport=transport)) + assert target.read_bytes() == b"abc" + + +def test_redirects_to_private_addresses_are_blocked(tmp_path, monkeypatch): + monkeypatch.setattr("fastvideo.entrypoints.openai.media_policy._resolve_addresses", + lambda host, port: ["93.184.216.34"] if host == "media.example" else ["127.0.0.1"]) + + def handler(request): + if request.url.host == "media.example": + return httpx.Response(302, headers={"location": "http://internal.example/secret"}) + return httpx.Response(200, content=b"secret") + + with pytest.raises(MediaPolicyError, match="private"): + asyncio.run( + fetch_media("http://media.example/clip.mp4", + str(tmp_path / "out"), + max_bytes=10, + policy=MediaPolicy(), + transport=httpx.MockTransport(handler))) + assert not (tmp_path / "out").exists() + + +def test_streamed_bytes_are_capped_even_without_content_length(tmp_path, monkeypatch): + monkeypatch.setattr("fastvideo.entrypoints.openai.media_policy._resolve_addresses", + lambda host, port: ["93.184.216.34"]) + + async def body(): + for _ in range(4): + yield b"12345" + + transport = httpx.MockTransport(lambda request: httpx.Response(200, content=body())) + with pytest.raises(MediaPolicyError, match="limit"): + asyncio.run( + fetch_media("http://media.example/clip.mp4", + str(tmp_path / "out"), + max_bytes=12, + policy=MediaPolicy(), + transport=transport)) + assert not (tmp_path / "out").exists() + + +def test_cancelled_downloads_remove_the_partial_file(tmp_path, monkeypatch): + monkeypatch.setattr("fastvideo.entrypoints.openai.media_policy._resolve_addresses", + lambda host, port: ["93.184.216.34"]) + async def scenario(): + first_chunk = asyncio.Event() + + async def body(): + yield b"partial" + first_chunk.set() + await asyncio.sleep(30) + yield b"never" + + transport = httpx.MockTransport(lambda request: httpx.Response(200, content=body())) + target = tmp_path / "out" + task = asyncio.create_task( + fetch_media("http://media.example/clip.mp4", + str(target), + max_bytes=1024, + policy=MediaPolicy(), + transport=transport)) + await asyncio.wait_for(first_chunk.wait(), 5) + await asyncio.sleep(0.05) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + return target + + assert not asyncio.run(scenario()).exists() + + +def test_only_http_urls_are_fetched(tmp_path): + with pytest.raises(MediaPolicyError, match="http"): + asyncio.run(fetch_media("file:///etc/hosts", str(tmp_path / "out"), max_bytes=10, policy=MediaPolicy())) diff --git a/fastvideo/tests/entrypoints/test_openai_serving_engine.py b/fastvideo/tests/entrypoints/test_openai_serving_engine.py index 1e412d6be..ace7dd80b 100644 --- a/fastvideo/tests/entrypoints/test_openai_serving_engine.py +++ b/fastvideo/tests/entrypoints/test_openai_serving_engine.py @@ -15,6 +15,7 @@ from fastapi.testclient import TestClient from fastvideo.api.parser import parse_config from fastvideo.api.schema import GenerationRequest +from fastvideo.entrypoints.openai.media_policy import MediaPolicy from fastvideo.entrypoints.openai.protocol import VideoGenerationRequest from fastvideo.entrypoints.openai.request_adapter import ( RequestAdaptationError, @@ -24,6 +25,7 @@ from fastvideo.entrypoints.openai.request_adapter import ( ) from fastvideo.entrypoints.openai.serving_engine import OpenAIServingEngine from fastvideo.entrypoints.openai.stores import VIDEO_STORE +from fastvideo.entrypoints.openai.video_api import _media_type_for_path class _BlockingGenerator: @@ -42,6 +44,27 @@ class _BlockingGenerator: self.shutdown_called = True +class _WaveGenerator: + + def __init__(self) -> None: + self.last_output: Path | None = None + + def generate(self, request): + output = Path(request.output.output_path) + self.last_output = output + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(b"RIFF\x00\x00\x00\x00WAVEfmt fastvideo-test") + return SimpleNamespace( + video_path=str(output), + generation_time=0.01, + peak_memory_mb=12.5, + logging_info=None, + ) + + def shutdown(self) -> None: + return None + + class _FileGenerator: def __init__(self) -> None: @@ -163,10 +186,88 @@ def test_request_adapter_owns_output_path(tmp_path: Path) -> None: assert adapted.output.output_path == str(tmp_path / "videos" / "video_gen_test.mp4") +def test_audio_request_maps_seconds_to_duration_and_wav_output(tmp_path: Path) -> None: + request = VideoGenerationRequest(prompt="rain on a metal roof", seconds=4, audio_start_in_s=0) + adapted = build_generation_request( + "audio_gen_test", + request, + _args("FastVideo/stable-audio-open-1.0-Diffusers", workload_type=SimpleNamespace(value="t2a")), + served_model_name="stable-audio", + output_dir=str(tmp_path), + ) + assert adapted.sampling.audio_start_in_s == 0.0 + assert adapted.sampling.audio_end_in_s == 4.0 + assert adapted.output.output_path == str(tmp_path / "audio" / "audio_gen_test.wav") + + +def test_audio_request_rejects_video_geometry(tmp_path: Path) -> None: + request = VideoGenerationRequest(prompt="rain", size="512x512") + with pytest.raises(RequestAdaptationError, match="does not accept size"): + build_generation_request( + "audio_gen_test", + request, + _args("FastVideo/stable-audio-open-1.0-Diffusers", workload_type=SimpleNamespace(value="t2a")), + served_model_name="stable-audio", + output_dir=str(tmp_path), + ) + + +def test_audio_seconds_override_operator_duration(tmp_path: Path) -> None: + default_request = parse_config( + GenerationRequest, + {"sampling": { + "audio_start_in_s": 1.0, + "audio_end_in_s": 10.0, + }}, + ) + request = VideoGenerationRequest(prompt="rain", seconds=4) + adapted = build_generation_request( + "audio_gen_test", + request, + _args("FastVideo/stable-audio-open-1.0-Diffusers", workload_type=SimpleNamespace(value="t2a")), + served_model_name="stable-audio", + output_dir=str(tmp_path), + default_request=default_request, + ) + assert adapted.sampling.audio_start_in_s == 1.0 + assert adapted.sampling.audio_end_in_s == 5.0 + + +def test_wav_media_type_is_detected_from_output_path() -> None: + assert _media_type_for_path("/tmp/request.wav") == "audio/wav" + assert _media_type_for_path("/tmp/request.MP4") == "video/mp4" + + +def test_missing_video_reference_fails_during_admission(tmp_path: Path) -> None: + request = VideoGenerationRequest(prompt="waves", video_reference={"video_url": str(tmp_path / "missing.mp4")}) + with pytest.raises(RequestAdaptationError, match="does not exist"): + asyncio.run(prepare_reference_media("request", request, str(tmp_path), policy=MediaPolicy(roots=(str(tmp_path), )))) + + +def test_local_media_paths_need_a_configured_root(tmp_path: Path) -> None: + clip = tmp_path / "clip.mp4" + clip.write_bytes(b"x") + request = VideoGenerationRequest(prompt="waves", video_reference={"video_url": str(clip)}) + with pytest.raises(RequestAdaptationError, match="server.media_roots"): + asyncio.run(prepare_reference_media("request", request, str(tmp_path), policy=MediaPolicy())) + + +def test_mmaudio_v2a_requires_video_reference(tmp_path: Path) -> None: + request = VideoGenerationRequest(prompt="waves") + with pytest.raises(RequestAdaptationError, match="requires video_reference"): + build_generation_request( + "audio_gen_test", + request, + _args("FastVideo/MMAudio-large-44k-v2-Diffusers", workload_type=SimpleNamespace(value="v2a")), + served_model_name="mmaudio", + output_dir=str(tmp_path), + ) + + def test_missing_reference_fails_during_admission(tmp_path: Path) -> None: request = VideoGenerationRequest(prompt="a fox", input_reference=str(tmp_path / "missing.png")) with pytest.raises(RequestAdaptationError, match="does not exist"): - asyncio.run(prepare_reference_media("request", request, str(tmp_path))) + asyncio.run(prepare_reference_media("request", request, str(tmp_path), policy=MediaPolicy(roots=(str(tmp_path), )))) def test_request_adapter_resolves_vllm_nested_params(tmp_path: Path) -> None: @@ -366,6 +467,61 @@ def test_unsupported_vllm_postprocessing_fails_at_admission(tmp_path: Path) -> N ) +def test_audio_jobs_publish_wav_media_type_and_seconds(tmp_path: Path) -> None: + from fastvideo.entrypoints.openai import state + from fastvideo.entrypoints.openai.video_api import router + + generator = _WaveGenerator() + engine = OpenAIServingEngine(generator) # type: ignore[arg-type,unused-ignore] + args = _args("FastVideo/stable-audio-open-1.0-Diffusers", workload_type=SimpleNamespace(value="t2a")) + state.set_state( + generator, # type: ignore[arg-type] + engine, + args, # type: ignore[arg-type] + str(tmp_path), + served_model_name="stable-audio", + ) + asyncio.run(VIDEO_STORE.clear()) + app = FastAPI() + app.include_router(router) + + try: + with TestClient(app) as client: + created = client.post( + "/v1/videos", + json={"model": "stable-audio", "prompt": "rain", "seconds": 3}, + ) + assert created.status_code == 200 + audio_id = created.json()["id"] + + for _ in range(50): + detail = client.get(f"/v1/videos/{audio_id}") + if detail.json()["status"] == "completed": + break + time.sleep(0.01) + body = detail.json() + assert body["status"] == "completed" + assert body["media_type"] == "audio/wav" + assert body["seconds"] == "3" + assert body["size"] is None + assert body["file_name"].endswith(".wav") + + content = client.get(f"/v1/videos/{audio_id}/content") + assert content.status_code == 200 + assert content.headers["content-type"] == "audio/wav" + assert content.content.startswith(b"RIFF") + + sync = client.post("/v1/videos/sync", json={"prompt": "rain", "seconds": 2}) + assert sync.status_code == 200 + assert sync.headers["content-type"] == "audio/wav" + assert sync.content.startswith(b"RIFF") + assert generator.last_output is not None + assert not generator.last_output.exists() + finally: + asyncio.run(VIDEO_STORE.clear()) + state.clear_state() + + def test_video_routes_cover_async_sync_list_content_and_delete(tmp_path: Path) -> None: from fastvideo.entrypoints.openai import state from fastvideo.entrypoints.openai.video_api import router diff --git a/fastvideo/tests/entrypoints/test_serving_family_baselines.py b/fastvideo/tests/entrypoints/test_serving_family_baselines.py new file mode 100644 index 000000000..bb7cf8b24 --- /dev/null +++ b/fastvideo/tests/entrypoints/test_serving_family_baselines.py @@ -0,0 +1,293 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Declaration checks for family serving baselines; no model weights are loaded.""" + +import asyncio +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from threading import Thread +from types import SimpleNamespace + +import pytest +import yaml + +from fastvideo.api.compat import request_to_sampling_param +from fastvideo.api.flux import FluxSamplingParam +from fastvideo.api.matrixgame2 import MatrixGame2SamplingParam +from fastvideo.api.parser import parse_config +from fastvideo.api.sampling_param import SamplingParam +from fastvideo.api.schema import GenerationRequest, ServeConfig +from fastvideo.entrypoints.openai.media_policy import MediaPolicy +from fastvideo.entrypoints.openai.protocol import VideoGenerationRequest +from fastvideo.entrypoints.openai.request_adapter import ( + RequestAdaptationError, + build_generation_request, + discard_materialized_media, + prepare_reference_media, +) +from fastvideo.pipelines.basic.hunyuan15.presets import HUNYUAN15_I2V_480P_DISTILLED + +ROOT = Path(__file__).resolve().parents[3] + +BASELINES = { + "openai_wan21_t2v_14b.yaml": "t2v", + "openai_wan22_i2v_a14b.yaml": "i2v", + "openai_hunyuan15_t2v_480p.yaml": "t2v", + "openai_kandinsky5_t2v.yaml": "t2v", + "openai_hunyuan15_i2v_480p.yaml": "i2v", + "openai_kandinsky6_ti2va.yaml": "t2v", + "openai_kandinsky6_ti2va_distilled.yaml": "t2v", + "openai_ltx2_distilled.yaml": "t2v", + "openai_flux1_dev.yaml": "t2i", + "openai_flux2_dev.yaml": "t2i", + "openai_flux2_klein.yaml": "t2i", + "openai_sd35.yaml": "t2i", + "openai_stable_audio.yaml": "t2a", + "openai_mmaudio.yaml": "v2a", + "openai_fasth3_omniref_rtx5090.yaml": "i2v", + "openai_fasth3_omniref_rtx_pro6000.yaml": "i2v", + "openai_fasth3_omniref_spark.yaml": "i2v", +} + + +def _load(name: str) -> ServeConfig: + path = ROOT / "examples" / "serving" / name + return parse_config(ServeConfig, yaml.safe_load(path.read_text())) + + +def _args(config: ServeConfig, workload: str) -> SimpleNamespace: + return SimpleNamespace( + model_path=config.generator.model_path, + lora_path=None, + lora_nickname="default", + lora_strength=1.0, + override_pipeline_cls_name=None, + workload_type=SimpleNamespace(value=workload), + ) + + +@pytest.mark.parametrize(("name", "workload"), BASELINES.items()) +def test_family_baseline_declares_workload_and_parses(name: str, workload: str) -> None: + config = _load(name) + assert config.generator.pipeline.workload_type == workload + assert config.server.served_model_name + + +@pytest.mark.parametrize("device", ["rtx5090", "rtx_pro6000", "spark"]) +def test_omniref_serving_matches_its_maintained_inference_recipe(device: str) -> None: + served = yaml.safe_load((ROOT / "examples" / "serving" / f"openai_fasth3_omniref_{device}.yaml").read_text()) + recipe = yaml.safe_load( + (ROOT / "examples" / "inference" / "basic" / f"basic_fasth3_omniref_{device}.yaml").read_text()) + assert served["generator"] == recipe["generator"] + assert served["generator"]["pipeline"]["components"]["override_pipeline_cls_name"] == ( + "MiniMaxH3Ref2VAModularPipeline") + for key in ("height", "width", "num_frames", "fps", "num_inference_steps", "guidance_scale"): + assert served["default_request"]["sampling"][key] == recipe["request"]["sampling"][key], key + + +def test_hunyuan_i2v_480p_keeps_the_registered_checkpoint_semantics(tmp_path: Path) -> None: + """The checkpoint is named 480p, but its registered preset renders 720x1280.""" + config = _load("openai_hunyuan15_i2v_480p.yaml") + defaults = HUNYUAN15_I2V_480P_DISTILLED.defaults + sampling = config.default_request.sampling + + assert config.generator.model_path.endswith("480p_i2v_step_distilled") + for field in ("height", "width", "num_frames", "fps", "num_inference_steps", "guidance_scale"): + assert getattr(sampling, field) == defaults[field] + assert config.default_request.negative_prompt == defaults["negative_prompt"] + + image = tmp_path / "first-frame.png" + image.write_bytes(b"not-decoded-at-admission") + request = VideoGenerationRequest(prompt="a red fox", image_reference={"image_url": str(image)}) + adapted = build_generation_request( + "i2v_test", + request, + _args(config, "i2v"), + served_model_name=config.server.served_model_name or "", + output_dir=str(tmp_path), + default_request=config.default_request, + ) + assert adapted.sampling.height == 720 + assert adapted.sampling.width == 1280 + assert adapted.inputs.image_path == str(image) + assert adapted.output.output_path.endswith(".mp4") + + +def test_stable_audio_seconds_override_produces_wav(tmp_path: Path) -> None: + config = _load("openai_stable_audio.yaml") + request = VideoGenerationRequest(prompt="rain on a metal roof", seconds=6) + adapted = build_generation_request( + "audio_test", + request, + _args(config, "t2a"), + served_model_name=config.server.served_model_name or "", + output_dir=str(tmp_path), + default_request=config.default_request, + ) + assert adapted.sampling.audio_start_in_s == 0.0 + assert adapted.sampling.audio_end_in_s == 6.0 + assert adapted.output.output_path == str(tmp_path / "audio" / "audio_test.wav") + + +def test_mmaudio_uses_operator_duration_and_video_reference(tmp_path: Path) -> None: + config = _load("openai_mmaudio.yaml") + video = tmp_path / "clip.mp4" + video.write_bytes(b"not-decoded-at-admission") + request = VideoGenerationRequest(prompt="waves", seconds=4, video_reference={"video_url": str(video)}) + adapted = build_generation_request( + "audio_test", + request, + _args(config, "v2a"), + served_model_name=config.server.served_model_name or "", + output_dir=str(tmp_path), + default_request=config.default_request, + ) + assert adapted.sampling.audio_end_in_s == 4.0 + assert adapted.inputs.video_path == str(video) + assert adapted.output.output_path == str(tmp_path / "audio" / "audio_test.wav") + + +def test_flux_request_uses_flux_sampling_defaults() -> None: + request = parse_config(GenerationRequest, {"prompt": "a red fox"}) + sampling = request_to_sampling_param(request, model_path="black-forest-labs/FLUX.1-dev") + assert isinstance(sampling, FluxSamplingParam) + assert sampling.num_inference_steps == 28 + assert sampling.guidance_scale == 3.5 + assert sampling.use_embedded_guidance is True + + +def test_matrixgame2_request_uses_its_registered_sampling_defaults() -> None: + # The registered sampling-param class now applies to every family that declares one, not only FLUX. + request = parse_config(GenerationRequest, {"prompt": "a road"}) + sampling = request_to_sampling_param(request, model_path="FastVideo/Matrix-Game-2.0-Base-Distilled-Diffusers") + assert isinstance(sampling, MatrixGame2SamplingParam) + assert (sampling.num_inference_steps, sampling.height, sampling.width, sampling.num_frames) == (3, 352, 640, 57) + + +def test_local_directory_without_model_index_uses_base_sampling_defaults(tmp_path: Path) -> None: + request = parse_config(GenerationRequest, {"prompt": "a road"}) + sampling = request_to_sampling_param(request, model_path=str(tmp_path)) + assert type(sampling) is SamplingParam + + +def test_flux_explicit_sampling_overrides_win_over_flux_defaults() -> None: + request = parse_config( + GenerationRequest, + { + "prompt": "a red fox", + "sampling": { + "num_inference_steps": 4, + "guidance_scale": 2.0, + "use_embedded_guidance": False, + }, + }, + ) + sampling = request_to_sampling_param(request, model_path="black-forest-labs/FLUX.1-dev") + assert sampling.num_inference_steps == 4 + assert sampling.guidance_scale == 2.0 + assert sampling.use_embedded_guidance is False + + +def _serve_clip(payload: bytes) -> tuple[ThreadingHTTPServer, str]: + class Handler(BaseHTTPRequestHandler): + + def do_GET(self) -> None: + self.send_response(200) + self.send_header("Content-Type", "video/mp4") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def log_message(self, *args) -> None: + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + Thread(target=server.serve_forever, daemon=True).start() + return server, f"http://127.0.0.1:{server.server_port}/clip.mp4" + + +def test_local_media_url_is_downloaded_and_tracked_for_cleanup(tmp_path: Path) -> None: + server, url = _serve_clip(b"local-clip") + try: + request = VideoGenerationRequest(prompt="waves", video_reference={"video_url": url}) + created = asyncio.run( + prepare_reference_media("request", request, str(tmp_path), policy=MediaPolicy(allow_private_urls=True))) + assert len(created) == 1 + assert Path(created[0]).read_bytes() == b"local-clip" + discard_materialized_media(created) + assert not Path(created[0]).exists() + finally: + server.shutdown() + server.server_close() + + +def test_server_media_reference_limit_is_a_validated_serve_field() -> None: + config = parse_config( + ServeConfig, + { + "generator": {"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers"}, + "server": {"media_reference_max_bytes": 4096}, + }, + ) + assert config.server.media_reference_max_bytes == 4096 + with pytest.raises(ValueError, match="must be positive"): + parse_config( + ServeConfig, + { + "generator": {"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers"}, + "server": {"media_reference_max_bytes": 0}, + }, + ) + + +def test_media_url_limit_is_configurable_and_removes_partial_file(tmp_path: Path) -> None: + server, url = _serve_clip(b"larger-than-four") + try: + request = VideoGenerationRequest(prompt="waves", video_reference={"video_url": url}) + with pytest.raises(RequestAdaptationError, match="4-byte limit"): + asyncio.run( + prepare_reference_media("request", + request, + str(tmp_path), + max_bytes=4, + policy=MediaPolicy(allow_private_urls=True))) + assert not list((tmp_path / "uploads").glob("*")) + finally: + server.shutdown() + server.server_close() + + +def test_media_policy_fields_are_validated_serve_fields() -> None: + config = parse_config( + ServeConfig, + { + "generator": {"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers"}, + "server": {"media_roots": ["/data/media"], "allow_private_media_urls": True}, + }, + ) + assert config.server.media_roots == ["/data/media"] + assert config.server.allow_private_media_urls is True + default = parse_config(ServeConfig, {"generator": {"model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers"}}) + assert default.server.media_roots == [] + assert default.server.allow_private_media_urls is False + + +def test_loopback_media_urls_are_rejected_by_default(tmp_path: Path) -> None: + server, url = _serve_clip(b"local-clip") + try: + request = VideoGenerationRequest(prompt="waves", video_reference={"video_url": url}) + with pytest.raises(RequestAdaptationError, match="private or local address"): + asyncio.run(prepare_reference_media("request", request, str(tmp_path), policy=MediaPolicy())) + assert not list((tmp_path / "uploads").glob("*")) + finally: + server.shutdown() + server.server_close() + + +def test_discard_materialized_media_removes_only_downloaded_files(tmp_path: Path) -> None: + downloaded = tmp_path / "request_video.mp4" + downloaded.write_bytes(b"temporary") + caller_owned = tmp_path / "caller.mp4" + caller_owned.write_bytes(b"keep") + discard_materialized_media([str(downloaded)]) + assert not downloaded.exists() + assert caller_owned.exists() diff --git a/fastvideo/tests/vaes/test_minimax_h3_nvfp4_decoder.py b/fastvideo/tests/vaes/test_minimax_h3_nvfp4_decoder.py index bcfa97e2e..10dd98e99 100644 --- a/fastvideo/tests/vaes/test_minimax_h3_nvfp4_decoder.py +++ b/fastvideo/tests/vaes/test_minimax_h3_nvfp4_decoder.py @@ -487,14 +487,15 @@ def _write_deploy_checkpoint(path, student_layers: int, base_layers: int, heads: return decoder -def _load_h3_vae(vae_dir, cpu: bool = True): +def _load_h3_vae(vae_dir, cpu: bool = True, decoder: str | None = None): from fastvideo.configs.models.vaes.minimax_h3_video import MiniMaxH3VideoVAEConfig from fastvideo.configs.pipelines import PipelineConfig from fastvideo.fastvideo_args import FastVideoArgs from fastvideo.models.loader.component_loader import VAELoader args = FastVideoArgs(model_path=str(vae_dir), - pipeline_config=PipelineConfig(vae_config=MiniMaxH3VideoVAEConfig(), vae_precision="fp32")) + pipeline_config=PipelineConfig(vae_config=MiniMaxH3VideoVAEConfig(), vae_precision="fp32"), + h3_vae_nvfp4_decoder=decoder) args.vae_cpu_offload = cpu # load on CPU return VAELoader().load(str(vae_dir), args) @@ -518,6 +519,20 @@ def test_loader_applies_exported_nvfp4_decoder(tmp_path): assert not any(parameter.requires_grad for parameter in vae.parameters()) +def test_loader_applies_nvfp4_decoder_from_config(tmp_path): + import fastvideo.envs as envs + + _write_vae_dir(tmp_path / "vae", decoder_layers=5) + _write_deploy_checkpoint(tmp_path / "d3.pt", student_layers=3, base_layers=5) + _write_deploy_checkpoint(tmp_path / "d4.pt", student_layers=4, base_layers=5) + vae = _load_h3_vae(tmp_path / "vae", decoder=str(tmp_path / "d3.pt")) + assert len(vae.decoder.transformer_blocks) == 3 + # The serve-config field wins over the environment variable. + with envs.FASTVIDEO_H3_VAE_NVFP4_DECODER.override(str(tmp_path / "d4.pt")): + vae = _load_h3_vae(tmp_path / "vae", decoder=str(tmp_path / "d3.pt")) + assert len(vae.decoder.transformer_blocks) == 3 + + @pytest.mark.parametrize("student_layers,base_layers,heads,match", [(6, 6, 2, "needs 6 decoder blocks"), (2, 2, 4, "does not fit")]) def test_loader_refuses_incompatible_nvfp4_decoder(tmp_path, student_layers, base_layers, heads, match): diff --git a/tests/local_tests/test_cookbook_native_contract.py b/tests/local_tests/test_cookbook_native_contract.py index 150bd2e0a..c01b26be4 100644 --- a/tests/local_tests/test_cookbook_native_contract.py +++ b/tests/local_tests/test_cookbook_native_contract.py @@ -17,6 +17,7 @@ import pytest from fastvideo.api.schema import GenerationRequest, ServeConfig from fastvideo.configs.pipelines.base import PipelineConfig from fastvideo.entrypoints.openai.mlx_server import MLXServeConfig +from fastvideo.fastvideo_args import FastVideoArgs ROOT = Path(__file__).resolve().parents[2] RUNTIME_CONFIGS = { @@ -25,6 +26,12 @@ RUNTIME_CONFIGS = { "fastvideo-mlx-rest": MLXServeConfig, } JSON_TYPES = {str: "string", int: "integer", float: "number", bool: "boolean", type(None): "null"} +# Individually verified experimental keys and the native field that receives each one. +EXPERIMENTAL_DECLARATIONS = { + "generator.pipeline.experimental.flow_shift": (PipelineConfig, "flow_shift"), + "generator.pipeline.experimental.video_decode_backend": (FastVideoArgs, "video_decode_backend"), + "generator.pipeline.experimental.h3_vae_nvfp4_decoder": (FastVideoArgs, "h3_vae_nvfp4_decoder"), +} def annotation_at_path(config_type: type, path: str) -> Any: @@ -49,10 +56,11 @@ def native_annotation(runtime: str, path: str) -> Any: # create_mlx_app normalizes this opaque mapping as GenerationRequest. assert annotation_at_path(config_type, "default_request") == dict[str, Any] return annotation_at_path(GenerationRequest, path.removeprefix("default_request.")) - if config_type is ServeConfig and path == "generator.pipeline.experimental.flow_shift": + if config_type is ServeConfig and path in EXPERIMENTAL_DECLARATIONS: # The compatibility adapter forwards experimental keys to pipeline args. assert annotation_at_path(config_type, "generator.pipeline.experimental") == dict[str, Any] - return annotation_at_path(PipelineConfig, "flow_shift") + native_type, field = EXPERIMENTAL_DECLARATIONS[path] + return annotation_at_path(native_type, field) return annotation_at_path(config_type, path)

    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⪙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 literal 0 HcmV?d00001 diff --git a/docs-site/tests/evidence/cookbook-hardware-catalog/after/narrow-lookup.png b/docs-site/tests/evidence/cookbook-hardware-catalog/after/narrow-lookup.png new file mode 100644 index 0000000000000000000000000000000000000000..b2b20ec0f58b2ba060aa7d54a4d37e3947f67527 GIT binary patch literal 111582 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