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:
@@ -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("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
# ── 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)
|
||||
|
||||
Reference in New Issue
Block a user