Cube-view SVG: detailed topology validation rendering

- Dedicated cube_view renderer showing 6×6 router grid with attachments
- PE blocks drawn next to their router (above/below)
- HBM pseudo channel port bar (64 ports, color-coded by PE owner)
- Per-PE BW annotations on HBM links
- Router color-coded by type (PE/M_CPU/SRAM/UCIe/relay)
- Title shows mode, channel count, per-PE and total BW
- Legend for all component types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 22:03:38 -07:00
parent 5c6abe6d12
commit e94f1de078
3 changed files with 548 additions and 331 deletions
+289 -1
View File
@@ -89,7 +89,10 @@ def emit_diagrams(graph: TopologyGraph, out_dir: Path) -> list[Path]:
for name, view in views:
if view is None:
continue
svg = _render_view_svg(view)
if name == "cube_view":
svg = _render_cube_view_svg(view, graph.spec)
else:
svg = _render_view_svg(view)
path = out_dir / f"{name}.svg"
path.write_text(svg, encoding="utf-8")
created.append(path)
@@ -380,3 +383,288 @@ def _label_font_size(box_width: float, label: str) -> int:
def _escape(text: str) -> str:
"""Escape XML special characters."""
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# ── Cube-specific renderer ──────────────────────────────────────────
def _render_cube_view_svg(view: ViewGraph, spec: dict) -> str:
"""Render cube view with topology validation detail.
Shows: 6×6 router grid, PE attachments, HBM pseudo channel ports,
M_CPU/SRAM positions, UCIe connections, BW annotations.
"""
mesh_data = spec.get("_mesh", {})
routers = mesh_data.get("routers", {})
n_rows = mesh_data.get("mesh", {}).get("rows", 6)
n_cols = mesh_data.get("mesh", {}).get("cols", 6)
cube = spec.get("cube", {})
mm = cube.get("memory_map", {})
clinks = cube.get("links", {})
cube_w = cube.get("geometry", {}).get("cube_mm", {}).get("w", 17.0)
cube_h = cube.get("geometry", {}).get("cube_mm", {}).get("h", 14.0)
channels_per_pe = mm.get("hbm_channels_per_pe", 8)
channel_bw = mm.get("hbm_channel_bw_gbs", 32.0)
total_ch = mm.get("hbm_pseudo_channels", 64)
mode = mm.get("hbm_mapping_mode", "n_to_one")
agg_bw = channels_per_pe * channel_bw
scale = 50 # px per mm
pad = 60
w_px = int(cube_w * scale + 2 * pad)
h_px = int(cube_h * scale + 2 * pad + 80) # extra for legend
parts: list[str] = []
parts.append(_svg_header(w_px, h_px, "cube"))
# Background
parts.append(f' <rect width="{w_px}" height="{h_px}" fill="#0f172a"/>')
# Title
parts.append(
f' <text x="{w_px // 2}" y="22" text-anchor="middle" '
f'font-family="monospace" font-size="14" font-weight="bold" fill="#94a3b8">'
f'CUBE TOPOLOGY — {cube_w}×{cube_h}mm | {n_rows}×{n_cols} Router Mesh | '
f'{mode} mode | {total_ch} pseudo-ch</text>'
)
# Subtitle
parts.append(
f' <text x="{w_px // 2}" y="40" text-anchor="middle" '
f'font-family="monospace" font-size="10" fill="#64748b">'
f'Per-PE: {channels_per_pe} ch × {channel_bw} GB/s = {agg_bw} GB/s | '
f'Cube total: {total_ch} × {channel_bw} = {total_ch * channel_bw} GB/s</text>'
)
# Cube boundary
bx, by = pad, pad
parts.append(
f' <rect x="{bx}" y="{by}" width="{cube_w * scale}" height="{cube_h * scale}" '
f'rx="6" fill="none" stroke="#475569" stroke-width="2" stroke-dasharray="8,4"/>'
)
def mm2px(x_mm: float, y_mm: float) -> tuple[float, float]:
return pad + x_mm * scale, pad + y_mm * scale
# ── HBM zone background (centered, 9×5mm) ──
hbm_x, hbm_y = mm2px(4.0, 4.5)
hbm_w, hbm_h = 9.0 * scale, 5.0 * scale
parts.append(
f' <rect x="{hbm_x:.0f}" y="{hbm_y:.0f}" '
f'width="{hbm_w:.0f}" height="{hbm_h:.0f}" '
f'rx="6" fill="#052e16" stroke="#047857" stroke-width="2" opacity="0.6"/>'
)
# HBM label
hcx, hcy = mm2px(8.5, 7.0)
parts.append(
f' <text x="{hcx:.0f}" y="{hcy - 15:.0f}" text-anchor="middle" '
f'font-family="monospace" font-size="11" font-weight="bold" fill="#047857">'
f'HBM_CTRL | {total_ch} pseudo channels</text>'
)
parts.append(
f' <text x="{hcx:.0f}" y="{hcy + 2:.0f}" text-anchor="middle" '
f'font-family="monospace" font-size="9" fill="#05966988">'
f'Total BW: {total_ch * channel_bw:.0f} GB/s</text>'
)
# ── Pseudo channel port indicators (horizontal bar inside HBM zone) ──
port_bar_y = hcy + 15
port_bar_w = 8.0 * scale # slightly narrower than HBM zone
port_bar_x = hcx - port_bar_w / 2
port_w = port_bar_w / total_ch
for i in range(total_ch):
pe_owner = i // channels_per_pe
# Color by PE owner
colors = ["#3b82f6", "#60a5fa", "#8b5cf6", "#a78bfa",
"#f59e0b", "#fbbf24", "#ef4444", "#f87171"]
c = colors[pe_owner % len(colors)]
px = port_bar_x + i * port_w
parts.append(
f' <rect x="{px:.1f}" y="{port_bar_y:.0f}" '
f'width="{max(port_w - 0.5, 1):.1f}" height="8" '
f'fill="{c}" opacity="0.7"/>'
)
# Port bar label
parts.append(
f' <text x="{hcx:.0f}" y="{port_bar_y + 20:.0f}" text-anchor="middle" '
f'font-family="monospace" font-size="7" fill="#05966988">'
f'{total_ch} ports | {channels_per_pe} per PE (color-coded)</text>'
)
# ── Router mesh links ──
for r in range(n_rows):
for c in range(n_cols):
rkey = f"r{r}c{c}"
if routers.get(rkey) is None:
continue
rx, ry = routers[rkey]["pos_mm"]
sx, sy = mm2px(rx, ry)
# Horizontal neighbor
for nc in range(c + 1, n_cols):
nkey = f"r{r}c{nc}"
if routers.get(nkey) is None:
continue
nx, ny = routers[nkey]["pos_mm"]
dx, dy = mm2px(nx, ny)
parts.append(
f' <line x1="{sx:.0f}" y1="{sy:.0f}" '
f'x2="{dx:.0f}" y2="{dy:.0f}" '
f'stroke="#475569" stroke-width="1" opacity="0.4"/>'
)
break
# Vertical neighbor
for nr in range(r + 1, n_rows):
nkey = f"r{nr}c{c}"
if routers.get(nkey) is None:
continue
nx, ny = routers[nkey]["pos_mm"]
dx, dy = mm2px(nx, ny)
parts.append(
f' <line x1="{sx:.0f}" y1="{sy:.0f}" '
f'x2="{dx:.0f}" y2="{dy:.0f}" '
f'stroke="#475569" stroke-width="1" opacity="0.4"/>'
)
break
# ── Router nodes ──
r_size = 10 # px radius
pe_routers: dict[str, str] = {} # rkey → pe label
for rkey, rval in routers.items():
if rval is None:
continue
rx, ry = rval["pos_mm"]
px, py = mm2px(rx, ry)
attach = rval.get("attach", [])
# Determine router type by attachments
has_pe = any(a.endswith(".dma") for a in attach)
has_mcpu = "m_cpu" in attach
has_sram = "sram" in attach
has_ucie = any(a.startswith("ucie_") for a in attach)
if has_pe:
fill, stroke = "#3b82f6", "#1d4ed8"
pe_name = [a for a in attach if a.endswith(".dma")][0].split(".")[0]
pe_routers[rkey] = pe_name.upper()
elif has_mcpu:
fill, stroke = "#f59e0b", "#d97706"
elif has_sram:
fill, stroke = "#f59e0b", "#d97706"
elif has_ucie:
fill, stroke = "#8b5cf6", "#6d28d9"
else:
fill, stroke = "#334155", "#475569"
parts.append(
f' <circle cx="{px:.0f}" cy="{py:.0f}" r="{r_size}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="1.5"/>'
)
# Router label
parts.append(
f' <text x="{px:.0f}" y="{py + 3:.0f}" text-anchor="middle" '
f'font-family="monospace" font-size="7" font-weight="bold" fill="white">'
f'{rkey}</text>'
)
# Attachment labels below router
label_parts = []
if has_pe:
label_parts.append(pe_routers[rkey])
if has_mcpu:
label_parts.append("M_CPU")
if has_sram:
label_parts.append("SRAM")
ucie_items = [a for a in attach if a.startswith("ucie_")]
if ucie_items:
label_parts.append(f"UCIe×{len(ucie_items)}")
if label_parts:
parts.append(
f' <text x="{px:.0f}" y="{py + r_size + 10:.0f}" text-anchor="middle" '
f'font-family="monospace" font-size="6" fill="#94a3b8">'
f'{", ".join(label_parts)}</text>'
)
# ── PE → HBM connection line (for PE routers) ──
if has_pe:
# Draw line from router to HBM zone edge
target_y = hbm_y if py < hbm_y else hbm_y + hbm_h
parts.append(
f' <line x1="{px:.0f}" y1="{py + r_size:.0f}" '
f'x2="{px:.0f}" y2="{target_y:.0f}" '
f'stroke="#10b981" stroke-width="1.5" opacity="0.5" '
f'stroke-dasharray="4,3"/>'
)
# BW annotation
bw_y = (py + r_size + target_y) / 2
parts.append(
f' <text x="{px + 12:.0f}" y="{bw_y:.0f}" '
f'font-family="monospace" font-size="6" fill="#10b98188">'
f'{agg_bw:.0f}GB/s</text>'
)
# ── PE blocks (drawn next to their router) ──
pe_w, pe_h = 30, 18 # px
for rkey, rval in routers.items():
if rval is None:
continue
attach = rval.get("attach", [])
pe_dma = [a for a in attach if a.endswith(".dma")]
if not pe_dma:
continue
pe_name = pe_dma[0].split(".")[0] # "pe0"
pe_label = pe_name.upper()
rx, ry = rval["pos_mm"]
px, py = mm2px(rx, ry)
# Position PE block: top-half PEs above router, bottom-half below
is_top = ry < cube_h / 2
pe_x = px - pe_w / 2
pe_y = py - r_size - pe_h - 4 if is_top else py + r_size + 4
parts.append(
f' <rect x="{pe_x:.0f}" y="{pe_y:.0f}" '
f'width="{pe_w}" height="{pe_h}" '
f'rx="3" fill="#2d1f3d" stroke="#a855f7" stroke-width="1"/>'
)
parts.append(
f' <text x="{px:.0f}" y="{pe_y + pe_h / 2 + 4:.0f}" text-anchor="middle" '
f'font-family="monospace" font-size="8" font-weight="bold" fill="#a855f7">'
f'{pe_label}</text>'
)
# PE ↔ router link
link_y1 = pe_y + pe_h if is_top else pe_y
link_y2 = py - r_size if is_top else py + r_size
parts.append(
f' <line x1="{px:.0f}" y1="{link_y1:.0f}" '
f'x2="{px:.0f}" y2="{link_y2:.0f}" '
f'stroke="#a855f7" stroke-width="1.5" opacity="0.7"/>'
)
# ── Legend ──
ly = h_px - 35
legend_items = [
("#3b82f6", "PE Router"),
("#f59e0b", "M_CPU / SRAM"),
("#8b5cf6", "UCIe"),
("#334155", "Relay"),
("#10b981", "HBM Link"),
("#475569", "Mesh Link"),
]
lx = pad
for color, label in legend_items:
parts.append(
f' <rect x="{lx}" y="{ly}" width="10" height="10" rx="2" '
f'fill="{color}" stroke="#475569" stroke-width="0.5"/>'
)
parts.append(
f' <text x="{lx + 14}" y="{ly + 9}" '
f'font-family="monospace" font-size="8" fill="#94a3b8">'
f'{label}</text>'
)
lx += len(label) * 7 + 24
parts.append("</svg>")
return "\n".join(parts)