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 }, }; }