624161f52f
Remove all xbar/bridge rendering from cube detail view. Replace 8 HBM slices with single HBM_CTRL block. Add green dotted lines showing router-to-HBM connectivity. Update legend, event animation, and PE view NOC destinations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1992 lines
66 KiB
HTML
1992 lines
66 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>KernBench Topology Viewer</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f172a;
|
|
--surface: #1e293b;
|
|
--surface2: #334155;
|
|
--border: #475569;
|
|
--text: #e2e8f0;
|
|
--text-dim: #94a3b8;
|
|
--accent: #3b82f6;
|
|
--accent-dim: #1d4ed8;
|
|
--green: #22c55e;
|
|
--yellow: #eab308;
|
|
--red: #ef4444;
|
|
--orange: #f97316;
|
|
--cube-fill: #1e3a5f;
|
|
--cube-stroke: #3b82f6;
|
|
--hbm-fill: #1a2e1a;
|
|
--hbm-stroke: #22c55e;
|
|
--pe-fill: #2d1f3d;
|
|
--pe-stroke: #a855f7;
|
|
--io-fill: #3d2b1f;
|
|
--io-stroke: #f97316;
|
|
--router-fill: #1f2d3d;
|
|
--router-stroke: #06b6d4;
|
|
--link-color: #475569;
|
|
--link-active: #3b82f6;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ── Top Bar ── */
|
|
.topbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 8px 16px;
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
height: 48px;
|
|
}
|
|
|
|
.topbar h1 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.view-tabs {
|
|
display: flex;
|
|
gap: 2px;
|
|
background: var(--bg);
|
|
border-radius: 6px;
|
|
padding: 2px;
|
|
}
|
|
|
|
.view-tab {
|
|
padding: 4px 14px;
|
|
font-size: 12px;
|
|
font-family: inherit;
|
|
border: none;
|
|
background: none;
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.view-tab:hover { color: var(--text); background: var(--surface2); }
|
|
.view-tab.active { color: var(--text); background: var(--accent-dim); }
|
|
|
|
.breadcrumb {
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.breadcrumb span { cursor: pointer; }
|
|
.breadcrumb span:hover { color: var(--accent); }
|
|
.breadcrumb .sep { color: var(--border); cursor: default; }
|
|
|
|
.topbar-spacer { flex: 1; }
|
|
|
|
.sim-info {
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ── Main Content ── */
|
|
.main {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Canvas Area ── */
|
|
.canvas-area {
|
|
flex: 1;
|
|
position: relative;
|
|
overflow: hidden;
|
|
background: var(--bg);
|
|
}
|
|
|
|
.canvas-area svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* ── Detail Panel (right sidebar) ── */
|
|
.detail-panel {
|
|
width: 280px;
|
|
flex-shrink: 0;
|
|
background: var(--surface);
|
|
border-left: 1px solid var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.panel-section {
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.panel-section h3 {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-dim);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.panel-section .kv {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.panel-section .kv .key { color: var(--text-dim); }
|
|
.panel-section .kv .val { color: var(--text); font-weight: 500; }
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 11px;
|
|
margin-bottom: 4px;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.legend-swatch {
|
|
width: 14px;
|
|
height: 10px;
|
|
border-radius: 2px;
|
|
border: 1px solid;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Bottom Bar (Timeline) ── */
|
|
.bottombar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 8px 16px;
|
|
background: var(--surface);
|
|
border-top: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
height: 44px;
|
|
}
|
|
|
|
.play-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface2);
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.play-btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
|
|
.timeline-container {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.timeline-label {
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
white-space: nowrap;
|
|
min-width: 50px;
|
|
}
|
|
|
|
.timeline-slider {
|
|
flex: 1;
|
|
-webkit-appearance: none;
|
|
height: 4px;
|
|
background: var(--surface2);
|
|
border-radius: 2px;
|
|
outline: none;
|
|
}
|
|
|
|
.timeline-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: var(--accent);
|
|
cursor: pointer;
|
|
border: 2px solid var(--bg);
|
|
}
|
|
|
|
.speed-btn {
|
|
font-size: 11px;
|
|
font-family: inherit;
|
|
padding: 2px 8px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--surface2);
|
|
color: var(--text-dim);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.speed-btn:hover { color: var(--text); border-color: var(--accent); }
|
|
|
|
.workload-select {
|
|
font-size: 11px;
|
|
font-family: inherit;
|
|
padding: 4px 8px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
background: var(--surface2);
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
min-width: 180px;
|
|
}
|
|
|
|
.workload-select:hover { border-color: var(--accent); }
|
|
.workload-select:focus { outline: none; border-color: var(--accent); }
|
|
.workload-select option { background: var(--surface); color: var(--text); }
|
|
|
|
/* ── Tooltip ── */
|
|
.tooltip {
|
|
position: absolute;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
font-size: 11px;
|
|
pointer-events: none;
|
|
display: none;
|
|
z-index: 100;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
max-width: 240px;
|
|
}
|
|
|
|
.tooltip .tt-title {
|
|
font-weight: 600;
|
|
color: var(--accent);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.tooltip .tt-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.tooltip .tt-row .tt-val { color: var(--text); }
|
|
|
|
/* ── SVG component styles ── */
|
|
.node-group { cursor: pointer; }
|
|
.node-group:hover rect,
|
|
.node-group:hover polygon { filter: brightness(1.3); }
|
|
|
|
.link-line {
|
|
stroke-linecap: round;
|
|
transition: stroke 0.2s, stroke-width 0.2s;
|
|
}
|
|
|
|
.link-line:hover {
|
|
stroke: var(--link-active);
|
|
stroke-width: 3;
|
|
}
|
|
|
|
/* Utilization color classes */
|
|
.util-idle { stroke: var(--border); }
|
|
.util-low { stroke: var(--accent); }
|
|
.util-med { stroke: var(--green); }
|
|
.util-high { stroke: var(--yellow); }
|
|
.util-sat { stroke: var(--red); }
|
|
|
|
/* Hot path highlight */
|
|
.hot-path { stroke: var(--orange); stroke-width: 3; opacity: 1; }
|
|
|
|
/* Animated packet dot */
|
|
@keyframes packet-move {
|
|
0% { offset-distance: 0%; }
|
|
100% { offset-distance: 100%; }
|
|
}
|
|
|
|
.packet-dot {
|
|
fill: var(--yellow);
|
|
r: 4;
|
|
}
|
|
|
|
/* Active component glow */
|
|
.active-component rect {
|
|
filter: brightness(1.5) drop-shadow(0 0 6px currentColor);
|
|
}
|
|
|
|
/* Hot path transition */
|
|
.link-line {
|
|
transition: stroke 0.3s ease, stroke-width 0.2s ease;
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ── Top Bar ── -->
|
|
<div class="topbar">
|
|
<h1>KernBench</h1>
|
|
<div class="view-tabs">
|
|
<button class="view-tab active" data-view="sip">SIP</button>
|
|
<button class="view-tab" data-view="cube">CUBE</button>
|
|
<button class="view-tab" data-view="pe">PE</button>
|
|
</div>
|
|
<div class="breadcrumb" id="breadcrumb">
|
|
<span data-nav="sip">SIP 0</span>
|
|
</div>
|
|
<div class="topbar-spacer"></div>
|
|
<select class="workload-select" id="workloadSelect">
|
|
<option value="" disabled>Loading workloads...</option>
|
|
</select>
|
|
<div class="sim-info">topology.yaml | 2 SIPs | 32 Cubes</div>
|
|
</div>
|
|
|
|
<!-- ── Main ── -->
|
|
<div class="main">
|
|
|
|
<!-- Canvas -->
|
|
<div class="canvas-area" id="canvasArea">
|
|
<svg id="topoSvg" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
<div class="tooltip" id="tooltip"></div>
|
|
</div>
|
|
|
|
<!-- Detail Panel -->
|
|
<div class="detail-panel">
|
|
|
|
<div class="panel-section">
|
|
<h3>Selected</h3>
|
|
<div id="selInfo">
|
|
<div class="kv"><span class="key">Click a component</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section">
|
|
<h3>Legend</h3>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--cube-fill);border-color:var(--cube-stroke)"></div>
|
|
Cube
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--io-fill);border-color:var(--io-stroke)"></div>
|
|
IO Chiplet
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--hbm-fill);border-color:var(--hbm-stroke)"></div>
|
|
HBM
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--pe-fill);border-color:var(--pe-stroke)"></div>
|
|
PE
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--router-fill);border-color:var(--router-stroke)"></div>
|
|
Router Mesh
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section">
|
|
<h3>Link Utilization</h3>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--border);border-color:var(--border)"></div>
|
|
Idle (0%)
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--accent);border-color:var(--accent)"></div>
|
|
Low (<25%)
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--green);border-color:var(--green)"></div>
|
|
Medium (25-50%)
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--yellow);border-color:var(--yellow)"></div>
|
|
High (50-75%)
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-swatch" style="background:var(--red);border-color:var(--red)"></div>
|
|
Saturated (>75%)
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel-section">
|
|
<h3>Topology</h3>
|
|
<div class="kv"><span class="key">Cube mesh</span><span class="val">4 x 4</span></div>
|
|
<div class="kv"><span class="key">PEs / cube</span><span class="val">8</span></div>
|
|
<div class="kv"><span class="key">HBM / cube</span><span class="val">48 GB</span></div>
|
|
<div class="kv"><span class="key">UCIe BW</span><span class="val">512 GB/s</span></div>
|
|
<div class="kv"><span class="key">HBM BW</span><span class="val">1024 GB/s</span></div>
|
|
<div class="kv"><span class="key">HBM eff.</span><span class="val">0.8</span></div>
|
|
</div>
|
|
|
|
<div class="panel-section">
|
|
<h3>Simulation</h3>
|
|
<div class="kv"><span class="key">Events</span><span class="val" id="eventCount">--</span></div>
|
|
<div class="kv"><span class="key">Duration</span><span class="val" id="simDuration">-- ns</span></div>
|
|
<div class="kv"><span class="key">In-flight</span><span class="val" id="inflight">0</span></div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Bottom Bar (Timeline) ── -->
|
|
<div class="bottombar">
|
|
<button class="play-btn" id="playBtn" title="Play/Pause">▶</button>
|
|
<div class="timeline-container">
|
|
<span class="timeline-label" id="timeLabel">t = 0.0 ns</span>
|
|
<input type="range" class="timeline-slider" id="timeSlider" min="0" max="1000" value="0" step="0.1">
|
|
<span class="timeline-label" id="timeEnd">0.0 ns</span>
|
|
</div>
|
|
<button class="speed-btn" id="speedBtn" title="Playback speed">1x</button>
|
|
</div>
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════
|
|
// KernBench Topology Viewer — Layout Prototype
|
|
// ═══════════════════════════════════════════
|
|
|
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
|
|
// ── Topology constants ──
|
|
const MESH_W = 4, MESH_H = 4;
|
|
const PE_PER_CUBE = 8;
|
|
const HBM_SLICES = 8;
|
|
|
|
// ── Layout constants (SVG coordinates) ──
|
|
const PAD = 60;
|
|
const CUBE_W = 120, CUBE_H = 100;
|
|
const CUBE_GAP_X = 32, CUBE_GAP_Y = 32;
|
|
const IO_H = 44;
|
|
const IO_Y = PAD;
|
|
const GRID_Y = IO_Y + IO_H + 40;
|
|
|
|
const GRID_W = MESH_W * CUBE_W + (MESH_W - 1) * CUBE_GAP_X;
|
|
const GRID_X = PAD;
|
|
const IO_W = GRID_W;
|
|
const IO_X = PAD;
|
|
|
|
const SVG_W = PAD * 2 + GRID_W;
|
|
const SVG_H = GRID_Y + MESH_H * CUBE_H + (MESH_H - 1) * CUBE_GAP_Y + PAD;
|
|
|
|
// ── State ──
|
|
let currentView = "sip";
|
|
let selectedNode = null;
|
|
|
|
// ── SVG helpers ──
|
|
function svgEl(tag, attrs = {}) {
|
|
const el = document.createElementNS(SVG_NS, tag);
|
|
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
|
|
return el;
|
|
}
|
|
|
|
function cubePos(col, row) {
|
|
return {
|
|
x: GRID_X + col * (CUBE_W + CUBE_GAP_X),
|
|
y: GRID_Y + row * (CUBE_H + CUBE_GAP_Y)
|
|
};
|
|
}
|
|
|
|
// ── Draw SIP View ──
|
|
function drawSipView(svg) {
|
|
svg.innerHTML = "";
|
|
svg.setAttribute("viewBox", `0 0 ${SVG_W} ${SVG_H}`);
|
|
|
|
// Background
|
|
svg.appendChild(svgEl("rect", {
|
|
width: SVG_W, height: SVG_H, fill: "#0f172a"
|
|
}));
|
|
|
|
// Title
|
|
const title = svgEl("text", {
|
|
x: SVG_W / 2, y: 28,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "13",
|
|
"font-weight": "bold",
|
|
fill: "#94a3b8"
|
|
});
|
|
title.textContent = "SIP 0 — 4x4 Cube Mesh";
|
|
svg.appendChild(title);
|
|
|
|
// IO Chiplet
|
|
const ioGroup = svgEl("g", { class: "node-group", "data-id": "io0" });
|
|
ioGroup.appendChild(svgEl("rect", {
|
|
x: IO_X, y: IO_Y, width: IO_W, height: IO_H,
|
|
rx: 6, fill: "#3d2b1f", stroke: "#f97316", "stroke-width": 1.5
|
|
}));
|
|
|
|
// IO chiplet internal components
|
|
const ioLabels = [
|
|
{ name: "PCIE_EP", x: IO_X + IO_W * 0.15 },
|
|
{ name: "IO_CPU", x: IO_X + IO_W * 0.38 },
|
|
{ name: "IO_NOC", x: IO_X + IO_W * 0.62 },
|
|
{ name: "UCIe", x: IO_X + IO_W * 0.85 }
|
|
];
|
|
for (const lbl of ioLabels) {
|
|
const box = svgEl("rect", {
|
|
x: lbl.x - 32, y: IO_Y + 6, width: 64, height: 20,
|
|
rx: 3, fill: "#5a3d26", stroke: "#f9731666", "stroke-width": 1
|
|
});
|
|
ioGroup.appendChild(box);
|
|
const t = svgEl("text", {
|
|
x: lbl.x, y: IO_Y + 20,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "9",
|
|
fill: "#f97316"
|
|
});
|
|
t.textContent = lbl.name;
|
|
ioGroup.appendChild(t);
|
|
}
|
|
|
|
const ioLabel = svgEl("text", {
|
|
x: IO_X + IO_W / 2, y: IO_Y + IO_H - 4,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "9",
|
|
fill: "#f9731688"
|
|
});
|
|
ioLabel.textContent = "IO Chiplet (io0)";
|
|
ioGroup.appendChild(ioLabel);
|
|
svg.appendChild(ioGroup);
|
|
|
|
// IO → top row UCIe links
|
|
for (let c = 0; c < MESH_W; c++) {
|
|
const pos = cubePos(c, 0);
|
|
const cx = pos.x + CUBE_W / 2;
|
|
const ioBottom = IO_Y + IO_H;
|
|
const cubeTop = pos.y;
|
|
|
|
// Link line
|
|
const line = svgEl("line", {
|
|
x1: cx, y1: ioBottom, x2: cx, y2: cubeTop,
|
|
stroke: "#f9731666", "stroke-width": 1.5,
|
|
"stroke-dasharray": "4,3",
|
|
class: "link-line"
|
|
});
|
|
svg.appendChild(line);
|
|
|
|
// Label
|
|
const lt = svgEl("text", {
|
|
x: cx + 8, y: (ioBottom + cubeTop) / 2,
|
|
"font-family": "monospace",
|
|
"font-size": "7",
|
|
fill: "#f9731688"
|
|
});
|
|
lt.textContent = "512";
|
|
svg.appendChild(lt);
|
|
}
|
|
|
|
// UCIe mesh links (horizontal + vertical)
|
|
for (let r = 0; r < MESH_H; r++) {
|
|
for (let c = 0; c < MESH_W; c++) {
|
|
const pos = cubePos(c, r);
|
|
|
|
// Right link
|
|
if (c < MESH_W - 1) {
|
|
const npos = cubePos(c + 1, r);
|
|
const srcIdx = r * MESH_W + c, dstIdx = r * MESH_W + c + 1;
|
|
svg.appendChild(svgEl("line", {
|
|
x1: pos.x + CUBE_W, y1: pos.y + CUBE_H / 2,
|
|
x2: npos.x, y2: npos.y + CUBE_H / 2,
|
|
stroke: "#3b82f644", "stroke-width": 2,
|
|
class: "link-line",
|
|
"data-link": `cube${srcIdx}-cube${dstIdx}`
|
|
}));
|
|
// BW label
|
|
const lt = svgEl("text", {
|
|
x: (pos.x + CUBE_W + npos.x) / 2,
|
|
y: pos.y + CUBE_H / 2 - 4,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "7",
|
|
fill: "#3b82f666"
|
|
});
|
|
lt.textContent = "512";
|
|
svg.appendChild(lt);
|
|
}
|
|
|
|
// Down link
|
|
if (r < MESH_H - 1) {
|
|
const npos = cubePos(c, r + 1);
|
|
const srcIdx = r * MESH_W + c, dstIdx = (r + 1) * MESH_W + c;
|
|
svg.appendChild(svgEl("line", {
|
|
x1: pos.x + CUBE_W / 2, y1: pos.y + CUBE_H,
|
|
x2: npos.x + CUBE_W / 2, y2: npos.y,
|
|
stroke: "#3b82f644", "stroke-width": 2,
|
|
class: "link-line",
|
|
"data-link": `cube${srcIdx}-cube${dstIdx}`
|
|
}));
|
|
const lt = svgEl("text", {
|
|
x: pos.x + CUBE_W / 2 + 8,
|
|
y: (pos.y + CUBE_H + npos.y) / 2,
|
|
"font-family": "monospace",
|
|
"font-size": "7",
|
|
fill: "#3b82f666"
|
|
});
|
|
lt.textContent = "512";
|
|
svg.appendChild(lt);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cubes
|
|
for (let r = 0; r < MESH_H; r++) {
|
|
for (let c = 0; c < MESH_W; c++) {
|
|
const idx = r * MESH_W + c;
|
|
const pos = cubePos(c, r);
|
|
drawCubeNode(svg, pos.x, pos.y, idx);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawCubeNode(svg, x, y, idx) {
|
|
const g = svgEl("g", {
|
|
class: "node-group",
|
|
"data-id": `cube${idx}`,
|
|
"data-type": "cube"
|
|
});
|
|
|
|
// Outer rect
|
|
g.appendChild(svgEl("rect", {
|
|
x, y, width: CUBE_W, height: CUBE_H,
|
|
rx: 6, fill: "#1e3a5f", stroke: "#3b82f6", "stroke-width": 1.5
|
|
}));
|
|
|
|
// Cube label
|
|
const label = svgEl("text", {
|
|
x: x + CUBE_W / 2, y: y + 14,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "10",
|
|
"font-weight": "bold",
|
|
fill: "#3b82f6"
|
|
});
|
|
label.textContent = `C${idx}`;
|
|
g.appendChild(label);
|
|
|
|
// Mini PE indicators (4 corners)
|
|
const pePositions = [
|
|
{ cx: x + 14, cy: y + 28 }, // NW
|
|
{ cx: x + CUBE_W - 14, cy: y + 28 }, // NE
|
|
{ cx: x + 14, cy: y + CUBE_H - 28 }, // SW
|
|
{ cx: x + CUBE_W - 14, cy: y + CUBE_H - 28 } // SE
|
|
];
|
|
for (let i = 0; i < pePositions.length; i++) {
|
|
const pp = pePositions[i];
|
|
// 2 PEs per corner — draw as a small double block
|
|
g.appendChild(svgEl("rect", {
|
|
x: pp.cx - 8, y: pp.cy - 5, width: 16, height: 10,
|
|
rx: 2, fill: "#2d1f3d", stroke: "#a855f766", "stroke-width": 0.8
|
|
}));
|
|
// PE pair label
|
|
const pt = svgEl("text", {
|
|
x: pp.cx, y: pp.cy + 3,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "6",
|
|
fill: "#a855f7aa"
|
|
});
|
|
pt.textContent = `${i*2},${i*2+1}`;
|
|
g.appendChild(pt);
|
|
}
|
|
|
|
// Center block: router mesh
|
|
g.appendChild(svgEl("rect", {
|
|
x: x + 30, y: y + 30, width: CUBE_W - 60, height: CUBE_H - 56,
|
|
rx: 3, fill: "#1f2d3d", stroke: "#06b6d466", "stroke-width": 0.8
|
|
}));
|
|
const xt = svgEl("text", {
|
|
x: x + CUBE_W / 2, y: y + CUBE_H / 2 + 1,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "7",
|
|
fill: "#06b6d4aa"
|
|
});
|
|
xt.textContent = "Router Mesh";
|
|
g.appendChild(xt);
|
|
|
|
// HBM indicators (top and bottom)
|
|
const hbmY1 = y + CUBE_H - 16;
|
|
g.appendChild(svgEl("rect", {
|
|
x: x + 20, y: hbmY1, width: CUBE_W - 40, height: 10,
|
|
rx: 2, fill: "#1a2e1a", stroke: "#22c55e66", "stroke-width": 0.8
|
|
}));
|
|
const ht = svgEl("text", {
|
|
x: x + CUBE_W / 2, y: hbmY1 + 7,
|
|
"text-anchor": "middle",
|
|
"font-family": "monospace",
|
|
"font-size": "6",
|
|
fill: "#22c55eaa"
|
|
});
|
|
ht.textContent = "HBM 48GB";
|
|
g.appendChild(ht);
|
|
|
|
// UCIe port indicators (N, S, E, W) — small triangles
|
|
const ports = [
|
|
{ px: x + CUBE_W / 2, py: y, dir: "N" },
|
|
{ px: x + CUBE_W / 2, py: y + CUBE_H, dir: "S" },
|
|
{ px: x, py: y + CUBE_H / 2, dir: "W" },
|
|
{ px: x + CUBE_W, py: y + CUBE_H / 2, dir: "E" }
|
|
];
|
|
for (const p of ports) {
|
|
const sz = 4;
|
|
let points;
|
|
switch (p.dir) {
|
|
case "N": points = `${p.px},${p.py} ${p.px-sz},${p.py+sz} ${p.px+sz},${p.py+sz}`; break;
|
|
case "S": points = `${p.px},${p.py} ${p.px-sz},${p.py-sz} ${p.px+sz},${p.py-sz}`; break;
|
|
case "W": points = `${p.px},${p.py} ${p.px+sz},${p.py-sz} ${p.px+sz},${p.py+sz}`; break;
|
|
case "E": points = `${p.px},${p.py} ${p.px-sz},${p.py-sz} ${p.px-sz},${p.py+sz}`; break;
|
|
}
|
|
g.appendChild(svgEl("polygon", {
|
|
points, fill: "#3b82f6", opacity: "0.5"
|
|
}));
|
|
}
|
|
|
|
// Click to drill down
|
|
g.addEventListener("click", () => onCubeClick(idx));
|
|
svg.appendChild(g);
|
|
}
|
|
|
|
// ── Draw Cube Detail View (matches cube_mesh_view.svg architecture) ──
|
|
function drawCubeView(svg, cubeIdx) {
|
|
svg.innerHTML = "";
|
|
// Use EXACT reference SVG coordinates (viewBox 1050x980, grid origin 180,170)
|
|
const W = 1050, H = 980;
|
|
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
|
|
svg.appendChild(svgEl("rect", { width: W, height: H, fill: "#0f172a" }));
|
|
|
|
// Title
|
|
const title = svgEl("text", {
|
|
x: W / 2, y: 28, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "16", "font-weight": "bold", fill: "#94a3b8"
|
|
});
|
|
title.textContent = `CUBE ${cubeIdx} INTERNAL ARCHITECTURE`;
|
|
svg.appendChild(title);
|
|
const subtitle = svgEl("text", {
|
|
x: W / 2, y: 46, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "11", fill: "#64748b"
|
|
});
|
|
subtitle.textContent = "17.0 x 14.0 mm | 6x6 Router Mesh | 8 PEs (~5mm2) | HBM 9x5mm | UCIe N/S/E/W x4";
|
|
svg.appendChild(subtitle);
|
|
|
|
// Cube boundary
|
|
svg.appendChild(svgEl("rect", {
|
|
x: 50, y: 62, width: 950, height: 770,
|
|
rx: 10, fill: "none", stroke: "#334155", "stroke-width": 3
|
|
}));
|
|
|
|
// ── EXACT coordinates from reference SVG ──
|
|
// Grid origin matches translate(180, 170) in reference
|
|
const OX = 180, OY = 170;
|
|
const CS = 140; // column step (reference: 0,140,280,420,560,700)
|
|
const rowY = [0, 95, 215, 310, 425, 520]; // exact reference row positions
|
|
|
|
const hbmExclude = new Set(["2,2","2,3","3,2","3,3"]);
|
|
const peRouters = new Set(["0,0","0,1","1,4","1,5","4,0","4,1","5,4","5,5"]);
|
|
const ucieRouters = new Set(["0,5","4,5"]);
|
|
const specialRouters = { "2,0": { fill: "#f59e0b", stroke: "#d97706" }, "3,0": { fill: "#f59e0b", stroke: "#d97706" } };
|
|
|
|
function rXY(r, c) { return { x: OX + c * CS, y: OY + rowY[r] }; }
|
|
|
|
// ── Mesh links (horizontal) ──
|
|
// Reference uses x offsets ±30 from router center for horizontal, ±30 for vertical
|
|
const fullRows = [0, 1, 4, 5]; // rows with all 6 columns connected
|
|
const hbmRows = [2, 3]; // rows with only side connections
|
|
for (let r = 0; r < 6; r++) {
|
|
for (let c = 0; c < 5; c++) {
|
|
const k1 = `${r},${c}`, k2 = `${r},${c+1}`;
|
|
if (hbmExclude.has(k1) || hbmExclude.has(k2)) continue;
|
|
// Skip links across HBM gap for rows 2,3 (only c0-c1 and c4-c5 connected)
|
|
if (hbmRows.includes(r) && c >= 1 && c <= 3) continue;
|
|
const p1 = rXY(r, c), p2 = rXY(r, c + 1);
|
|
svg.appendChild(svgEl("line", {
|
|
x1: p1.x + 30, y1: p1.y, x2: p2.x - 30, y2: p2.y,
|
|
stroke: "#d1d5db55", "stroke-width": 1.5, class: "link-line",
|
|
"data-link": `r${r}c${c}-r${r}c${c+1}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
// ── Mesh links (vertical) ──
|
|
for (let c = 0; c < 6; c++) {
|
|
for (let r = 0; r < 5; r++) {
|
|
const k1 = `${r},${c}`, k2 = `${r+1},${c}`;
|
|
if (hbmExclude.has(k1) || hbmExclude.has(k2)) continue;
|
|
// Skip vertical links into/through HBM zone for cols 2,3
|
|
if ((c === 2 || c === 3) && r >= 0 && r <= 3) continue;
|
|
const p1 = rXY(r, c), p2 = rXY(r + 1, c);
|
|
svg.appendChild(svgEl("line", {
|
|
x1: p1.x, y1: p1.y + 30, x2: p2.x, y2: p2.y - 30,
|
|
stroke: "#d1d5db55", "stroke-width": 1.5, class: "link-line",
|
|
"data-link": `r${r}c${c}-r${r+1}c${c}`
|
|
}));
|
|
}
|
|
}
|
|
|
|
// ── Router nodes ──
|
|
for (let r = 0; r < 6; r++) {
|
|
for (let c = 0; c < 6; c++) {
|
|
if (hbmExclude.has(`${r},${c}`)) continue;
|
|
const p = rXY(r, c);
|
|
const key = `${r},${c}`;
|
|
let fill = "#e2e8f0", stroke = "#94a3b8", rad = 12, textFill = "#475569";
|
|
if (peRouters.has(key)) { fill = "#3b82f6"; stroke = "#1d4ed8"; rad = 16; textFill = "white"; }
|
|
else if (ucieRouters.has(key)) { fill = "#8b5cf6"; stroke = "#6d28d9"; rad = 16; textFill = "white"; }
|
|
else if (specialRouters[key]) { fill = specialRouters[key].fill; stroke = specialRouters[key].stroke; rad = 16; textFill = "white"; }
|
|
|
|
svg.appendChild(svgEl("circle", { cx: p.x, cy: p.y, r: rad, fill, stroke, "stroke-width": 2 }));
|
|
const t = svgEl("text", {
|
|
x: p.x, y: p.y + 4, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": peRouters.has(key) || ucieRouters.has(key) || specialRouters[key] ? "7" : "6",
|
|
"font-weight": "bold", fill: textFill
|
|
});
|
|
t.textContent = `r${r}c${c}`;
|
|
svg.appendChild(t);
|
|
}
|
|
}
|
|
|
|
// ── HBM ZONE ──
|
|
const hbmZoneX = OX + 145, hbmZoneY = OY + 195, hbmZoneW = 410, hbmZoneH = 152;
|
|
svg.appendChild(svgEl("rect", {
|
|
x: hbmZoneX, y: hbmZoneY, width: hbmZoneW, height: hbmZoneH,
|
|
rx: 8, fill: "#052e16", stroke: "#047857", "stroke-width": 2
|
|
}));
|
|
const hzmLabel = svgEl("text", {
|
|
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 16, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "9", "font-weight": "bold", fill: "#047857"
|
|
});
|
|
hzmLabel.textContent = "HBM 9.0 x 5.0 mm | hbm_ctrl_v1";
|
|
svg.appendChild(hzmLabel);
|
|
|
|
// Single HBM_CTRL block (centered in HBM zone)
|
|
const hbmCtrlG = svgEl("g", { class: "node-group", "data-id": "hbm_ctrl" });
|
|
hbmCtrlG.appendChild(svgEl("rect", {
|
|
x: hbmZoneX + 40, y: hbmZoneY + 28, width: hbmZoneW - 80, height: 40,
|
|
rx: 6, fill: "#047857", stroke: "#065f46", "stroke-width": 1.5
|
|
}));
|
|
const hbmCtrlT = svgEl("text", {
|
|
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 53, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "10", "font-weight": "bold", fill: "white"
|
|
});
|
|
hbmCtrlT.textContent = "HBM_CTRL";
|
|
hbmCtrlG.appendChild(hbmCtrlT);
|
|
svg.appendChild(hbmCtrlG);
|
|
|
|
// Exclusion zone label
|
|
const hexLabel = svgEl("text", {
|
|
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 85, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "7", fill: "#ef4444aa"
|
|
});
|
|
hexLabel.textContent = "Router exclusion: r2c2, r2c3, r3c2, r3c3";
|
|
svg.appendChild(hexLabel);
|
|
|
|
// "All routers connect to HBM" annotation
|
|
const hbmAnnot = svgEl("text", {
|
|
x: hbmZoneX + hbmZoneW / 2, y: hbmZoneY + 100, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "6", fill: "#059669aa"
|
|
});
|
|
hbmAnnot.textContent = "All routers → HBM_CTRL (mesh-connected)";
|
|
svg.appendChild(hbmAnnot);
|
|
|
|
// ── HBM connectivity indicators (thin green dotted lines from edge routers to HBM zone) ──
|
|
// Draw thin green dotted lines from routers adjacent to HBM zone down/up to HBM
|
|
const hbmConnRouters = [
|
|
{ r: 1, c: 2 }, { r: 1, c: 3 }, // top edge of HBM zone
|
|
{ r: 4, c: 2 }, { r: 4, c: 3 }, // bottom edge of HBM zone
|
|
{ r: 2, c: 1 }, { r: 3, c: 1 }, // left edge of HBM zone
|
|
{ r: 2, c: 4 }, { r: 3, c: 4 }, // right edge of HBM zone
|
|
];
|
|
for (const hr of hbmConnRouters) {
|
|
const rp = rXY(hr.r, hr.c);
|
|
// Draw line toward the HBM zone center
|
|
const hbmCenterX = hbmZoneX + hbmZoneW / 2;
|
|
const hbmCenterY = hbmZoneY + hbmZoneH / 2;
|
|
// Compute endpoint clipped to HBM zone edge
|
|
let ex = hbmCenterX, ey = hbmCenterY;
|
|
if (hr.r <= 1) { ey = hbmZoneY; ex = rp.x; } // top routers → top of HBM zone
|
|
else if (hr.r >= 4) { ey = hbmZoneY + hbmZoneH; ex = rp.x; } // bottom routers → bottom of HBM zone
|
|
else if (hr.c <= 1) { ex = hbmZoneX; ey = rp.y; } // left routers → left of HBM zone
|
|
else { ex = hbmZoneX + hbmZoneW; ey = rp.y; } // right routers → right of HBM zone
|
|
svg.appendChild(svgEl("line", {
|
|
x1: rp.x, y1: rp.y, x2: ex, y2: ey,
|
|
stroke: "#05966988", "stroke-width": 1, "stroke-dasharray": "3,3"
|
|
}));
|
|
}
|
|
|
|
// ── M_CPU (r2c0) and SRAM (r3c0) labels ──
|
|
const mcpuP = rXY(2, 0);
|
|
svg.appendChild(svgEl("rect", {
|
|
x: mcpuP.x - 42, y: mcpuP.y + 18, width: 84, height: 18,
|
|
rx: 4, fill: "#f59e0b", stroke: "#d97706", "stroke-width": 1.5
|
|
}));
|
|
let bt = svgEl("text", {
|
|
x: mcpuP.x, y: mcpuP.y + 31, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "white"
|
|
});
|
|
bt.textContent = "M_CPU";
|
|
svg.appendChild(bt);
|
|
|
|
const sramP = rXY(3, 0);
|
|
svg.appendChild(svgEl("rect", {
|
|
x: sramP.x - 42, y: sramP.y + 18, width: 84, height: 18,
|
|
rx: 4, fill: "#f59e0b", stroke: "#d97706", "stroke-width": 1.5
|
|
}));
|
|
bt = svgEl("text", {
|
|
x: sramP.x, y: sramP.y + 31, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "white"
|
|
});
|
|
bt.textContent = "SRAM";
|
|
svg.appendChild(bt);
|
|
|
|
// ── PEs at corners (matching reference exactly) ──
|
|
const PEW = 84, PEH = 40;
|
|
const peData = [
|
|
// PE0 NW → r0c0 (0mm, above)
|
|
{ idx: 0, corner: "NW", rx: 0, ry: 0, above: true, wire: "0mm" },
|
|
// PE1 NW → r0c1 (0mm, above)
|
|
{ idx: 1, corner: "NW", rx: 140, ry: 0, above: true, wire: "0mm" },
|
|
// PE2 NE → r1c4 (4mm wire, above mesh)
|
|
{ idx: 2, corner: "NE", rx: 560, ry: 95, above: true, wire: "4.0mm" },
|
|
// PE3 NE → r1c5 (4mm wire, above mesh)
|
|
{ idx: 3, corner: "NE", rx: 700, ry: 95, above: true, wire: "4.0mm" },
|
|
// PE4 SW → r4c0 (4mm wire, below mesh)
|
|
{ idx: 4, corner: "SW", rx: 0, ry: 425, above: false, wire: "4.0mm" },
|
|
// PE5 SW → r4c1 (4mm wire, below mesh)
|
|
{ idx: 5, corner: "SW", rx: 140, ry: 425, above: false, wire: "4.0mm" },
|
|
// PE6 SE → r5c4 (0mm, below)
|
|
{ idx: 6, corner: "SE", rx: 560, ry: 520, above: false, wire: "0mm" },
|
|
// PE7 SE → r5c5 (0mm, below)
|
|
{ idx: 7, corner: "SE", rx: 700, ry: 520, above: false, wire: "0mm" },
|
|
];
|
|
|
|
for (const pe of peData) {
|
|
const routerX = OX + pe.rx;
|
|
const routerY = OY + pe.ry;
|
|
// PE block positioned at y=-62 (above) or y=556 (below) in reference coords
|
|
const peX = routerX - PEW / 2;
|
|
const peY = pe.above ? OY - 62 : OY + 556;
|
|
|
|
// Wire from PE to router
|
|
if (pe.wire === "0mm") {
|
|
svg.appendChild(svgEl("line", {
|
|
x1: routerX, y1: pe.above ? peY + PEH : peY,
|
|
x2: routerX, y2: pe.above ? routerY - 16 : routerY + 16,
|
|
stroke: "#ef4444", "stroke-width": 2
|
|
}));
|
|
} else {
|
|
svg.appendChild(svgEl("line", {
|
|
x1: routerX, y1: pe.above ? peY + PEH : peY,
|
|
x2: routerX, y2: pe.above ? routerY - 16 : routerY + 16,
|
|
stroke: "#ef4444", "stroke-width": 2, "stroke-dasharray": "5,3"
|
|
}));
|
|
// Wire distance label
|
|
const midY = pe.above
|
|
? (peY + PEH + routerY - 16) / 2
|
|
: (routerY + 16 + peY) / 2;
|
|
const wt = svgEl("text", {
|
|
x: routerX + 12, y: midY + 3, "font-family": "monospace",
|
|
"font-size": "7", "font-weight": "bold", fill: "#dc2626"
|
|
});
|
|
wt.textContent = "4mm";
|
|
svg.appendChild(wt);
|
|
}
|
|
|
|
const peG = svgEl("g", { class: "node-group", "data-id": `pe${pe.idx}`, "data-type": "pe" });
|
|
peG.appendChild(svgEl("rect", {
|
|
x: peX, y: peY, width: PEW, height: PEH,
|
|
rx: 6, fill: "#7f1d1d", stroke: "#ef4444", "stroke-width": 1.5
|
|
}));
|
|
const pt = svgEl("text", {
|
|
x: routerX, y: peY + 17, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "9", "font-weight": "bold", fill: "#fecaca"
|
|
});
|
|
pt.textContent = `PE${pe.idx}`;
|
|
peG.appendChild(pt);
|
|
const ps = svgEl("text", {
|
|
x: routerX, y: peY + 30, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "7", fill: "#fecaca88"
|
|
});
|
|
ps.textContent = `${pe.corner} · ${pe.wire}`;
|
|
peG.appendChild(ps);
|
|
peG.addEventListener("click", (e) => { e.stopPropagation(); onPeClick(cubeIdx, pe.idx); });
|
|
svg.appendChild(peG);
|
|
}
|
|
|
|
// ── UCIe-N (horizontal, above mesh) ──
|
|
// Reference: rect at (210+OX, -108+OY) → but we use OX-relative coords inside the grid
|
|
const ucieNx = OX + 210, ucieNy = OY - 108, ucieNw = 280, ucieNh = 26;
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieNx, y: ucieNy, width: ucieNw, height: ucieNh,
|
|
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
|
|
}));
|
|
bt = svgEl("text", {
|
|
x: ucieNx + 30, y: ucieNy + 14,
|
|
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
|
|
});
|
|
bt.textContent = "UCIe-N";
|
|
svg.appendChild(bt);
|
|
// c0-c3 sub-rectangles
|
|
const ucieNCx = [OX + 310, OX + 348, OX + 396, OX + 440];
|
|
for (let ci = 0; ci < 4; ci++) {
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieNCx[ci], y: ucieNy + 4, width: 32, height: 16,
|
|
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
|
|
}));
|
|
const ct = svgEl("text", {
|
|
x: ucieNCx[ci] + 16, y: ucieNy + 15, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "6", fill: "white"
|
|
});
|
|
ct.textContent = `c${ci}`;
|
|
svg.appendChild(ct);
|
|
}
|
|
// N connection lines (90-degree paths)
|
|
const ucieNConns = [
|
|
{ cx: ucieNCx[0] + 16, routerX: OX, routerY: OY, vy: OY - 50 },
|
|
{ cx: ucieNCx[1] + 16, routerX: OX+140, routerY: OY, vy: OY - 44 },
|
|
{ cx: ucieNCx[2] + 16, routerX: OX+560, routerY: OY, vy: OY - 44 },
|
|
{ cx: ucieNCx[3] + 16, routerX: OX+700, routerY: OY, vy: OY - 50 },
|
|
];
|
|
for (const nc of ucieNConns) {
|
|
svg.appendChild(svgEl("path", {
|
|
d: `M ${nc.cx} ${ucieNy + ucieNh} V ${nc.vy} H ${nc.routerX} V ${nc.routerY - 16}`,
|
|
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
|
|
}));
|
|
}
|
|
|
|
// ── UCIe-S (horizontal, below mesh) ──
|
|
const ucieSx = OX + 210, ucieSy = OY + 636, ucieSw = 280, ucieSh = 26;
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieSx, y: ucieSy, width: ucieSw, height: ucieSh,
|
|
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
|
|
}));
|
|
bt = svgEl("text", {
|
|
x: ucieSx + 30, y: ucieSy + 14,
|
|
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
|
|
});
|
|
bt.textContent = "UCIe-S";
|
|
svg.appendChild(bt);
|
|
const ucieSCx = [OX + 310, OX + 348, OX + 396, OX + 440];
|
|
for (let ci = 0; ci < 4; ci++) {
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieSCx[ci], y: ucieSy + 4, width: 32, height: 16,
|
|
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
|
|
}));
|
|
const ct = svgEl("text", {
|
|
x: ucieSCx[ci] + 16, y: ucieSy + 15, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "6", fill: "white"
|
|
});
|
|
ct.textContent = `c${ci}`;
|
|
svg.appendChild(ct);
|
|
}
|
|
// S connection lines
|
|
const ucieSConns = [
|
|
{ cx: ucieSCx[0] + 16, routerX: OX, routerY: OY+520, vy: OY + 572 },
|
|
{ cx: ucieSCx[1] + 16, routerX: OX+140, routerY: OY+520, vy: OY + 566 },
|
|
{ cx: ucieSCx[2] + 16, routerX: OX+560, routerY: OY+520, vy: OY + 566 },
|
|
{ cx: ucieSCx[3] + 16, routerX: OX+700, routerY: OY+520, vy: OY + 572 },
|
|
];
|
|
for (const sc of ucieSConns) {
|
|
svg.appendChild(svgEl("path", {
|
|
d: `M ${sc.cx} ${ucieSy} V ${sc.vy} H ${sc.routerX} V ${sc.routerY + 16}`,
|
|
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
|
|
}));
|
|
}
|
|
|
|
// ── UCIe-E (vertical, right side) ──
|
|
const ucieEx = OX + 762, ucieEy = OY + 225, ucieEw = 52, ucieEh = 110;
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieEx, y: ucieEy, width: ucieEw, height: ucieEh,
|
|
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
|
|
}));
|
|
bt = svgEl("text", {
|
|
x: ucieEx + ucieEw / 2, y: ucieEy + 17, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
|
|
});
|
|
bt.textContent = "UCIe-E";
|
|
svg.appendChild(bt);
|
|
const ucieECy = [ucieEy + 25, ucieEy + 43, ucieEy + 61, ucieEy + 79];
|
|
for (let ci = 0; ci < 4; ci++) {
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieEx + 6, y: ucieECy[ci], width: 40, height: 14,
|
|
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
|
|
}));
|
|
const ct = svgEl("text", {
|
|
x: ucieEx + 26, y: ucieECy[ci] + 10, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "6", fill: "white"
|
|
});
|
|
ct.textContent = `c${ci}`;
|
|
svg.appendChild(ct);
|
|
}
|
|
// E connection lines (90-degree paths via vertical tracks)
|
|
const ucieEConns = [
|
|
{ cy: ucieECy[0] + 7, routerX: OX+700, routerY: OY, vx: OX + 742 },
|
|
{ cy: ucieECy[1] + 7, routerX: OX+700, routerY: OY+95, vx: OX + 730 },
|
|
{ cy: ucieECy[2] + 7, routerX: OX+700, routerY: OY+425,vx: OX + 730 },
|
|
{ cy: ucieECy[3] + 7, routerX: OX+700, routerY: OY+520,vx: OX + 742 },
|
|
];
|
|
for (const ec of ucieEConns) {
|
|
svg.appendChild(svgEl("path", {
|
|
d: `M ${ucieEx} ${ec.cy} H ${ec.vx} V ${ec.routerY} H ${ec.routerX + 16}`,
|
|
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
|
|
}));
|
|
}
|
|
|
|
// ── UCIe-W (vertical, left side) ──
|
|
const ucieWx = OX - 124, ucieWy = OY + 225, ucieWw = 52, ucieWh = 110;
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieWx, y: ucieWy, width: ucieWw, height: ucieWh,
|
|
rx: 5, fill: "#1e1b4b", stroke: "#8b5cf6", "stroke-width": 1.5
|
|
}));
|
|
bt = svgEl("text", {
|
|
x: ucieWx + ucieWw / 2, y: ucieWy + 17, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "8", "font-weight": "bold", fill: "#8b5cf6"
|
|
});
|
|
bt.textContent = "UCIe-W";
|
|
svg.appendChild(bt);
|
|
const ucieWCy = [ucieWy + 25, ucieWy + 43, ucieWy + 61, ucieWy + 79];
|
|
for (let ci = 0; ci < 4; ci++) {
|
|
svg.appendChild(svgEl("rect", {
|
|
x: ucieWx + 6, y: ucieWCy[ci], width: 40, height: 14,
|
|
rx: 2, fill: "#8b5cf6", stroke: "#6d28d9", "stroke-width": 1
|
|
}));
|
|
const ct = svgEl("text", {
|
|
x: ucieWx + 26, y: ucieWCy[ci] + 10, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "6", fill: "white"
|
|
});
|
|
ct.textContent = `c${ci}`;
|
|
svg.appendChild(ct);
|
|
}
|
|
// W connection lines (90-degree paths via vertical tracks)
|
|
const ucieWConns = [
|
|
{ cy: ucieWCy[0] + 7, routerX: OX, routerY: OY, vx: OX - 42 },
|
|
{ cy: ucieWCy[1] + 7, routerX: OX, routerY: OY+95, vx: OX - 30 },
|
|
{ cy: ucieWCy[2] + 7, routerX: OX, routerY: OY+425,vx: OX - 30 },
|
|
{ cy: ucieWCy[3] + 7, routerX: OX, routerY: OY+520,vx: OX - 42 },
|
|
];
|
|
for (const wc of ucieWConns) {
|
|
svg.appendChild(svgEl("path", {
|
|
d: `M ${ucieWx + ucieWw} ${wc.cy} H ${wc.vx} V ${wc.routerY} H ${wc.routerX - 16}`,
|
|
fill: "none", stroke: "#8b5cf6", "stroke-width": 1.2, "stroke-dasharray": "4,3"
|
|
}));
|
|
}
|
|
|
|
// ── Legend ──
|
|
const legY = H - 50;
|
|
const legItems = [
|
|
{ color: "#3b82f6", label: "PE Router" },
|
|
{ color: "#e2e8f0", label: "Relay", textColor: "#475569" },
|
|
{ color: "#8b5cf6", label: "UCIe Router" },
|
|
{ color: "#f59e0b", label: "M_CPU/SRAM" },
|
|
{ color: "#059669", label: "HBM Link", type: "line" },
|
|
{ color: "#047857", label: "HBM Ctrl", type: "rect" },
|
|
{ color: "#ef4444", label: "PE (~5mm2)", type: "rect" },
|
|
{ color: "#8b5cf6", label: "UCIe Port", type: "rect", rectFill: "#1e1b4b" },
|
|
];
|
|
let legX = 60;
|
|
for (const li of legItems) {
|
|
if (li.type === "line") {
|
|
svg.appendChild(svgEl("line", {
|
|
x1: legX, y1: legY, x2: legX + 25, y2: legY,
|
|
stroke: li.color, "stroke-width": 2.5, "stroke-dasharray": "8,4"
|
|
}));
|
|
legX += 30;
|
|
} else if (li.type === "rect") {
|
|
svg.appendChild(svgEl("rect", {
|
|
x: legX, y: legY - 6, width: 20, height: 12, rx: 3,
|
|
fill: li.rectFill || li.color, stroke: li.color, "stroke-width": 1
|
|
}));
|
|
legX += 25;
|
|
} else {
|
|
svg.appendChild(svgEl("circle", { cx: legX + 7, cy: legY, r: 7, fill: li.color, stroke: li.color === "#e2e8f0" ? "#94a3b8" : li.color }));
|
|
legX += 20;
|
|
}
|
|
const lt = svgEl("text", {
|
|
x: legX, y: legY + 4, "font-family": "monospace", "font-size": "8", fill: "#94a3b8"
|
|
});
|
|
lt.textContent = li.label;
|
|
svg.appendChild(lt);
|
|
legX += li.label.length * 5.5 + 16;
|
|
}
|
|
|
|
// Data path description
|
|
const dpT = svgEl("text", {
|
|
x: 60, y: legY + 24, "font-family": "monospace", "font-size": "7", fill: "#64748b"
|
|
});
|
|
dpT.textContent = "Data: PE_DMA → Router Mesh → HBM_CTRL | All traffic routed through 6x6 mesh";
|
|
svg.appendChild(dpT);
|
|
}
|
|
|
|
// ── Draw PE Detail View (ADR-0014 compliant, NOC on left side) ──
|
|
function drawPeView(svg, cubeIdx, peIdx) {
|
|
svg.innerHTML = "";
|
|
const W = 750, H = 580;
|
|
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
|
|
svg.appendChild(svgEl("rect", { width: W, height: H, fill: "#0f172a" }));
|
|
|
|
// Title
|
|
const title = svgEl("text", {
|
|
x: W / 2, y: 24, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "14", "font-weight": "bold", fill: "#94a3b8"
|
|
});
|
|
title.textContent = `Cube ${cubeIdx} / PE ${peIdx} — Internal Architecture`;
|
|
svg.appendChild(title);
|
|
|
|
// ── Layout constants ──
|
|
const nocX = 70; // NOC column center x
|
|
const nocW = 60; // NOC column width
|
|
const peLeft = 170; // PE boundary left
|
|
const peRight = 700; // PE boundary right
|
|
const peTop = 50;
|
|
const peBot = 500;
|
|
const peCX = (peLeft + peRight) / 2; // PE center x
|
|
|
|
// ── PE boundary ──
|
|
svg.appendChild(svgEl("rect", {
|
|
x: peLeft, y: peTop, width: peRight - peLeft, height: peBot - peTop,
|
|
rx: 8, fill: "none", stroke: "#ef444444", "stroke-width": 1.5, "stroke-dasharray": "6,4"
|
|
}));
|
|
const bndLabel = svgEl("text", {
|
|
x: peLeft + 8, y: peTop + 14, "font-family": "monospace", "font-size": "7", fill: "#ef444466"
|
|
});
|
|
bndLabel.textContent = `PE${peIdx} boundary`;
|
|
svg.appendChild(bndLabel);
|
|
|
|
// ── NOC column (left side, vertical bar) ──
|
|
const nocTop = 80, nocBot = 460;
|
|
svg.appendChild(svgEl("rect", {
|
|
x: nocX - nocW/2, y: nocTop, width: nocW, height: nocBot - nocTop,
|
|
rx: 8, fill: "#1e293b", stroke: "#3b82f6", "stroke-width": 2
|
|
}));
|
|
const nocTitle = svgEl("text", {
|
|
x: nocX, y: nocTop + 18, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "10", "font-weight": "bold", fill: "#3b82f6"
|
|
});
|
|
nocTitle.textContent = "NOC";
|
|
svg.appendChild(nocTitle);
|
|
const nocSub = svgEl("text", {
|
|
x: nocX, y: nocTop + 30, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "7", fill: "#3b82f688"
|
|
});
|
|
nocSub.textContent = "6x6 mesh";
|
|
svg.appendChild(nocSub);
|
|
|
|
// NOC destinations (inside NOC column)
|
|
const nocDests = [
|
|
{ label: "HBM", sub: "ctrl", y: nocTop + 50, fill: "#059669", bg: "#052e16" },
|
|
{ label: "SRAM", sub: "128x4", y: nocTop + 86, fill: "#f59e0b", bg: "#3d2b1f" },
|
|
{ label: "UCIe", sub: "inter", y: nocTop + 122, fill: "#8b5cf6", bg: "#1e1b4b" },
|
|
{ label: "M_CPU", sub: "cmd", y: nocTop + 158, fill: "#f59e0b", bg: "#3d2b1f" },
|
|
];
|
|
for (const nd of nocDests) {
|
|
svg.appendChild(svgEl("rect", {
|
|
x: nocX - 24, y: nd.y, width: 48, height: 28,
|
|
rx: 3, fill: nd.bg, stroke: nd.fill, "stroke-width": 1
|
|
}));
|
|
const nt = svgEl("text", {
|
|
x: nocX, y: nd.y + 12, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "7", "font-weight": "bold", fill: nd.fill
|
|
});
|
|
nt.textContent = nd.label;
|
|
svg.appendChild(nt);
|
|
const ns = svgEl("text", {
|
|
x: nocX, y: nd.y + 23, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "6", fill: nd.fill + "88"
|
|
});
|
|
ns.textContent = nd.sub;
|
|
svg.appendChild(ns);
|
|
}
|
|
|
|
// ── PE components (inside PE boundary) ──
|
|
const comps = [
|
|
{ id: "pe_cpu", label: "PE_CPU", x: peCX - 60, y: 80, w: 120, h: 44, fill: "#3d2b1f", stroke: "#f97316", sub: "overhead: 2ns", sub2: "kernel dispatch" },
|
|
{ id: "pe_sched", label: "PE_SCHEDULER", x: peCX - 75, y: 158, w: 150, h: 48, fill: "#3d2b1f", stroke: "#f97316", sub: "overhead: 1ns", sub2: "tile scheduling" },
|
|
{ id: "pe_dma", label: "PE_DMA", x: peLeft + 20, y: 250, w: 120, h: 58, fill: "#1f2d3d", stroke: "#06b6d4", sub: "RD:1 WR:1", sub2: "dual egress" },
|
|
{ id: "pe_gemm", label: "PE_GEMM", x: peCX - 60, y: 250, w: 120, h: 58, fill: "#2d1f3d", stroke: "#a855f7", sub: "8 TFLOPS F16", sub2: "shared accel_slot" },
|
|
{ id: "pe_math", label: "PE_MATH", x: peRight - 140, y: 250, w: 120, h: 58, fill: "#2d1f3d", stroke: "#a855f7", sub: "element-wise", sub2: "shared accel_slot" },
|
|
{ id: "pe_tcm", label: "PE_TCM", x: peCX - 100, y: 360, w: 200, h: 58, fill: "#052e16", stroke: "#22c55e", sub: "16 MB SRAM", sub2: "scheduler_reserved + allocatable" },
|
|
];
|
|
|
|
const compMap = {};
|
|
for (const c of comps) {
|
|
compMap[c.id] = { cx: c.x + c.w / 2, cy: c.y + c.h / 2, bottom: c.y + c.h, top: c.y, left: c.x, right: c.x + c.w, ...c };
|
|
}
|
|
|
|
// ── Internal connections (behind nodes) ──
|
|
const internalConns = [
|
|
{ from: "pe_cpu", to: "pe_sched", label: "0.5mm" },
|
|
{ from: "pe_sched", to: "pe_dma", label: "0.5mm" },
|
|
{ from: "pe_sched", to: "pe_gemm", label: "0.5mm" },
|
|
{ from: "pe_sched", to: "pe_math", label: "0.5mm" },
|
|
{ from: "pe_dma", to: "pe_tcm", label: "512 GB/s" },
|
|
{ from: "pe_gemm", to: "pe_tcm", label: "512 GB/s" },
|
|
{ from: "pe_math", to: "pe_tcm", label: "512 GB/s" },
|
|
];
|
|
|
|
for (const conn of internalConns) {
|
|
const from = compMap[conn.from];
|
|
const to = compMap[conn.to];
|
|
svg.appendChild(svgEl("line", {
|
|
x1: from.cx, y1: from.bottom, x2: to.cx, y2: to.top,
|
|
stroke: "#47556988", "stroke-width": 1.5, class: "link-line",
|
|
"data-link": `${conn.from}-${conn.to}`
|
|
}));
|
|
if (conn.label.includes("GB")) {
|
|
const midX = (from.cx + to.cx) / 2 + 8;
|
|
const midY = (from.bottom + to.top) / 2;
|
|
const lt = svgEl("text", {
|
|
x: midX, y: midY, "font-family": "monospace", "font-size": "7", fill: "#47569988"
|
|
});
|
|
lt.textContent = conn.label;
|
|
svg.appendChild(lt);
|
|
}
|
|
}
|
|
|
|
// ── Draw component blocks ──
|
|
for (const c of comps) {
|
|
const g = svgEl("g", { class: "node-group", "data-id": c.id });
|
|
g.appendChild(svgEl("rect", {
|
|
x: c.x, y: c.y, width: c.w, height: c.h,
|
|
rx: 6, fill: c.fill, stroke: c.stroke, "stroke-width": 1.5
|
|
}));
|
|
const t = svgEl("text", {
|
|
x: c.x + c.w / 2, y: c.y + 17, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "10", "font-weight": "bold", fill: c.stroke
|
|
});
|
|
t.textContent = c.label;
|
|
g.appendChild(t);
|
|
const sub = svgEl("text", {
|
|
x: c.x + c.w / 2, y: c.y + 30, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "7", fill: c.stroke + "88"
|
|
});
|
|
sub.textContent = c.sub;
|
|
g.appendChild(sub);
|
|
if (c.sub2) {
|
|
const sub2 = svgEl("text", {
|
|
x: c.x + c.w / 2, y: c.y + 42, "text-anchor": "middle",
|
|
"font-family": "monospace", "font-size": "6", fill: c.stroke + "55"
|
|
});
|
|
sub2.textContent = c.sub2;
|
|
g.appendChild(sub2);
|
|
}
|
|
svg.appendChild(g);
|
|
}
|
|
|
|
// ── NOC → PE_CPU: command path (horizontal, orange dashed) ──
|
|
const cpuComp = compMap["pe_cpu"];
|
|
const cmdY = cpuComp.cy;
|
|
svg.appendChild(svgEl("line", {
|
|
x1: nocX + nocW/2, y1: cmdY, x2: cpuComp.left, y2: cmdY,
|
|
stroke: "#f97316", "stroke-width": 2, "stroke-dasharray": "5,3",
|
|
"data-link": "noc-pe_cpu"
|
|
}));
|
|
// Arrow head
|
|
svg.appendChild(svgEl("path", {
|
|
d: `M ${cpuComp.left} ${cmdY} l -8 -4 v 8 z`,
|
|
fill: "#f97316"
|
|
}));
|
|
const cmdLabel = svgEl("text", {
|
|
x: (nocX + nocW/2 + cpuComp.left) / 2, y: cmdY - 6,
|
|
"text-anchor": "middle", "font-family": "monospace", "font-size": "7", fill: "#f97316aa"
|
|
});
|
|
cmdLabel.textContent = "cmd (M_CPU)";
|
|
svg.appendChild(cmdLabel);
|
|
|
|
// ── NOC → PE_DMA: data path (horizontal, cyan dashed) ──
|
|
const dmaComp = compMap["pe_dma"];
|
|
const dataY = dmaComp.cy;
|
|
svg.appendChild(svgEl("line", {
|
|
x1: nocX + nocW/2, y1: dataY, x2: dmaComp.left, y2: dataY,
|
|
stroke: "#06b6d4", "stroke-width": 2, "stroke-dasharray": "5,3",
|
|
"data-link": "noc-pe_dma"
|
|
}));
|
|
// Bidirectional arrows
|
|
svg.appendChild(svgEl("path", {
|
|
d: `M ${dmaComp.left} ${dataY} l -8 -4 v 8 z`,
|
|
fill: "#06b6d4"
|
|
}));
|
|
svg.appendChild(svgEl("path", {
|
|
d: `M ${nocX + nocW/2} ${dataY} l 8 -4 v 8 z`,
|
|
fill: "#06b6d4"
|
|
}));
|
|
const dataLabel = svgEl("text", {
|
|
x: (nocX + nocW/2 + dmaComp.left) / 2, y: dataY - 6,
|
|
"text-anchor": "middle", "font-family": "monospace", "font-size": "7", fill: "#06b6d4aa"
|
|
});
|
|
dataLabel.textContent = "data (256 GB/s)";
|
|
svg.appendChild(dataLabel);
|
|
|
|
// ── Legend at bottom ──
|
|
const legY = H - 28;
|
|
const legends = [
|
|
{ color: "#06b6d4", label: "Data path (PE_DMA ↔ NOC)" },
|
|
{ color: "#f97316", label: "Cmd path (M_CPU → NOC → PE_CPU)" },
|
|
{ color: "#a855f7", label: "Compute (shared accel_slot)" },
|
|
{ color: "#22c55e", label: "TCM (staging memory)" },
|
|
];
|
|
let lx = 60;
|
|
for (const lg of legends) {
|
|
svg.appendChild(svgEl("rect", {
|
|
x: lx, y: legY - 4, width: 12, height: 8, rx: 2, fill: lg.color
|
|
}));
|
|
const lt = svgEl("text", {
|
|
x: lx + 16, y: legY + 3, "font-family": "monospace", "font-size": "7", fill: "#94a3b8"
|
|
});
|
|
lt.textContent = lg.label;
|
|
svg.appendChild(lt);
|
|
lx += lg.label.length * 4.5 + 28;
|
|
}
|
|
}
|
|
|
|
// ── Navigation ──
|
|
function onCubeClick(cubeIdx) {
|
|
currentView = "cube";
|
|
updateViewTabs();
|
|
updateBreadcrumb(`SIP 0`, `Cube ${cubeIdx}`);
|
|
drawCubeView(document.getElementById("topoSvg"), cubeIdx);
|
|
updateSelection({ type: "cube", id: cubeIdx });
|
|
}
|
|
|
|
function onPeClick(cubeIdx, peIdx) {
|
|
currentView = "pe";
|
|
updateViewTabs();
|
|
updateBreadcrumb(`SIP 0`, `Cube ${cubeIdx}`, `PE ${peIdx}`);
|
|
drawPeView(document.getElementById("topoSvg"), cubeIdx, peIdx);
|
|
updateSelection({ type: "pe", id: peIdx, cube: cubeIdx });
|
|
}
|
|
|
|
function updateViewTabs() {
|
|
document.querySelectorAll(".view-tab").forEach(t => {
|
|
t.classList.toggle("active", t.dataset.view === currentView);
|
|
});
|
|
}
|
|
|
|
function updateBreadcrumb(...parts) {
|
|
const bc = document.getElementById("breadcrumb");
|
|
bc.innerHTML = "";
|
|
parts.forEach((p, i) => {
|
|
if (i > 0) {
|
|
const sep = document.createElement("span");
|
|
sep.className = "sep";
|
|
sep.textContent = ">";
|
|
bc.appendChild(sep);
|
|
}
|
|
const span = document.createElement("span");
|
|
span.textContent = p;
|
|
span.addEventListener("click", () => {
|
|
if (i === 0) {
|
|
currentView = "sip";
|
|
updateViewTabs();
|
|
updateBreadcrumb("SIP 0");
|
|
drawSipView(document.getElementById("topoSvg"));
|
|
updateSelection(null);
|
|
}
|
|
});
|
|
bc.appendChild(span);
|
|
});
|
|
}
|
|
|
|
function updateSelection(sel) {
|
|
const div = document.getElementById("selInfo");
|
|
if (!sel) {
|
|
div.innerHTML = '<div class="kv"><span class="key">Click a component</span></div>';
|
|
return;
|
|
}
|
|
let html = "";
|
|
if (sel.type === "cube") {
|
|
html = `
|
|
<div class="kv"><span class="key">Type</span><span class="val">Cube</span></div>
|
|
<div class="kv"><span class="key">ID</span><span class="val">C${sel.id}</span></div>
|
|
<div class="kv"><span class="key">PEs</span><span class="val">${PE_PER_CUBE}</span></div>
|
|
<div class="kv"><span class="key">HBM</span><span class="val">48 GB</span></div>
|
|
<div class="kv"><span class="key">HBM BW</span><span class="val">1024 GB/s</span></div>
|
|
<div class="kv"><span class="key">UCIe ports</span><span class="val">4 (N/S/E/W)</span></div>
|
|
`;
|
|
} else if (sel.type === "pe") {
|
|
html = `
|
|
<div class="kv"><span class="key">Type</span><span class="val">PE</span></div>
|
|
<div class="kv"><span class="key">ID</span><span class="val">PE${sel.id}</span></div>
|
|
<div class="kv"><span class="key">Cube</span><span class="val">C${sel.cube}</span></div>
|
|
<div class="kv"><span class="key">TCM</span><span class="val">16 MB</span></div>
|
|
<div class="kv"><span class="key">GEMM</span><span class="val">8 TFLOPS</span></div>
|
|
<div class="kv"><span class="key">DMA</span><span class="val">RD:1 WR:1</span></div>
|
|
`;
|
|
}
|
|
div.innerHTML = html;
|
|
}
|
|
|
|
// ── Tab click handlers ──
|
|
document.querySelectorAll(".view-tab").forEach(tab => {
|
|
tab.addEventListener("click", () => {
|
|
const view = tab.dataset.view;
|
|
if (view === "sip") {
|
|
currentView = "sip";
|
|
updateViewTabs();
|
|
updateBreadcrumb("SIP 0");
|
|
drawSipView(document.getElementById("topoSvg"));
|
|
updateSelection(null);
|
|
}
|
|
// cube and pe views require selecting a specific cube/pe
|
|
});
|
|
});
|
|
|
|
// ══════════════════════════════════════════════════════════
|
|
// Phase C: Replay Engine — Hot Path Visualization
|
|
// ══════════════════════════════════════════════════════════
|
|
|
|
const timeSlider = document.getElementById("timeSlider");
|
|
const timeLabel = document.getElementById("timeLabel");
|
|
const timeEnd = document.getElementById("timeEnd");
|
|
const playBtn = document.getElementById("playBtn");
|
|
const speedBtn = document.getElementById("speedBtn");
|
|
const eventCountEl = document.getElementById("eventCount");
|
|
const simDurationEl = document.getElementById("simDuration");
|
|
const inflightEl = document.getElementById("inflight");
|
|
|
|
// ── Replay state ──
|
|
let allEvents = [];
|
|
let durationNs = 0;
|
|
let currentTimeNs = 0;
|
|
let playing = false;
|
|
let speed = 1;
|
|
let lastFrameTs = 0;
|
|
let packetDots = []; // active animated dots on SVG
|
|
|
|
// ── Color ramp for utilization ──
|
|
const UTIL_COLORS = {
|
|
idle: "#475569",
|
|
low: "#3b82f6",
|
|
med: "#22c55e",
|
|
high: "#eab308",
|
|
sat: "#ef4444",
|
|
hot: "#f97316",
|
|
};
|
|
|
|
// ── Workload selector ──
|
|
const workloadSelect = document.getElementById("workloadSelect");
|
|
|
|
async function loadWorkloadList() {
|
|
try {
|
|
const resp = await fetch("/api/workloads");
|
|
const workloads = await resp.json();
|
|
workloadSelect.innerHTML = "";
|
|
|
|
// Group by category
|
|
const categories = {};
|
|
for (const w of workloads) {
|
|
const cat = w.category || "other";
|
|
if (!categories[cat]) categories[cat] = [];
|
|
categories[cat].push(w);
|
|
}
|
|
|
|
const catLabels = { probe: "Probe Cases", bench: "Benchmarks", other: "Other" };
|
|
for (const [cat, items] of Object.entries(categories)) {
|
|
const grp = document.createElement("optgroup");
|
|
grp.label = catLabels[cat] || cat;
|
|
for (const w of items) {
|
|
const opt = document.createElement("option");
|
|
opt.value = w.id;
|
|
opt.textContent = w.name;
|
|
opt.title = w.description;
|
|
grp.appendChild(opt);
|
|
}
|
|
workloadSelect.appendChild(grp);
|
|
}
|
|
|
|
// Load first workload
|
|
if (workloads.length > 0) {
|
|
workloadSelect.value = workloads[0].id;
|
|
await loadWorkloadEvents(workloads[0].id);
|
|
}
|
|
} catch (e) {
|
|
workloadSelect.innerHTML = '<option value="" disabled selected>Start server: kernbench web</option>';
|
|
console.warn("Could not load workloads:", e.message);
|
|
}
|
|
}
|
|
|
|
async function loadWorkloadEvents(workloadId) {
|
|
try {
|
|
const resp = await fetch(`/api/events/${encodeURIComponent(workloadId)}`);
|
|
allEvents = await resp.json();
|
|
durationNs = allEvents.length ? Math.max(...allEvents.map(e => e.t_ns)) : 0;
|
|
currentTimeNs = 0;
|
|
playing = false;
|
|
playBtn.innerHTML = "▶";
|
|
timeSlider.max = durationNs.toFixed(1);
|
|
timeSlider.value = 0;
|
|
timeLabel.textContent = "t = 0.0 ns";
|
|
timeEnd.textContent = `${durationNs.toFixed(1)} ns`;
|
|
eventCountEl.textContent = allEvents.length;
|
|
simDurationEl.textContent = `${durationNs.toFixed(1)} ns`;
|
|
inflightEl.textContent = "0";
|
|
clearHotPaths(document.getElementById("topoSvg"));
|
|
console.log(`Loaded workload "${workloadId}": ${allEvents.length} events, ${durationNs.toFixed(1)} ns`);
|
|
} catch (e) {
|
|
console.warn("Could not load workload events:", e.message);
|
|
allEvents = [];
|
|
durationNs = 0;
|
|
}
|
|
}
|
|
|
|
workloadSelect.addEventListener("change", () => {
|
|
loadWorkloadEvents(workloadSelect.value);
|
|
});
|
|
|
|
// ── Extract cube index from node_id ──
|
|
function extractCubeIdx(nodeId) {
|
|
const m = nodeId.match(/cube(\d+)/);
|
|
return m ? parseInt(m[1]) : -1;
|
|
}
|
|
|
|
// ── Get events active at time t ──
|
|
function getActiveEvents(t, windowNs = 20) {
|
|
// Events are "active" if they started within [t - windowNs, t]
|
|
return allEvents.filter(e => e.t_ns <= t && e.t_ns >= t - windowNs);
|
|
}
|
|
|
|
// ── Get hop events that cross between two cubes ──
|
|
function getActiveSipHops(t, windowNs = 30) {
|
|
return allEvents.filter(e =>
|
|
e.type === "hop" &&
|
|
e.t_ns <= t && e.t_ns >= t - windowNs &&
|
|
e.src && e.dst
|
|
);
|
|
}
|
|
|
|
// ── Map hop event to SIP link ID ──
|
|
function hopToSipLink(hop) {
|
|
const srcCube = extractCubeIdx(hop.src);
|
|
const dstCube = extractCubeIdx(hop.dst);
|
|
if (srcCube < 0 || dstCube < 0 || srcCube === dstCube) return null;
|
|
const lo = Math.min(srcCube, dstCube);
|
|
const hi = Math.max(srcCube, dstCube);
|
|
return `cube${lo}-cube${hi}`;
|
|
}
|
|
|
|
// ── Map hop event to CUBE view link ID (router mesh) ──
|
|
function hopToCubeLink(hop) {
|
|
// Try to extract router positions from node_ids like "sip0.cube5.noc.r4c0"
|
|
const srcMatch = hop.src.match(/r(\d+)c(\d+)/);
|
|
const dstMatch = hop.dst.match(/r(\d+)c(\d+)/);
|
|
if (srcMatch && dstMatch) {
|
|
const [sr, sc] = [parseInt(srcMatch[1]), parseInt(srcMatch[2])];
|
|
const [dr, dc] = [parseInt(dstMatch[1]), parseInt(dstMatch[2])];
|
|
const loR = Math.min(sr, dr), loC = Math.min(sc, dc);
|
|
const hiR = Math.max(sr, dr), hiC = Math.max(sc, dc);
|
|
if (sr === dr) return `r${sr}c${loC}-r${sr}c${hiC}`;
|
|
if (sc === dc) return `r${loR}c${sc}-r${hiR}c${sc}`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── Map hop/process to PE view link ID ──
|
|
function eventToPeLink(ev) {
|
|
if (ev.type === "hop") {
|
|
// DMA ↔ NOC
|
|
if (ev.src.includes("pe_dma") || ev.dst.includes("pe_dma")) return "noc-pe_dma";
|
|
// M_CPU → PE_CPU
|
|
if (ev.dst.includes("pe_cpu")) return "noc-pe_cpu";
|
|
}
|
|
if (ev.type === "process") {
|
|
const comp = ev.component || "";
|
|
if (comp.includes("pe_gemm")) return "pe_sched-pe_gemm";
|
|
if (comp.includes("pe_scheduler")) return "pe_cpu-pe_sched";
|
|
if (comp.includes("pe_dma")) return "pe_sched-pe_dma";
|
|
if (comp.includes("pe_cpu")) return "noc-pe_cpu";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── Utilization color from bytes/traffic ──
|
|
function utilColor(nbytes) {
|
|
if (nbytes <= 0) return UTIL_COLORS.idle;
|
|
if (nbytes < 1024) return UTIL_COLORS.low;
|
|
if (nbytes < 16384) return UTIL_COLORS.med;
|
|
if (nbytes < 65536) return UTIL_COLORS.high;
|
|
return UTIL_COLORS.sat;
|
|
}
|
|
|
|
// ── Clear all hot path visual effects ──
|
|
function clearHotPaths(svg) {
|
|
// Remove packet dots
|
|
svg.querySelectorAll(".packet-dot").forEach(d => d.remove());
|
|
packetDots = [];
|
|
// Reset link colors
|
|
svg.querySelectorAll(".link-line[data-link]").forEach(el => {
|
|
el.classList.remove("hot-path");
|
|
// Restore original stroke based on view
|
|
if (currentView === "sip") {
|
|
el.style.stroke = "";
|
|
el.style.strokeWidth = "";
|
|
} else if (currentView === "cube") {
|
|
el.style.stroke = "";
|
|
el.style.strokeWidth = "";
|
|
} else {
|
|
el.style.stroke = "";
|
|
el.style.strokeWidth = "";
|
|
}
|
|
});
|
|
// Reset component highlights
|
|
svg.querySelectorAll(".node-group[data-id]").forEach(g => {
|
|
g.classList.remove("active-component");
|
|
const rect = g.querySelector("rect");
|
|
if (rect) rect.style.filter = "";
|
|
});
|
|
}
|
|
|
|
// ── Apply hot path visualization at current time ──
|
|
function applyHotPaths(svg, t) {
|
|
clearHotPaths(svg);
|
|
if (!allEvents.length || durationNs <= 0) return;
|
|
|
|
const activeHops = getActiveSipHops(t, 30);
|
|
let inflight = 0;
|
|
|
|
if (currentView === "sip") {
|
|
// ── SIP VIEW: color inter-cube links by traffic ──
|
|
const linkTraffic = {}; // linkId → total bytes
|
|
for (const hop of activeHops) {
|
|
const linkId = hopToSipLink(hop);
|
|
if (linkId) {
|
|
linkTraffic[linkId] = (linkTraffic[linkId] || 0) + (hop.nbytes || 0);
|
|
}
|
|
}
|
|
for (const [linkId, bytes] of Object.entries(linkTraffic)) {
|
|
const el = svg.querySelector(`[data-link="${linkId}"]`);
|
|
if (el) {
|
|
el.style.stroke = utilColor(bytes);
|
|
el.style.strokeWidth = "3";
|
|
el.classList.add("hot-path");
|
|
inflight++;
|
|
}
|
|
}
|
|
// Add packet dots on active links
|
|
for (const hop of activeHops) {
|
|
if (hop.type !== "hop") continue;
|
|
const linkId = hopToSipLink(hop);
|
|
if (!linkId) continue;
|
|
const el = svg.querySelector(`[data-link="${linkId}"]`);
|
|
if (!el) continue;
|
|
// Calculate progress within the hop
|
|
const hopAge = t - hop.t_ns;
|
|
const hopDuration = hop.latency_ns || 1;
|
|
const progress = Math.min(1, Math.max(0, hopAge / hopDuration));
|
|
const x1 = parseFloat(el.getAttribute("x1"));
|
|
const y1 = parseFloat(el.getAttribute("y1"));
|
|
const x2 = parseFloat(el.getAttribute("x2"));
|
|
const y2 = parseFloat(el.getAttribute("y2"));
|
|
const dotX = x1 + (x2 - x1) * progress;
|
|
const dotY = y1 + (y2 - y1) * progress;
|
|
const dot = svgEl("circle", {
|
|
cx: dotX, cy: dotY, r: 4,
|
|
fill: "#eab308", class: "packet-dot", opacity: 0.9
|
|
});
|
|
svg.appendChild(dot);
|
|
packetDots.push(dot);
|
|
}
|
|
|
|
} else if (currentView === "cube") {
|
|
// ── CUBE VIEW: highlight router mesh links ──
|
|
const linkTraffic = {};
|
|
for (const hop of activeHops) {
|
|
const linkId = hopToCubeLink(hop);
|
|
if (linkId) {
|
|
linkTraffic[linkId] = (linkTraffic[linkId] || 0) + (hop.nbytes || 0);
|
|
}
|
|
}
|
|
for (const [linkId, bytes] of Object.entries(linkTraffic)) {
|
|
const el = svg.querySelector(`[data-link="${linkId}"]`);
|
|
if (el) {
|
|
el.style.stroke = utilColor(bytes);
|
|
el.style.strokeWidth = "3";
|
|
el.classList.add("hot-path");
|
|
inflight++;
|
|
}
|
|
}
|
|
// Highlight HBM component referenced in events
|
|
const activeProcesses = allEvents.filter(e =>
|
|
e.type === "process" && e.t_ns <= t && e.t_ns >= t - 30
|
|
);
|
|
for (const proc of activeProcesses) {
|
|
const comp = proc.component || "";
|
|
if (comp.includes("hbm_ctrl")) highlightComponent(svg, "hbm_ctrl");
|
|
}
|
|
|
|
} else if (currentView === "pe") {
|
|
// ── PE VIEW: highlight data/command paths ──
|
|
const activeEvts = getActiveEvents(t, 30);
|
|
const activeLinks = new Set();
|
|
for (const ev of activeEvts) {
|
|
const linkId = eventToPeLink(ev);
|
|
if (linkId) activeLinks.add(linkId);
|
|
}
|
|
for (const linkId of activeLinks) {
|
|
const el = svg.querySelector(`[data-link="${linkId}"]`);
|
|
if (el) {
|
|
el.style.strokeWidth = "3.5";
|
|
el.style.opacity = "1";
|
|
el.classList.add("hot-path");
|
|
inflight++;
|
|
}
|
|
}
|
|
// Highlight active PE components
|
|
const activeProcs = allEvents.filter(e =>
|
|
e.type === "process" && e.t_ns <= t && e.t_ns >= t - 20
|
|
);
|
|
for (const proc of activeProcs) {
|
|
const comp = proc.component || "";
|
|
for (const name of ["pe_cpu", "pe_scheduler", "pe_dma", "pe_gemm", "pe_math", "pe_tcm"]) {
|
|
if (comp.includes(name)) highlightComponent(svg, name.replace("pe_scheduler", "pe_sched"));
|
|
}
|
|
}
|
|
}
|
|
|
|
inflightEl.textContent = inflight;
|
|
}
|
|
|
|
function highlightComponent(svg, dataId) {
|
|
const g = svg.querySelector(`[data-id="${dataId}"]`);
|
|
if (g) {
|
|
g.classList.add("active-component");
|
|
const rect = g.querySelector("rect");
|
|
if (rect) rect.style.filter = "brightness(1.5) drop-shadow(0 0 6px currentColor)";
|
|
}
|
|
}
|
|
|
|
// ── Animation loop ──
|
|
function animate(ts) {
|
|
if (playing && durationNs > 0) {
|
|
if (lastFrameTs === 0) lastFrameTs = ts;
|
|
const dtMs = ts - lastFrameTs;
|
|
lastFrameTs = ts;
|
|
// Advance simulation time: dtMs * speed → ns
|
|
// 1ms real = speed ns sim (adjustable)
|
|
currentTimeNs += dtMs * speed * 0.5; // 0.5 factor for comfortable viewing
|
|
if (currentTimeNs > durationNs) {
|
|
currentTimeNs = durationNs;
|
|
playing = false;
|
|
playBtn.innerHTML = "▶";
|
|
lastFrameTs = 0;
|
|
}
|
|
timeSlider.value = currentTimeNs.toFixed(1);
|
|
timeLabel.textContent = `t = ${currentTimeNs.toFixed(1)} ns`;
|
|
applyHotPaths(document.getElementById("topoSvg"), currentTimeNs);
|
|
} else {
|
|
lastFrameTs = 0;
|
|
}
|
|
requestAnimationFrame(animate);
|
|
}
|
|
|
|
// ── Timeline controls ──
|
|
timeSlider.addEventListener("input", () => {
|
|
currentTimeNs = parseFloat(timeSlider.value);
|
|
timeLabel.textContent = `t = ${currentTimeNs.toFixed(1)} ns`;
|
|
applyHotPaths(document.getElementById("topoSvg"), currentTimeNs);
|
|
});
|
|
|
|
playBtn.addEventListener("click", () => {
|
|
playing = !playing;
|
|
playBtn.innerHTML = playing ? "▮▮" : "▶";
|
|
if (playing) lastFrameTs = 0;
|
|
});
|
|
|
|
speedBtn.addEventListener("click", () => {
|
|
const speeds = [0.5, 1, 2, 4, 10];
|
|
const idx = speeds.indexOf(speed);
|
|
speed = speeds[(idx + 1) % speeds.length];
|
|
speedBtn.textContent = `${speed}x`;
|
|
});
|
|
|
|
// ── Tooltip ──
|
|
const tooltip = document.getElementById("tooltip");
|
|
document.getElementById("canvasArea").addEventListener("mousemove", (e) => {
|
|
const target = e.target.closest(".node-group");
|
|
if (target) {
|
|
const id = target.dataset.id || "";
|
|
const type = target.dataset.type || "";
|
|
let html = `<div class="tt-title">${id || type}</div>`;
|
|
if (type === "cube") {
|
|
html += '<div class="tt-row"><span>PEs</span><span class="tt-val">8</span></div>';
|
|
html += '<div class="tt-row"><span>HBM</span><span class="tt-val">48 GB</span></div>';
|
|
html += '<div class="tt-row"><span>Click to drill down</span></div>';
|
|
}
|
|
tooltip.innerHTML = html;
|
|
tooltip.style.display = "block";
|
|
tooltip.style.left = (e.offsetX + 12) + "px";
|
|
tooltip.style.top = (e.offsetY - 10) + "px";
|
|
} else {
|
|
tooltip.style.display = "none";
|
|
}
|
|
});
|
|
|
|
// ── WebSocket client (optional live streaming) ──
|
|
let ws = null;
|
|
function connectWebSocket() {
|
|
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
|
const url = `${proto}//${location.host}/ws`;
|
|
try {
|
|
ws = new WebSocket(url);
|
|
ws.onopen = () => {
|
|
console.log("WebSocket connected");
|
|
// Can request live streaming: ws.send(JSON.stringify({cmd: "stream_demo", speed: 1}));
|
|
};
|
|
ws.onmessage = (msg) => {
|
|
const data = JSON.parse(msg.data);
|
|
if (data.type === "events") {
|
|
// Batch load
|
|
allEvents = data.events;
|
|
durationNs = allEvents.length ? Math.max(...allEvents.map(e => e.t_ns)) : 0;
|
|
timeSlider.max = durationNs.toFixed(1);
|
|
timeEnd.textContent = `${durationNs.toFixed(1)} ns`;
|
|
eventCountEl.textContent = allEvents.length;
|
|
simDurationEl.textContent = `${durationNs.toFixed(1)} ns`;
|
|
} else if (data.type === "event") {
|
|
// Streaming: append single event
|
|
allEvents.push(data);
|
|
durationNs = Math.max(durationNs, data.t_ns);
|
|
timeSlider.max = durationNs.toFixed(1);
|
|
eventCountEl.textContent = allEvents.length;
|
|
}
|
|
};
|
|
ws.onclose = () => console.log("WebSocket disconnected");
|
|
ws.onerror = () => {}; // Silently handle WS errors
|
|
} catch (e) {
|
|
// WebSocket not available (static file mode)
|
|
}
|
|
}
|
|
|
|
// ── Init ──
|
|
drawSipView(document.getElementById("topoSvg"));
|
|
loadWorkloadList();
|
|
connectWebSocket();
|
|
requestAnimationFrame(animate);
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|