[docs] Add model-family inference cookbook (#1787)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
This commit is contained in:
co-authored by
Aryan Kumar
parent
a159b63c67
commit
ccc9014430
+344
-44
@@ -1,57 +1,357 @@
|
||||
(() => {
|
||||
let recipesPromise;
|
||||
const patternCharacters = "0123456789ABCDEF";
|
||||
|
||||
const loadRecipes = (url) => {
|
||||
recipesPromise ||= fetch(url).then((response) => {
|
||||
const loadRecipes = (url) =>
|
||||
fetch(url).then((response) => {
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return response.json();
|
||||
});
|
||||
return recipesPromise;
|
||||
|
||||
const initPatterns = (root) => {
|
||||
root.querySelectorAll("[data-cookbook-pattern]").forEach((pattern, patternIndex) => {
|
||||
if (pattern.dataset.patternReady) return;
|
||||
pattern.dataset.patternReady = "true";
|
||||
let value = "";
|
||||
for (let index = 0; index < 1800; index += 1) {
|
||||
const characterIndex = (index * 7 + patternIndex * 11) % patternCharacters.length;
|
||||
value += patternCharacters.charAt(characterIndex);
|
||||
if (index % 4 === 3 && index % 32 !== 31) value += " ";
|
||||
if (index % 32 === 31) value += "\n";
|
||||
}
|
||||
pattern.textContent = value;
|
||||
});
|
||||
};
|
||||
|
||||
const groupIdFor = (recipe) => recipe.group || recipe.id;
|
||||
|
||||
const gpuCountLabel = (hardware) => {
|
||||
const count = hardware?.gpu_count;
|
||||
return `${count} GPU${count === 1 ? "" : "s"}`;
|
||||
};
|
||||
|
||||
const runtimeFor = (recipe) => {
|
||||
const platform = recipe.hardware?.platform || "cuda";
|
||||
if (platform === "mlx") {
|
||||
return {
|
||||
id: "mlx",
|
||||
label: "Apple Silicon · MLX",
|
||||
hint:
|
||||
recipe.hardware?.minimum_memory ||
|
||||
[recipe.hardware?.accelerator, recipe.hardware?.system_memory].filter(Boolean).join(" · ") ||
|
||||
"Memory not recorded",
|
||||
};
|
||||
}
|
||||
if (platform === "mps") {
|
||||
return {
|
||||
id: "mps",
|
||||
label: "Apple Silicon · MPS",
|
||||
hint: recipe.hardware?.minimum_memory || recipe.hardware?.system_memory || "Memory not recorded",
|
||||
};
|
||||
}
|
||||
const hardware = recipe.hardware || {};
|
||||
return {
|
||||
id: "cuda",
|
||||
label: "NVIDIA CUDA",
|
||||
hint: hardware.accelerator
|
||||
? `${hardware.accelerator} · ${gpuCountLabel(hardware)}`
|
||||
: `${gpuCountLabel(hardware)} configured · GPU model not recorded`,
|
||||
};
|
||||
};
|
||||
|
||||
const runtimeSummary = (recipe) => {
|
||||
const runtime = runtimeFor(recipe);
|
||||
const hardware = recipe.hardware || {};
|
||||
if (runtime.id === "mlx") {
|
||||
return [hardware.accelerator || "Apple Silicon", hardware.system_memory || hardware.minimum_memory, "MLX"]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
if (runtime.id === "mps") {
|
||||
return [hardware.accelerator || "Apple Silicon", hardware.system_memory || hardware.minimum_memory, "PyTorch MPS"]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
}
|
||||
if (hardware.accelerator) return [hardware.accelerator, gpuCountLabel(hardware)].join(" · ");
|
||||
return `NVIDIA CUDA · ${gpuCountLabel(hardware)} configured · GPU model and VRAM not recorded`;
|
||||
};
|
||||
|
||||
const renderHardwareEvidence = (container, badge, recipe) => {
|
||||
const hardware = recipe.hardware || {};
|
||||
const runtime = runtimeFor(recipe);
|
||||
const isValidated = hardware.evidence === "validated";
|
||||
container.classList.toggle("cookbook-hardware-state--verified", isValidated);
|
||||
container.classList.toggle("cookbook-hardware-state--source", !isValidated);
|
||||
badge.classList.toggle("cookbook-badge--verified", isValidated);
|
||||
badge.classList.toggle("cookbook-badge--configured", !isValidated);
|
||||
badge.textContent = isValidated ? "Recorded run" : "Source config";
|
||||
|
||||
const heading = document.createElement("strong");
|
||||
heading.textContent = isValidated ? "Recorded hardware" : "Source configuration";
|
||||
const details = document.createElement("span");
|
||||
|
||||
if (isValidated) {
|
||||
const recorded = [
|
||||
hardware.accelerator,
|
||||
runtime.id === "cuda" ? gpuCountLabel(hardware) : hardware.system_memory,
|
||||
].filter(Boolean);
|
||||
const statements = [`${recorded.join(" · ")}.`];
|
||||
if (hardware.minimum_memory) statements.push(`Documented minimum: ${hardware.minimum_memory}.`);
|
||||
if (hardware.peak_memory) statements.push(`Measured: ${hardware.peak_memory}.`);
|
||||
if (!hardware.minimum_memory) statements.push("This recorded device is not a minimum requirement.");
|
||||
details.textContent = ` ${statements.join(" ")}`;
|
||||
} else if (runtime.id === "cuda") {
|
||||
details.textContent = ` NVIDIA CUDA · ${gpuCountLabel(hardware)}. The source does not record the GPU model or VRAM.`;
|
||||
} else {
|
||||
details.textContent = ` ${runtime.label}. The source does not record a device or memory requirement.`;
|
||||
}
|
||||
|
||||
container.replaceChildren(heading, details);
|
||||
if (hardware.evidence_url) {
|
||||
const evidenceLink = document.createElement("a");
|
||||
evidenceLink.href = hardware.evidence_url;
|
||||
evidenceLink.textContent = "View run evidence";
|
||||
evidenceLink.setAttribute("aria-label", `View recorded hardware evidence for ${recipe.label}`);
|
||||
container.append(" ", evidenceLink);
|
||||
}
|
||||
};
|
||||
|
||||
const compactLifecycle = (root) => {
|
||||
const lifecycle = root.querySelector(".cookbook-lifecycle");
|
||||
if (!lifecycle || lifecycle.dataset.compact) return;
|
||||
lifecycle.dataset.compact = "true";
|
||||
const stages = [...lifecycle.querySelectorAll(".cookbook-lifecycle__stage")];
|
||||
const active = stages.find((stage) => stage.classList.contains("cookbook-lifecycle__stage--active"));
|
||||
const planned = stages
|
||||
.filter((stage) => stage !== active)
|
||||
.map((stage) => stage.childNodes[0]?.textContent?.trim())
|
||||
.filter(Boolean);
|
||||
const summary = document.createElement("span");
|
||||
summary.className = "cookbook-lifecycle__summary";
|
||||
summary.textContent = `Next: ${planned.join(", ")}`;
|
||||
lifecycle.replaceChildren(...(active ? [active] : []), summary);
|
||||
};
|
||||
|
||||
const initFamilyBuilder = async (root) => {
|
||||
const family = root.dataset.family;
|
||||
if (!family) return;
|
||||
|
||||
compactLifecycle(root);
|
||||
|
||||
const builderHeading = root.querySelector(".cookbook-builder__intro h2");
|
||||
const builderIntro = root.querySelector(".cookbook-builder__intro > p");
|
||||
if (builderHeading) builderHeading.textContent = "Pick a recipe and runtime";
|
||||
if (builderIntro) {
|
||||
builderIntro.textContent =
|
||||
"Start with the result you want, then choose one of the runtimes FastVideo actually maintains for it.";
|
||||
}
|
||||
|
||||
const selectionLabels = root.querySelectorAll(".cookbook-selection-row__label");
|
||||
if (selectionLabels[0]) {
|
||||
selectionLabels[0].querySelector("strong").textContent = "Recipe";
|
||||
selectionLabels[0].querySelector("span").textContent = "Task and checkpoint";
|
||||
}
|
||||
if (selectionLabels[1]) {
|
||||
selectionLabels[1].querySelector("strong").textContent = "Runtime";
|
||||
selectionLabels[1].querySelector("span").textContent = "Maintained paths only";
|
||||
}
|
||||
|
||||
const modelOptions = root.querySelector("[data-cookbook-model-options]");
|
||||
const hardwareOptions = root.querySelector("[data-cookbook-hardware-options]");
|
||||
const description = root.querySelector("[data-cookbook-description]");
|
||||
const hardwareNote = root.querySelector(".cookbook-hardware-note");
|
||||
const label = root.querySelector("[data-cookbook-label]");
|
||||
const model = root.querySelector("[data-cookbook-model]");
|
||||
const task = root.querySelector("[data-cookbook-task]");
|
||||
const hardwareValue = root.querySelector("[data-cookbook-gpus]");
|
||||
const artifact = root.querySelector("[data-cookbook-artifact]");
|
||||
const evidenceCell = root.querySelector("[data-cookbook-evidence]");
|
||||
const source = root.querySelector("[data-cookbook-source]");
|
||||
const modelLink = root.querySelector("[data-cookbook-model-link]");
|
||||
const command = root.querySelector("[data-cookbook-command]");
|
||||
const status = root.querySelector("[data-cookbook-status]");
|
||||
const hardwareState = root.querySelector("[data-cookbook-hardware-state]");
|
||||
const hardwareBadge = root.querySelector("[data-cookbook-hardware-badge]");
|
||||
const count = root.querySelector("[data-cookbook-count]");
|
||||
const result = root.querySelector(".cookbook-result");
|
||||
const commandBlock = root.querySelector(".cookbook-command");
|
||||
|
||||
modelOptions.setAttribute("aria-label", "Recipe");
|
||||
hardwareOptions.setAttribute("aria-label", "Runtime");
|
||||
|
||||
if (hardwareNote) {
|
||||
hardwareNote.textContent = "Exact device and memory details appear only when a recorded run supports them.";
|
||||
}
|
||||
const runtimeFactLabel = hardwareValue?.closest("div")?.querySelector("dt");
|
||||
if (runtimeFactLabel) runtimeFactLabel.textContent = "Hardware";
|
||||
|
||||
let recipes;
|
||||
try {
|
||||
({ recipes } = await loadRecipes(root.dataset.recipes));
|
||||
} catch (error) {
|
||||
if (status) status.textContent = "Recipes could not be loaded. Use the maintained examples link below.";
|
||||
console.error("Failed to load FastVideo cookbook recipes", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const familyRecipes = recipes.filter((recipe) => recipe.family === family);
|
||||
if (!familyRecipes.length) return;
|
||||
|
||||
const byId = new Map(familyRecipes.map((recipe) => [recipe.id, recipe]));
|
||||
const groups = new Map();
|
||||
familyRecipes.forEach((recipe) => {
|
||||
const groupId = groupIdFor(recipe);
|
||||
if (!groups.has(groupId)) groups.set(groupId, []);
|
||||
groups.get(groupId).push(recipe);
|
||||
});
|
||||
if (count) count.textContent = `${familyRecipes.length} maintained recipes`;
|
||||
|
||||
modelOptions.replaceChildren();
|
||||
groups.forEach((groupRecipes, groupId) => {
|
||||
const representative = groupRecipes[0];
|
||||
const option = document.createElement("button");
|
||||
option.type = "button";
|
||||
option.dataset.recipeGroup = groupId;
|
||||
option.setAttribute("aria-pressed", "false");
|
||||
const optionLabel = document.createElement("strong");
|
||||
optionLabel.textContent = representative.group_label || representative.label;
|
||||
const optionTask = document.createElement("span");
|
||||
optionTask.textContent = representative.group_task || representative.task;
|
||||
option.append(optionLabel, optionTask);
|
||||
modelOptions.append(option);
|
||||
});
|
||||
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const requestedRecipe = query.get("recipe");
|
||||
const defaultRecipeId = familyRecipes[0].id;
|
||||
let selectedRecipeId = requestedRecipe && byId.has(requestedRecipe) ? requestedRecipe : defaultRecipeId;
|
||||
let selectedGroupId = groupIdFor(byId.get(selectedRecipeId));
|
||||
|
||||
const renderRuntimeOptions = () => {
|
||||
const groupRecipes = groups.get(selectedGroupId) || [];
|
||||
hardwareOptions.replaceChildren();
|
||||
groupRecipes.forEach((recipe) => {
|
||||
const runtime = runtimeFor(recipe);
|
||||
const option = document.createElement("button");
|
||||
option.type = "button";
|
||||
option.dataset.recipeId = recipe.id;
|
||||
option.dataset.runtimeId = runtime.id;
|
||||
option.setAttribute("aria-pressed", "false");
|
||||
const optionLabel = document.createElement("strong");
|
||||
optionLabel.textContent = runtime.label;
|
||||
const optionHint = document.createElement("span");
|
||||
optionHint.textContent = runtime.hint;
|
||||
option.append(optionLabel, optionHint);
|
||||
hardwareOptions.append(option);
|
||||
});
|
||||
};
|
||||
|
||||
let notes = root.querySelector("[data-cookbook-notes]");
|
||||
if (!notes) {
|
||||
notes = document.createElement("aside");
|
||||
notes.className = "cookbook-recipe-notes";
|
||||
notes.dataset.cookbookNotes = "";
|
||||
notes.hidden = true;
|
||||
result.insertBefore(notes, commandBlock);
|
||||
}
|
||||
|
||||
const render = ({ groupChanged = false, historyMode = "replace" } = {}) => {
|
||||
if (!byId.has(selectedRecipeId)) selectedRecipeId = defaultRecipeId;
|
||||
let recipe = byId.get(selectedRecipeId);
|
||||
if (groupChanged || groupIdFor(recipe) !== selectedGroupId) {
|
||||
const currentRuntime = runtimeFor(recipe).id;
|
||||
const groupRecipes = groups.get(selectedGroupId) || [];
|
||||
recipe = groupRecipes.find((candidate) => runtimeFor(candidate).id === currentRuntime) || groupRecipes[0];
|
||||
selectedRecipeId = recipe.id;
|
||||
}
|
||||
|
||||
selectedGroupId = groupIdFor(recipe);
|
||||
renderRuntimeOptions();
|
||||
const runtime = runtimeFor(recipe);
|
||||
|
||||
modelOptions.querySelectorAll("button").forEach((option) => {
|
||||
const selected = option.dataset.recipeGroup === selectedGroupId;
|
||||
option.classList.toggle("cookbook-option--selected", selected);
|
||||
option.setAttribute("aria-pressed", String(selected));
|
||||
});
|
||||
hardwareOptions.querySelectorAll("button").forEach((option) => {
|
||||
const selected = option.dataset.recipeId === recipe.id;
|
||||
option.classList.toggle("cookbook-option--selected", selected);
|
||||
option.setAttribute("aria-pressed", String(selected));
|
||||
});
|
||||
|
||||
description.textContent = recipe.summary;
|
||||
label.textContent = recipe.label;
|
||||
model.textContent = recipe.model;
|
||||
task.textContent = recipe.task;
|
||||
hardwareValue.textContent = runtimeSummary(recipe);
|
||||
if (artifact) artifact.textContent = recipe.expected_artifact || "Not yet documented for this recipe.";
|
||||
if (evidenceCell) {
|
||||
evidenceCell.textContent = recipe.evidence || "Source-backed";
|
||||
evidenceCell.classList.toggle("cookbook-badge--verified", recipe.evidence === "Verified");
|
||||
evidenceCell.classList.toggle("cookbook-badge--source-backed", recipe.evidence !== "Verified");
|
||||
}
|
||||
source.href = `https://github.com/hao-ai-lab/FastVideo/blob/main/${recipe.source}`;
|
||||
modelLink.href = `https://huggingface.co/${recipe.model}`;
|
||||
command.textContent = recipe.command;
|
||||
|
||||
renderHardwareEvidence(hardwareState, hardwareBadge, recipe);
|
||||
|
||||
const limitations = recipe.limitations || [];
|
||||
notes.replaceChildren();
|
||||
notes.hidden = limitations.length === 0;
|
||||
if (limitations.length) {
|
||||
const notesHeading = document.createElement("strong");
|
||||
notesHeading.textContent = "Know before you run";
|
||||
const notesList = document.createElement("ul");
|
||||
limitations.forEach((item) => {
|
||||
const listItem = document.createElement("li");
|
||||
listItem.textContent = item;
|
||||
notesList.append(listItem);
|
||||
});
|
||||
notes.append(notesHeading, notesList);
|
||||
}
|
||||
|
||||
const nextQuery = new URLSearchParams(window.location.search);
|
||||
nextQuery.set("recipe", recipe.id);
|
||||
nextQuery.set("runtime", runtime.id);
|
||||
nextQuery.delete("gpus");
|
||||
const nextUrl = `${window.location.pathname}?${nextQuery.toString()}${window.location.hash}`;
|
||||
if (historyMode === "push") window.history.pushState({}, "", nextUrl);
|
||||
else if (historyMode === "replace") window.history.replaceState({}, "", nextUrl);
|
||||
status.textContent = `${recipe.label} selected for ${runtime.label}.`;
|
||||
};
|
||||
|
||||
modelOptions.addEventListener("click", (event) => {
|
||||
const option = event.target.closest("button[data-recipe-group]");
|
||||
if (!option) return;
|
||||
selectedGroupId = option.dataset.recipeGroup;
|
||||
render({ groupChanged: true, historyMode: "push" });
|
||||
});
|
||||
hardwareOptions.addEventListener("click", (event) => {
|
||||
const option = event.target.closest("button[data-recipe-id]");
|
||||
if (!option) return;
|
||||
selectedRecipeId = option.dataset.recipeId;
|
||||
selectedGroupId = groupIdFor(byId.get(selectedRecipeId));
|
||||
render({ historyMode: "push" });
|
||||
});
|
||||
|
||||
render();
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
const nextQuery = new URLSearchParams(window.location.search);
|
||||
const nextRecipe = nextQuery.get("recipe");
|
||||
selectedRecipeId = nextRecipe && byId.has(nextRecipe) ? nextRecipe : defaultRecipeId;
|
||||
selectedGroupId = groupIdFor(byId.get(selectedRecipeId));
|
||||
render({ historyMode: "none" });
|
||||
});
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
document.querySelectorAll("[data-cookbook]").forEach(async (root) => {
|
||||
initPatterns(document);
|
||||
document.querySelectorAll("[data-cookbook][data-family]").forEach((root) => {
|
||||
if (root.dataset.initialized) return;
|
||||
root.dataset.initialized = "true";
|
||||
|
||||
const select = root.querySelector("[data-cookbook-recipe]");
|
||||
const model = root.querySelector("[data-cookbook-model]");
|
||||
const source = root.querySelector("[data-cookbook-source]");
|
||||
const command = root.querySelector("[data-cookbook-command]");
|
||||
const status = root.querySelector("[data-cookbook-status]");
|
||||
|
||||
try {
|
||||
const { recipes } = await loadRecipes(root.dataset.recipes);
|
||||
const byId = new Map(recipes.map((recipe) => [recipe.id, recipe]));
|
||||
const groups = new Map();
|
||||
|
||||
select.replaceChildren();
|
||||
recipes.forEach((recipe) => {
|
||||
if (!groups.has(recipe.task)) {
|
||||
const group = document.createElement("optgroup");
|
||||
group.label = recipe.task;
|
||||
groups.set(recipe.task, group);
|
||||
select.append(group);
|
||||
}
|
||||
groups.get(recipe.task).append(new Option(recipe.label, recipe.id));
|
||||
});
|
||||
|
||||
const render = () => {
|
||||
const recipe = byId.get(select.value);
|
||||
model.textContent = recipe.model;
|
||||
source.textContent = recipe.source;
|
||||
source.href = `https://github.com/hao-ai-lab/FastVideo/blob/main/${recipe.source}`;
|
||||
command.textContent = recipe.command;
|
||||
status.textContent = `${recipe.label} selected.`;
|
||||
};
|
||||
|
||||
select.addEventListener("change", render);
|
||||
select.disabled = false;
|
||||
render();
|
||||
} catch (error) {
|
||||
status.textContent = "Recipes could not be loaded. Use the examples link below.";
|
||||
console.error("Failed to load FastVideo cookbook recipes", error);
|
||||
}
|
||||
initFamilyBuilder(root);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user