const OP_TYPES = ["F", "B", "VF", "VB", "GF", "GB"];
const state = {
schedulers: [],
durationSpecs: defaultDurationSpecs(),
limits: {
maxMicrobatches: null,
},
result: null,
chromeTrace: null,
perfettoTrace: null,
selectedOpId: null,
scale: 72,
showLabels: true,
compare: {
resultA: null,
resultB: null,
comparison: null,
scale: 72,
showLabels: true,
selected: null,
},
};
const colors = {
F: ["#7db7f0", "#2f78c4", "#174f96"],
B: ["#8bd2a2", "#32a463", "#197a43"],
VF: ["#f2c66d", "#e49c31", "#b86f18"],
VB: ["#f08d80", "#d95749", "#a9362f"],
GF: ["#c8a2f0", "#8f62cc", "#653b9d"],
GB: ["#ee9fc5", "#d65c93", "#aa356e"],
};
const els = {
tabSimulate: document.getElementById("tab-simulate"),
tabCompare: document.getElementById("tab-compare"),
simulateView: document.getElementById("simulate-view"),
compareView: document.getElementById("compare-view"),
form: document.getElementById("config-form"),
scheduler: document.getElementById("scheduler"),
ppSize: document.getElementById("pp-size"),
vppSize: document.getElementById("vpp-size"),
numMicrobatches: document.getElementById("num-microbatches"),
veForwardLimit: document.getElementById("ve-forward-limit"),
seed: document.getElementById("seed"),
durationFields: document.getElementById("duration-fields"),
summary: document.getElementById("summary-strip"),
timeline: document.getElementById("timeline"),
timelineWrap: document.getElementById("timeline-wrap"),
tooltip: document.getElementById("tooltip"),
details: document.getElementById("details-panel"),
status: document.getElementById("status"),
zoom: document.getElementById("zoom-slider"),
fit: document.getElementById("fit-button"),
showLabels: document.getElementById("show-labels"),
downloadChrome: document.getElementById("download-chrome"),
downloadPerfetto: document.getElementById("download-perfetto"),
compareForm: document.getElementById("compare-form"),
comparePpSize: document.getElementById("compare-pp-size"),
compareVppSize: document.getElementById("compare-vpp-size"),
compareNumMicrobatches: document.getElementById("compare-num-microbatches"),
compareVeForwardLimit: document.getElementById("compare-ve-forward-limit"),
compareSeed: document.getElementById("compare-seed"),
compareIncludeEncoder: document.getElementById("compare-include-encoder"),
compareIncludeGenerator: document.getElementById("compare-include-generator"),
compareSchedulerA: document.getElementById("compare-scheduler-a"),
compareSchedulerB: document.getElementById("compare-scheduler-b"),
compareDurationFields: document.getElementById("compare-duration-fields"),
compareSummary: document.getElementById("compare-summary-strip"),
compareTimelineA: document.getElementById("compare-timeline-a"),
compareTimelineB: document.getElementById("compare-timeline-b"),
compareTimelineAWrap: document.getElementById("compare-timeline-a-wrap"),
compareTimelineBWrap: document.getElementById("compare-timeline-b-wrap"),
compareTitleA: document.getElementById("compare-title-a"),
compareTitleB: document.getElementById("compare-title-b"),
compareDetails: document.getElementById("compare-details-panel"),
compareZoom: document.getElementById("compare-zoom-slider"),
compareFit: document.getElementById("compare-fit-button"),
compareShowLabels: document.getElementById("compare-show-labels"),
errorModal: document.getElementById("error-modal"),
errorTitle: document.getElementById("error-title"),
errorMessage: document.getElementById("error-message"),
errorClose: document.getElementById("error-close"),
errorOk: document.getElementById("error-ok"),
};
let gestureStartScale = null;
let compareGestureStartScale = null;
let syncingCompareScroll = false;
init();
async function init() {
renderDurationFields(els.durationFields, OP_TYPES);
if (els.compareDurationFields) {
renderDurationFields(els.compareDurationFields, OP_TYPES);
}
bindEvents();
await loadSchedulers();
await simulate();
}
function bindEvents() {
if (els.tabSimulate && els.tabCompare) {
els.tabSimulate.addEventListener("click", () => setActiveTab("simulate"));
els.tabCompare.addEventListener("click", () => setActiveTab("compare"));
}
els.form.addEventListener("submit", async (event) => {
event.preventDefault();
await simulate();
});
els.zoom.addEventListener("input", () => {
state.scale = Number(els.zoom.value);
renderTimeline();
});
els.showLabels.addEventListener("change", () => {
state.showLabels = els.showLabels.checked;
renderTimeline();
});
els.fit.addEventListener("click", () => fitTimeline());
els.downloadChrome.addEventListener("click", () => downloadJson("pp-simulation-chrome-trace.json", state.chromeTrace));
els.downloadPerfetto.addEventListener("click", () => downloadJson("pp-simulation-perfetto-trace.json", state.perfettoTrace));
els.errorClose.addEventListener("click", clearError);
els.errorOk.addEventListener("click", clearError);
els.errorModal.addEventListener("click", (event) => {
if (event.target === els.errorModal) clearError();
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && !els.errorModal.hidden) clearError();
});
els.scheduler.addEventListener("change", updateSchedulerControls);
els.timelineWrap.addEventListener("wheel", handleTimelineWheel, { passive: false });
els.timelineWrap.addEventListener("gesturestart", handleGestureStart, { passive: false });
els.timelineWrap.addEventListener("gesturechange", handleGestureChange, { passive: false });
if (!els.compareForm) return;
els.compareForm.addEventListener("submit", async (event) => {
event.preventDefault();
await compareSchedulers();
});
els.compareZoom.addEventListener("input", () => {
state.compare.scale = Number(els.compareZoom.value);
renderCompare();
});
els.compareShowLabels.addEventListener("change", () => {
state.compare.showLabels = els.compareShowLabels.checked;
renderCompare();
});
els.compareFit.addEventListener("click", () => fitCompareTimelines());
for (const input of [
els.compareIncludeEncoder,
els.compareIncludeGenerator,
].filter(Boolean)) {
input.addEventListener("change", updateCompareControls);
}
els.compareTimelineAWrap.addEventListener("wheel", handleCompareWheel, { passive: false });
els.compareTimelineBWrap.addEventListener("wheel", handleCompareWheel, { passive: false });
els.compareTimelineAWrap.addEventListener("scroll", () => syncCompareScroll(els.compareTimelineAWrap, els.compareTimelineBWrap));
els.compareTimelineBWrap.addEventListener("scroll", () => syncCompareScroll(els.compareTimelineBWrap, els.compareTimelineAWrap));
els.compareTimelineAWrap.addEventListener("gesturestart", handleCompareGestureStart, { passive: false });
els.compareTimelineBWrap.addEventListener("gesturestart", handleCompareGestureStart, { passive: false });
els.compareTimelineAWrap.addEventListener("gesturechange", handleCompareGestureChange, { passive: false });
els.compareTimelineBWrap.addEventListener("gesturechange", handleCompareGestureChange, { passive: false });
}
async function loadSchedulers() {
const response = await fetch("/api/schedulers");
const payload = await response.json();
state.schedulers = payload.schedulers || [];
state.durationSpecs = payload.duration_specs || defaultDurationSpecs();
state.limits.maxMicrobatches = payload.limits ? payload.limits.max_microbatches : null;
applyInputLimits();
populateSchedulerSelect(els.scheduler, state.schedulers);
els.scheduler.value = "UnifiedBigMacVPP";
renderDurationFields(els.durationFields, visibleOpTypes());
updateSchedulerControls();
if (els.compareForm) updateCompareControls();
}
function applyInputLimits() {
const maxMicrobatches = Number(state.limits.maxMicrobatches);
for (const input of [els.numMicrobatches, els.compareNumMicrobatches].filter(Boolean)) {
if (Number.isFinite(maxMicrobatches) && maxMicrobatches > 0) {
input.max = String(maxMicrobatches);
input.title = `Maximum ${maxMicrobatches} microbatches in this deployment`;
if (Number(input.value) > maxMicrobatches) {
input.value = String(maxMicrobatches);
}
} else {
input.removeAttribute("max");
input.removeAttribute("title");
}
}
}
function populateSchedulerSelect(select, schedulers) {
select.innerHTML = "";
for (const item of schedulers) {
const option = document.createElement("option");
option.value = item.name;
option.textContent = item.description || item.name;
select.appendChild(option);
}
}
function setActiveTab(tab) {
if (!els.simulateView || !els.compareView) return;
const isCompare = tab === "compare";
els.tabSimulate.classList.toggle("active", !isCompare);
els.tabCompare.classList.toggle("active", isCompare);
els.simulateView.classList.toggle("active", !isCompare);
els.compareView.classList.toggle("active", isCompare);
if (isCompare && !state.compare.resultA) {
renderCompareSummary();
}
}
function updateSchedulerControls() {
const option = state.schedulers.find((item) => item.name === els.scheduler.value);
const usesVpp = option ? option.uses_vpp : true;
const usesVeForwardLimit = option ? option.uses_ve_forward_limit : true;
els.vppSize.disabled = !usesVpp;
els.veForwardLimit.disabled = !usesVeForwardLimit;
renderDurationFields(els.durationFields, visibleOpTypes());
}
function updateCompareControls() {
if (!els.compareForm) return;
if (els.compareIncludeGenerator.checked) {
els.compareIncludeEncoder.checked = true;
}
els.compareVeForwardLimit.disabled = !els.compareIncludeEncoder.checked;
const compatible = compatibleSchedulers();
const previousA = els.compareSchedulerA.value;
const previousB = els.compareSchedulerB.value;
populateSchedulerSelect(els.compareSchedulerA, compatible);
populateSchedulerSelect(els.compareSchedulerB, compatible);
if (compatible.some((item) => item.name === previousA)) {
els.compareSchedulerA.value = previousA;
}
if (compatible.some((item) => item.name === previousB)) {
els.compareSchedulerB.value = previousB;
} else if (compatible.length > 1) {
els.compareSchedulerB.value = compatible[1].name;
}
renderDurationFields(els.compareDurationFields, compareVisibleOpTypes());
}
function compatibleSchedulers() {
const shape = compareShape();
return state.schedulers.filter((item) => {
return Boolean(item.include_encoder) === shape.include_encoder
&& Boolean(item.include_generator) === shape.include_generator;
});
}
function compareShape() {
return {
include_encoder: els.compareIncludeEncoder.checked,
include_generator: els.compareIncludeGenerator.checked,
};
}
function renderDurationFields(container, opTypes) {
container.innerHTML = "";
for (const opType of opTypes) {
const spec = state.durationSpecs[opType] || { mean: 1, variance: 0 };
const row = document.createElement("div");
row.className = "duration-grid";
row.innerHTML = `
${opType}
`;
container.appendChild(row);
}
}
async function simulate() {
setStatus("Simulating...");
clearError();
try {
const response = await fetch("/api/simulate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(readConfig()),
});
const payload = await readApiPayload(response);
if (!response.ok) {
throw new Error(payload.error || "Simulation failed");
}
state.result = payload.simulation;
state.chromeTrace = payload.chrome_trace;
state.perfettoTrace = payload.perfetto_trace;
state.selectedOpId = null;
renderSummary();
fitTimeline();
renderDetails(null);
setStatus("Ready");
} catch (error) {
showError("Simulation failed", error);
}
}
async function compareSchedulers() {
const compatible = compatibleSchedulers();
if (compatible.length < 2 && els.compareSchedulerA.value === els.compareSchedulerB.value) {
showError("Compare failed", new Error("Need two compatible schedulers"));
return;
}
setStatus("Comparing...");
clearError();
try {
const response = await fetch("/api/compare", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(readCompareConfig()),
});
const payload = await readApiPayload(response);
if (!response.ok) {
throw new Error(payload.error || "Compare failed");
}
state.compare.resultA = payload.result_a;
state.compare.resultB = payload.result_b;
state.compare.comparison = payload.comparison;
state.compare.selected = null;
renderCompareSummary();
fitCompareTimelines();
renderCompareDetails(null, null);
setStatus("Ready");
} catch (error) {
showError("Compare failed", error);
}
}
async function readApiPayload(response) {
const text = await response.text();
if (!text) return {};
try {
return JSON.parse(text);
} catch (error) {
return {
error: response.ok
? "Server returned an invalid JSON response"
: `Server returned HTTP ${response.status}: ${text.slice(0, 240)}`,
};
}
}
function readConfig() {
return {
scheduler: els.scheduler.value,
pp_size: Number(els.ppSize.value),
vpp_size: Number(els.vppSize.value),
num_microbatches: Number(els.numMicrobatches.value),
ve_forward_limit: Number(els.veForwardLimit.value),
seed: els.seed.value === "" ? null : Number(els.seed.value),
duration_specs: readDurationSpecs(els.durationFields),
};
}
function readCompareConfig() {
return {
scheduler_a: els.compareSchedulerA.value,
scheduler_b: els.compareSchedulerB.value,
seed: els.compareSeed.value === "" ? null : Number(els.compareSeed.value),
workload: {
pp_size: Number(els.comparePpSize.value),
vpp_size: Number(els.compareVppSize.value),
num_microbatches: Number(els.compareNumMicrobatches.value),
ve_forward_limit: Number(els.compareVeForwardLimit.value),
include_encoder: els.compareIncludeEncoder.checked,
include_generator: els.compareIncludeGenerator.checked,
},
duration_specs: readDurationSpecs(els.compareDurationFields),
};
}
function readDurationSpecs(container) {
const specs = JSON.parse(JSON.stringify(state.durationSpecs));
for (const opType of OP_TYPES) {
if (!specs[opType]) specs[opType] = { mean: 0, variance: 0 };
}
for (const input of container.querySelectorAll("input")) {
specs[input.dataset.op][input.dataset.field] = Number(input.value);
}
return specs;
}
function visibleOpTypes() {
const option = state.schedulers.find((item) => item.name === els.scheduler.value);
return option && option.op_types ? option.op_types : OP_TYPES;
}
function compareVisibleOpTypes() {
const opTypes = ["F", "B"];
if (els.compareIncludeEncoder.checked) opTypes.push("VF", "VB");
if (els.compareIncludeGenerator.checked) opTypes.push("GF", "GB");
return opTypes;
}
function renderSummary() {
const result = state.result;
if (!result) {
els.summary.innerHTML = "";
return;
}
els.summary.innerHTML = summaryMetrics(result).map(renderMetric).join("");
}
function renderCompareSummary() {
const comparison = state.compare.comparison;
if (!comparison) {
els.compareSummary.innerHTML = "";
return;
}
const delta = comparison.makespan_delta;
const speedup = comparison.speedup_a_over_b;
const metrics = [
["A Makespan", formatNumber(comparison.makespan_a)],
["B Makespan", formatNumber(comparison.makespan_b)],
["B - A", formatSigned(delta)],
["B/A", speedup == null ? "-" : `${speedup.toFixed(3)}x`],
];
els.compareSummary.innerHTML = metrics.map(renderMetric).join("");
}
function summaryMetrics(result) {
const summary = result.summary;
const utilValues = Object.values(summary.rank_utilization || {});
const avgUtil = utilValues.length ? utilValues.reduce((a, b) => a + b, 0) / utilValues.length : 0;
return [
["Makespan", formatNumber(summary.makespan)],
["Total Wait", formatNumber(summary.total_wait_time)],
["Avg Utilization", `${(avgUtil * 100).toFixed(1)}%`],
["Ops", String(result.metadata.op_count)],
];
}
function renderMetric([label, value]) {
return `
${escapeHtml(label)}
${escapeHtml(value)}
`;
}
function fitTimeline() {
if (!state.result) return;
const available = Math.max(260, els.timelineWrap.clientWidth - 150);
const makespan = Math.max(1, state.result.summary.makespan);
const scale = clamp(available / makespan, 20, 180);
setScale(scale);
}
function fitCompareTimelines() {
if (!state.compare.resultA || !state.compare.resultB) return;
const available = Math.max(260, Math.min(
els.compareTimelineAWrap.clientWidth,
els.compareTimelineBWrap.clientWidth,
) - 150);
const makespan = Math.max(
1,
state.compare.resultA.summary.makespan,
state.compare.resultB.summary.makespan,
);
setCompareScale(clamp(available / makespan, 20, 180));
}
function handleTimelineWheel(event) {
if (!state.result || !event.ctrlKey) return;
event.preventDefault();
const factor = Math.exp(-event.deltaY * 0.01);
zoomAroundClientX(factor, event.clientX);
}
function handleCompareWheel(event) {
if (!state.compare.resultA || !event.ctrlKey) return;
event.preventDefault();
const factor = Math.exp(-event.deltaY * 0.01);
zoomCompareAroundClientX(factor, event.clientX, event.currentTarget);
}
function handleGestureStart(event) {
if (!state.result) return;
event.preventDefault();
gestureStartScale = state.scale;
}
function handleGestureChange(event) {
if (!state.result || gestureStartScale == null) return;
event.preventDefault();
const targetScale = gestureStartScale * event.scale;
zoomAroundClientX(targetScale / state.scale, event.clientX);
}
function handleCompareGestureStart(event) {
if (!state.compare.resultA) return;
event.preventDefault();
compareGestureStartScale = state.compare.scale;
}
function handleCompareGestureChange(event) {
if (!state.compare.resultA || compareGestureStartScale == null) return;
event.preventDefault();
const targetScale = compareGestureStartScale * event.scale;
zoomCompareAroundClientX(targetScale / state.compare.scale, event.clientX, event.currentTarget);
}
function zoomAroundClientX(factor, clientX) {
const oldScale = state.scale;
const newScale = clamp(oldScale * factor, Number(els.zoom.min), Number(els.zoom.max));
if (Math.abs(newScale - oldScale) < 0.001) return;
const rect = els.timelineWrap.getBoundingClientRect();
const localX = clientX - rect.left + els.timelineWrap.scrollLeft;
const timeAtPointer = Math.max(0, (localX - 92) / oldScale);
setScale(newScale);
const newLocalX = 92 + timeAtPointer * newScale;
els.timelineWrap.scrollLeft = Math.max(0, newLocalX - (clientX - rect.left));
}
function zoomCompareAroundClientX(factor, clientX, wrap) {
const oldScale = state.compare.scale;
const newScale = clamp(oldScale * factor, Number(els.compareZoom.min), Number(els.compareZoom.max));
if (Math.abs(newScale - oldScale) < 0.001) return;
const rect = wrap.getBoundingClientRect();
const localX = clientX - rect.left + wrap.scrollLeft;
const timeAtPointer = Math.max(0, (localX - 92) / oldScale);
setCompareScale(newScale);
const newLocalX = 92 + timeAtPointer * newScale;
const scrollLeft = Math.max(0, newLocalX - (clientX - rect.left));
els.compareTimelineAWrap.scrollLeft = scrollLeft;
els.compareTimelineBWrap.scrollLeft = scrollLeft;
}
function setScale(scale) {
state.scale = clamp(scale, Number(els.zoom.min), Number(els.zoom.max));
els.zoom.value = String(Math.round(state.scale));
renderTimeline();
}
function setCompareScale(scale) {
state.compare.scale = clamp(scale, Number(els.compareZoom.min), Number(els.compareZoom.max));
els.compareZoom.value = String(Math.round(state.compare.scale));
renderCompare();
}
function syncCompareScroll(source, target) {
if (syncingCompareScroll) return;
syncingCompareScroll = true;
target.scrollLeft = source.scrollLeft;
requestAnimationFrame(() => {
syncingCompareScroll = false;
});
}
function renderTimeline() {
renderPipelineTimeline({
svg: els.timeline,
result: state.result,
scale: state.scale,
showLabels: state.showLabels,
maxTime: state.result ? state.result.summary.makespan : 1,
selectedOpId: state.selectedOpId,
side: "single",
onSelect: (op) => {
state.selectedOpId = op.id;
renderTimeline();
renderDetails(op);
},
});
}
function renderCompare() {
const resultA = state.compare.resultA;
const resultB = state.compare.resultB;
const maxTime = Math.max(
1,
resultA ? resultA.summary.makespan : 1,
resultB ? resultB.summary.makespan : 1,
);
els.compareTitleA.textContent = resultA ? resultA.metadata.scheduler : "Scheduler A";
els.compareTitleB.textContent = resultB ? resultB.metadata.scheduler : "Scheduler B";
renderPipelineTimeline({
svg: els.compareTimelineA,
result: resultA,
scale: state.compare.scale,
showLabels: state.compare.showLabels,
maxTime,
selectedOpId: state.compare.selected && state.compare.selected.side === "A"
? state.compare.selected.id
: null,
side: "A",
onSelect: (op) => {
state.compare.selected = { side: "A", id: op.id };
renderCompare();
renderCompareDetails(op, "A");
},
});
renderPipelineTimeline({
svg: els.compareTimelineB,
result: resultB,
scale: state.compare.scale,
showLabels: state.compare.showLabels,
maxTime,
selectedOpId: state.compare.selected && state.compare.selected.side === "B"
? state.compare.selected.id
: null,
side: "B",
onSelect: (op) => {
state.compare.selected = { side: "B", id: op.id };
renderCompare();
renderCompareDetails(op, "B");
},
});
}
function renderPipelineTimeline({
svg,
result,
scale,
showLabels,
maxTime,
selectedOpId,
side,
onSelect,
}) {
if (!result) {
svg.innerHTML = "";
return;
}
const opsByRank = groupByRank(result.ops);
const ranks = Object.keys(opsByRank).map(Number).sort((a, b) => a - b);
const left = 92;
const top = 34;
const laneHeight = 38;
const laneGap = 8;
const rightPad = 28;
const bottomPad = 24;
const width = Math.ceil(left + Math.max(1, maxTime) * scale + rightPad);
const height = Math.ceil(top + ranks.length * (laneHeight + laneGap) + bottomPad);
svg.setAttribute("width", width);
svg.setAttribute("height", height);
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
svg.innerHTML = "";
renderAxis(svg, left, top, width, height, maxTime, scale);
ranks.forEach((rank, rankIndex) => {
const y = top + rankIndex * (laneHeight + laneGap);
addRect(svg, "rank-band", 0, y, width, laneHeight);
addText(svg, "rank-label", 14, y + 24, rankLabel(result, rank));
renderRankOps(svg, opsByRank[rank], left, y, laneHeight, {
scale,
showLabels,
selectedOpId,
side,
onSelect,
});
});
}
function rankLabel(result, rank) {
const layout = result && result.metadata ? result.metadata.pipeline_layout : null;
if (!layout) return `Rank ${rank}`;
if (rank === layout.encoder_stage) return "Encoder";
if (rank === layout.generator_stage) return "Generator";
const llmStart = layout.llm_stage_offset;
const llmEnd = llmStart + layout.llm_pp_size - 1;
if (rank >= llmStart && rank <= llmEnd) return `LLM ${rank - llmStart}`;
return `Rank ${rank}`;
}
function renderAxis(svg, left, top, width, height, maxTime, scale) {
const tickCount = Math.min(12, Math.max(4, Math.ceil(maxTime / 5)));
for (let i = 0; i <= tickCount; i++) {
const time = (maxTime * i) / tickCount;
const x = left + time * scale;
addLine(svg, "grid-line", x, top - 18, x, height - 12);
addText(svg, "axis-text", x + 3, 18, formatNumber(time));
}
addLine(svg, "axis-line", left, top - 8, width - 18, top - 8);
}
function renderRankOps(svg, ops, left, y, laneHeight, options) {
let previousEnd = 0;
for (const op of ops) {
const wait = Math.max(0, op.start_time - previousEnd);
if (wait > 1e-9) {
addRect(svg, "wait-rect", left + previousEnd * options.scale, y + 5, wait * options.scale, laneHeight - 10);
}
const x = left + op.start_time * options.scale;
const width = Math.max(1.5, op.duration * options.scale);
const rect = addRect(svg, "op-rect", x, y + 5, width, laneHeight - 10);
rect.setAttribute("fill", opColor(op));
rect.dataset.opId = op.id;
rect.dataset.side = options.side;
rect.dataset.matchKey = matchKey(op);
if (op.id === options.selectedOpId) rect.classList.add("selected");
rect.addEventListener("mouseenter", (event) => {
showTooltip(event, op);
highlightMatch(matchKey(op), true);
});
rect.addEventListener("mousemove", (event) => moveTooltip(event));
rect.addEventListener("mouseleave", () => {
hideTooltip();
highlightMatch(matchKey(op), false);
});
rect.addEventListener("click", () => options.onSelect(op));
if (options.showLabels && width >= 28) {
const label = width >= 54 ? op.label : compactLabel(op);
const text = addText(svg, "op-label", x + width / 2, y + 25, label);
text.setAttribute("text-anchor", "middle");
}
previousEnd = op.end_time;
}
}
function renderDetails(op) {
if (!op) {
els.details.innerHTML = `No op selected
`;
return;
}
const depRows = Object.entries(op.dep_reasons || {}).map(([dep, reasons]) => {
return renderDependencyDetail(dep, reasons, state.result);
}).join("");
els.details.innerHTML = renderOpDetails(op, depRows);
}
function renderCompareDetails(op, side) {
if (!op) {
els.compareDetails.innerHTML = `No op selected
`;
return;
}
const result = side === "A" ? state.compare.resultA : state.compare.resultB;
const peer = findMatchedCompareOp(op, side);
const depRows = Object.entries(op.dep_reasons || {}).map(([dep, reasons]) => {
return renderDependencyDetail(dep, reasons, result);
}).join("");
const peerRow = peer
? `Matched op ${escapeHtml(peer.label)} · ${escapeHtml(peer.metadata)} · duration ${formatNumber(peer.duration)}
`
: "";
els.compareDetails.innerHTML = renderOpDetails(op, peerRow + depRows, `Scheduler ${side}`);
}
function renderOpDetails(op, depRows, prefix = null) {
return `
${detail("Source", prefix)}
${detail("Label", op.label)}
${detail("Type", op.op_type)}
${detail("Rank", op.rank)}
${detail("Microbatch", op.microbatch_id)}
${detail("Chunk", op.chunk_id)}
${detail("Wait", formatNumber(op.wait_time))}
${detail("Start", formatNumber(op.start_time))}
${detail("End", formatNumber(op.end_time))}
${detail("Duration", formatNumber(op.duration))}
${detail("Deps", op.deps.length)}
${depRows || "No dependencies"}
`;
}
function findMatchedCompareOp(op, side) {
const result = side === "A" ? state.compare.resultB : state.compare.resultA;
if (!result) return null;
const key = matchKey(op);
const peer = result.ops.find((candidate) => matchKey(candidate) === key);
if (!peer) return null;
return {
...peer,
metadata: side === "A" ? "Scheduler B" : "Scheduler A",
};
}
function renderDependencyDetail(depId, reasons, result) {
const depOp = opById(depId, result);
const title = depOp
? `${depOp.label} on Rank ${depOp.rank}`
: depId;
const timing = depOp
? `ends at ${formatNumber(depOp.end_time)}`
: "source op not found";
const reasonText = reasons.map(reasonExplanation).join("; ");
return `
${escapeHtml(title)} ${escapeHtml(timing)}
${escapeHtml(reasonText)}
${escapeHtml(depId)}
`;
}
function opById(opId, result = state.result) {
if (!result) return null;
return result.ops.find((op) => op.id === opId) || null;
}
function reasonExplanation(reason) {
const explanations = {
rank_order: "same-rank program order: this rank must finish the previous scheduled op first",
encoder_forward_ready: "LLM forward waits for the matching encoder forward output",
ve_forward_ready: "LLM forward waits for the matching VE forward output",
ve_forward_unit_ready: "LLM forward waits until the VE forward unit for this batch is ready",
llm_forward_from_previous_rank: "pipeline forward dependency: this rank consumes activation from the previous PP rank",
vpp_forward_from_previous_chunk: "VPP forward dependency: this chunk starts after the previous chunk reaches the last PP rank",
backward_consumes_forward_activation: "backward must wait for its matching forward activation on this rank",
llm_backward_from_next_rank: "pipeline backward dependency: gradient arrives from the next PP rank",
vpp_backward_from_next_chunk: "VPP backward dependency: previous chunk waits for backward from the next chunk",
llm_output_unit_ready: "generator forward waits for the LLM output unit",
generator_backward_consumes_forward: "generator backward consumes the matching generator forward activation on this rank",
generator_backward_ready: "LLM backward waits for the matching generator backward gradient",
generator_backward_unit_ready: "LLM backward waits for generator backward gradients",
llm_output_ready: "generator forward waits for the matching LLM output",
llm_input_gradient_unit_ready: "VE backward waits for LLM input gradients for this unit",
llm_input_gradient_ready: "encoder backward waits for the matching LLM input gradient",
};
return explanations[reason] || reason;
}
function detail(key, value) {
if (value == null) return "";
return `
${escapeHtml(key)}
${escapeHtml(value == null ? "-" : String(value))}
`;
}
function showTooltip(event, op) {
els.tooltip.innerHTML = `
${escapeHtml(op.label)}
rank ${op.rank} · ${escapeHtml(op.op_type)} · mb ${op.microbatch_id ?? "-"}
start ${formatNumber(op.start_time)} · end ${formatNumber(op.end_time)}
duration ${formatNumber(op.duration)} · wait ${formatNumber(op.wait_time)}
deps ${op.deps.length}
`;
els.tooltip.style.display = "block";
moveTooltip(event);
}
function moveTooltip(event) {
els.tooltip.style.left = `${event.clientX + 14}px`;
els.tooltip.style.top = `${event.clientY + 14}px`;
}
function hideTooltip() {
els.tooltip.style.display = "none";
}
function highlightMatch(key, enabled) {
for (const rect of document.querySelectorAll(".op-rect")) {
if (rect.dataset.matchKey === key) {
rect.classList.toggle("matched", enabled);
}
}
}
function groupByRank(ops) {
const grouped = {};
for (const op of ops) {
if (!grouped[op.rank]) grouped[op.rank] = [];
grouped[op.rank].push(op);
}
for (const rank of Object.keys(grouped)) {
grouped[rank].sort((a, b) => a.index - b.index);
}
return grouped;
}
function opColor(op) {
const palette = colors[op.op_type] || ["#cbd5e1", "#94a3b8", "#64748b"];
const chunk = op.chunk_id == null ? 0 : Number(op.chunk_id);
return palette[chunk % palette.length];
}
function compactLabel(op) {
if (op.microbatch_id == null) return op.op_type;
return `${op.op_type}${op.microbatch_id}`;
}
function matchKey(op) {
const allowCrossVpp = state.compare.comparison
&& state.compare.comparison.workload
&& state.compare.comparison.workload.allow_cross_vpp;
const chunk = allowCrossVpp && (op.op_type === "F" || op.op_type === "B")
? 0
: (op.chunk_id == null ? 0 : Number(op.chunk_id));
return `${op.rank}|${op.op_type}|${op.microbatch_id ?? "none"}|${chunk}`;
}
function addRect(svg, className, x, y, width, height) {
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("class", className);
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("width", Math.max(0, width));
rect.setAttribute("height", Math.max(0, height));
svg.appendChild(rect);
return rect;
}
function addLine(svg, className, x1, y1, x2, y2) {
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("class", className);
line.setAttribute("x1", x1);
line.setAttribute("y1", y1);
line.setAttribute("x2", x2);
line.setAttribute("y2", y2);
svg.appendChild(line);
return line;
}
function addText(svg, className, x, y, value) {
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("class", className);
text.setAttribute("x", x);
text.setAttribute("y", y);
text.textContent = value;
svg.appendChild(text);
return text;
}
function downloadJson(filename, payload) {
if (!payload) return;
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);
}
function setStatus(value) {
els.status.textContent = value;
}
function showError(title, error) {
const message = error && error.message ? error.message : String(error || "Unknown error");
setStatus("Error");
els.errorTitle.textContent = title;
els.errorMessage.textContent = message;
els.errorModal.hidden = false;
els.errorOk.focus();
}
function clearError() {
els.errorModal.hidden = true;
els.errorMessage.textContent = "";
}
function formatNumber(value) {
return Number(value).toFixed(2);
}
function formatSigned(value) {
const number = Number(value);
return `${number >= 0 ? "+" : ""}${formatNumber(number)}`;
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function defaultDurationSpecs() {
return {
F: { mean: 1.0, variance: 0.0 },
B: { mean: 2.0, variance: 0.0 },
VF: { mean: 0.8, variance: 0.01 },
VB: { mean: 0.9, variance: 0.01 },
GF: { mean: 0.6, variance: 0.01 },
GB: { mean: 0.7, variance: 0.01 },
};
}