Files
kernbench2/scripts/paper/paper_latency_model_diagram.py
T
ywkang 7f437a20bd paper(platform): edit-pass through §2 KernBench Platform done
- Reordered §2 subsections to follow the SIP → CUBE → PE → graph
  flow: Why KernBench → Device and execution model → Latency model
  → Modeled hardware configuration. Readers now meet the device
  hierarchy before the graph abstraction that re-uses it.
- §2.2 Device and execution model: starts with the SIP/CUBE/PE
  hierarchy paragraphs (each anchoring fig:sip-arch, fig:cube-arch,
  fig:pe-arch); then the runtime-API/sim-engine/components bullet
  list; then the atomic-vs-composite command distinction (corrects
  the prior over-narrow framing that read every PE command as
  composite -- atomic single-engine commands exist too, and PE_CPU
  itself runs control-plane work directly).
- §2.3 Latency model: opens with the four-contribution decomposition
  (per-node overhead, per-edge transmission, drain, queuing delay)
  and the latency_model schematic; retains existing The hardware as
  a graph / From graph to DES / Latency contributions / Congestion
  / Control-plane cost model / Accuracy paragraphs. Accuracy
  paragraph now closes on KernBench's sufficiency for *relative*
  HW/SW design trade-offs given analytic + external-simulator
  agreement.
- New figures and assets:
  - figures/sip_architecture.pdf  (SIP-level graph view)
  - figures/cube_architecture.pdf (CUBE-level zoom-in)
  - figures/latency_model.png      (conceptual latency-model
                                   schematic with per-node /
                                   per-edge / drain / queuing-delay
                                   colour coding)
  - figures/pe_architecture.png    (carried over)
- Source-of-truth generator for the latency schematic:
  scripts/paper/paper_latency_model_diagram.py (a report-only
  harness under scripts/paper/ per the /paper isolation rule).
- main.tex preamble: \usepackage{tikz} added (kept from prior
  sequence-diagram draft -- harmless now that the latency model is
  a PNG; left in to keep paragraph numbering stable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 17:17:19 -07:00

536 lines
19 KiB
Python

#!/usr/bin/env python3
"""Conceptual latency-model diagram for the KernBench paper (v5).
Generic naming (Requester Node A/B, Router, Destination Node), with
Router internals visible (two input ports -> switch -> output queue ->
output port) so the queuing delay can be located *at the out port*
rather than at the box edge. The Destination Node shows queue -> drain
slot -> processing logic.
Highlights:
* Same-size Routers, each with explicit switching logic + output queue.
* Edge labels removed -- the wires speak for themselves.
* Annotation lines are strictly vertical; arrow heads use a larger
mutation_scale so no thin line protrudes past the tip; `shrinkA`
pulls the tail away from the text so they no longer overlap.
* "flit-level interleaving on wires" (not "shared FIFO").
* Destination: queue -> drain -> processing logic (no "served").
Output:
docs/report/1H-codesign-paper/figures/latency_model.png
Per /paper isolation: this is a report-only harness under scripts/paper/.
"""
import math
from pathlib import Path
import matplotlib.patches as patches
import matplotlib.pyplot as plt
OUT = Path(
"/Users/ywkang/kernbench/docs/report/1H-codesign-paper/figures/latency_model.png"
)
OUT.parent.mkdir(parents=True, exist_ok=True)
# --- Colours --------------------------------------------------------------
C_A = "#E07B5E"
C_B = "#5E9BD1"
C_NODE = "#FFFFFF"
C_BORD = "#222222"
C_WIRE = "#222222"
C_ANN = "#333333"
C_DIM = "#777777"
C_SWITCH = "#FAFAFA"
C_OQUEUE = "#EFEFEF"
C_PROC = "#F4ECDC"
C_QUEUE = "#3CB371" # medium-sea-green: distinctive vs A/B flit colours
# --- Canvas ---------------------------------------------------------------
fig = plt.figure(figsize=(17.0, 5.0))
ax = fig.add_subplot(111)
ax.set_xlim(0, 33)
ax.set_ylim(2.2, 11.2)
ax.axis("off")
# --- Top header: end-to-end latency formula -----------------------------
ax.text(
16.5, 10.6,
r"End-to-end latency = $\Sigma$ per-node overhead + "
r"$\Sigma$ per-edge transmission + drain + "
r"queuing delay",
ha="center", fontsize=12, color=C_ANN, weight="bold",
)
ax.plot([1.5, 31.5], [10.05, 10.05], color=C_DIM, lw=0.6)
# --- Helpers --------------------------------------------------------------
def box(cx, cy, w, h, label, fs=11, fweight="normal"):
ax.add_patch(patches.FancyBboxPatch(
(cx - w / 2, cy - h / 2), w, h,
boxstyle="round,pad=0.05",
facecolor=C_NODE, edgecolor=C_BORD, linewidth=1.6,
))
if label:
ax.text(cx, cy, label, ha="center", va="center",
fontsize=fs, weight=fweight)
def draw_flit_aligned(cx, cy, angle_rad, w, h, color, label):
"""Draw a flit polygon rotated to align with the wire angle."""
cos_a, sin_a = math.cos(angle_rad), math.sin(angle_rad)
corners = []
for dx, dy in [(-w / 2, -h / 2), (w / 2, -h / 2),
(w / 2, h / 2), (-w / 2, h / 2)]:
rx = dx * cos_a - dy * sin_a
ry = dx * sin_a + dy * cos_a
corners.append((cx + rx, cy + ry))
ax.add_patch(patches.Polygon(
corners, facecolor=color, edgecolor="black", linewidth=0.5,
))
ax.text(cx, cy, label, ha="center", va="center",
fontsize=9, color="white", weight="bold")
def draw_wire_with_flits(x0, y0, x1, y1, n_flits, label_char,
color=None, flit_w=0.66, flit_h=0.95, gap=0.10):
"""Wire (line) + endpoint arrowhead + flits centred on the line.
Wires are drawn in the neutral C_WIRE colour: the wire itself is
the place where the *transmission* delay accumulates (flit_size /
BW), not where flits queue. Queuing happens before the wire, at
the FIFO at the egress side -- coloured separately.
"""
head_back = 0.30
dx, dy = x1 - x0, y1 - y0
wire_len = math.hypot(dx, dy)
ux, uy = dx / wire_len, dy / wire_len
x_line_end = x1 - ux * head_back
y_line_end = y1 - uy * head_back
ax.plot([x0, x_line_end], [y0, y_line_end],
color=C_WIRE, lw=1.6, zorder=1)
head_len, head_half = 0.30, 0.16
bx = x1 - ux * head_len
by = y1 - uy * head_len
px, py = -uy, ux
tri = [
(x1, y1),
(bx + px * head_half, by + py * head_half),
(bx - px * head_half, by - py * head_half),
]
ax.add_patch(patches.Polygon(tri, facecolor=C_WIRE,
edgecolor=C_WIRE, linewidth=0.0))
# Flit train
angle_rad = math.atan2(dy, dx)
train_len = n_flits * flit_w + (n_flits - 1) * gap
centre_p = 0.5
start_p = centre_p - (train_len / 2) / wire_len
step_p = (flit_w + gap) / wire_len
if isinstance(label_char, str):
labels = [label_char] * n_flits
cols = [color] * n_flits
else:
labels = [c[0] for c in label_char]
cols = [c[1] for c in label_char]
for i in range(n_flits):
p = start_p + (i + 0.5) * step_p
cx = x0 + p * dx
cy = y0 + p * dy
draw_flit_aligned(cx, cy, angle_rad,
flit_w, flit_h, cols[i], labels[i])
def draw_router(cx, cy, w, h, two_inputs=True):
"""Same-size Router with explicit internal switching logic and an
output queue. Returns the wire-attachment (x,y) for each input
port and the single output port:
((in1_x, in1_y), (in2_x, in2_y) or None, (out_x, out_y))
"""
# Outer box
box(cx, cy, w, h, "")
ax.text(cx, cy + h / 2 - 0.32, "Router",
ha="center", fontsize=10, weight="bold")
# Input ports (small circles on the left edge)
in_x = cx - w / 2
in_x_internal = in_x + 0.25
if two_inputs:
in_y_top = cy + 0.55
in_y_bot = cy - 0.55
for iy in (in_y_top, in_y_bot):
ax.add_patch(patches.Circle(
(in_x_internal, iy), 0.12,
facecolor="white", edgecolor=C_BORD, linewidth=0.9,
))
else:
in_y_top = None
in_y_bot = cy
ax.add_patch(patches.Circle(
(in_x_internal, in_y_bot), 0.12,
facecolor="white", edgecolor=C_BORD, linewidth=0.9,
))
# Switch (small box, centre-left-ish). sw_cy = cy so that the
# output queue and (single-input) input port are at the same y as
# the router centre -- this keeps all router-to-router edges
# strictly horizontal.
sw_w, sw_h = 0.85, 1.10
sw_cx = cx - 0.55
sw_cy = cy
# The switch is the router's processing logic -- colour it the
# same as the Destination Node's processing-logic block so the
# two read as the same "processing" concept.
ax.add_patch(patches.Rectangle(
(sw_cx - sw_w / 2, sw_cy - sw_h / 2), sw_w, sw_h,
facecolor=C_PROC, edgecolor=C_BORD, linewidth=0.8,
))
ax.text(sw_cx, sw_cy, "switch", ha="center", va="center",
fontsize=7.5, style="italic")
# Short feeder lines (no arrowhead) from input ports to the
# switch. We deliberately drop arrowheads here: the head + port
# circle were too small at this scale and read as an overlap.
sw_left_x = sw_cx - sw_w / 2
if two_inputs:
for iy in (in_y_top, in_y_bot):
ax.plot(
[in_x_internal + 0.12, sw_left_x],
[iy, sw_cy + 0.25 * ((iy - cy) / 0.55)],
color=C_DIM, lw=0.7, zorder=1,
)
else:
ax.plot(
[in_x_internal + 0.12, sw_left_x],
[in_y_bot, sw_cy],
color=C_DIM, lw=0.7, zorder=1,
)
# Output queue (small queue holding a couple of flits). This *is*
# a real queueing location, so its fill takes the C_QUEUE family.
oq_w, oq_h = 0.95, 0.55
oq_cx = cx + 0.55
oq_cy = sw_cy
ax.add_patch(patches.Rectangle(
(oq_cx - oq_w / 2, oq_cy - oq_h / 2), oq_w, oq_h,
facecolor="#D9F0E1", edgecolor=C_QUEUE, linewidth=0.9,
))
ax.text(oq_cx, oq_cy + oq_h / 2 + 0.18, "FIFO",
ha="center", fontsize=7, color=C_DIM, style="italic")
# Two small flits inside (A and B) hinting at the in-flight contents
mini_w, mini_h = 0.22, 0.34
for i, (col, lab) in enumerate([(C_A, "A"), (C_B, "B")]):
mx = oq_cx - 0.30 + i * (mini_w + 0.06)
ax.add_patch(patches.Rectangle(
(mx, oq_cy - mini_h / 2), mini_w, mini_h,
facecolor=col, edgecolor="black", linewidth=0.3,
))
ax.text(mx + mini_w / 2, oq_cy, lab,
ha="center", va="center",
fontsize=5.5, color="white", weight="bold")
# Short feeder line (no arrowhead) from switch into out queue
ax.plot(
[sw_cx + sw_w / 2, oq_cx - oq_w / 2],
[sw_cy, oq_cy],
color=C_DIM, lw=0.7, zorder=1,
)
# Short feeder line from out queue to out port
out_x_internal = cx + w / 2 - 0.25
ax.plot(
[oq_cx + oq_w / 2, out_x_internal - 0.12],
[oq_cy, oq_cy],
color=C_DIM, lw=0.7, zorder=1,
)
# Output port circle on the right edge (a port marker, not a
# queue -- the queue is on the wire that follows, not the port).
ax.add_patch(patches.Circle(
(out_x_internal, oq_cy), 0.12,
facecolor="white", edgecolor=C_BORD, linewidth=0.9,
))
return (
(in_x_internal, in_y_top) if two_inputs else None,
(in_x_internal, in_y_bot),
(out_x_internal, oq_cy),
oq_cx, # also return the out-queue centre so callouts can target it
oq_cy,
)
# --- Box layout ---------------------------------------------------------
# Requester centres set to match Router 1's input-port y so that
# Edge 1A and Edge 1B are strictly horizontal. Box heights are kept
# small enough that a visible gap separates the two Requester boxes.
R1_TMP_CY = 7.0
ReqA = (2.8, R1_TMP_CY + 0.55) # = 7.55, matches in_y_top of R1
ReqB = (2.8, R1_TMP_CY - 0.55) # = 6.45, matches in_y_bot of R1
box(*ReqA, 3.0, 0.85, "Requester\nNode A", fs=10)
box(*ReqB, 3.0, 0.85, "Requester\nNode B", fs=10)
R_W, R_H = 3.0, 2.8
R1 = (10.5, 7.0)
R2 = (20.0, 7.0)
r1_in_top, r1_in_bot, r1_out, r1_oq_cx, r1_oq_cy = draw_router(
*R1, R_W, R_H, two_inputs=True,
)
_, r2_in_bot, r2_out, r2_oq_cx, r2_oq_cy = draw_router(
*R2, R_W, R_H, two_inputs=False,
)
# Destination Node (same height as Router; wide enough so queue +
# drain + processing-logic all fit on a single horizontal row).
Dst = (28.4, 7.0)
Dst_W, Dst_H = 6.0, R_H # match the Router height
box(*Dst, Dst_W, Dst_H, "")
ax.text(Dst[0], Dst[1] + Dst_H / 2 - 0.25, "Destination Node",
ha="center", fontsize=10, weight="bold")
# --- Edges: requester -> router 1 (Edge 1A & 1B, with flits) ------------
draw_wire_with_flits(
ReqA[0] + 1.6, ReqA[1],
r1_in_top[0] - 0.02, r1_in_top[1],
n_flits=4, color=C_A, label_char="A",
)
draw_wire_with_flits(
ReqB[0] + 1.6, ReqB[1],
r1_in_bot[0] - 0.02, r1_in_bot[1],
n_flits=4, color=C_B, label_char="B",
)
# Edge 2: router1 out -> router2 in (horizontal, interleaved)
labels_e2 = [("A", C_A) if i % 2 == 0 else ("B", C_B) for i in range(8)]
E2_y = r1_out[1]
draw_wire_with_flits(
r1_out[0] + 0.02, r1_out[1],
r2_in_bot[0] - 0.02, r2_in_bot[1],
n_flits=8, label_char=labels_e2,
)
# Edge 3: router2 out -> destination (horizontal, interleaved)
labels_e3 = [("A", C_A) if i % 2 == 0 else ("B", C_B) for i in range(4)]
draw_wire_with_flits(
r2_out[0] + 0.02, r2_out[1],
Dst[0] - Dst_W / 2, r2_out[1],
n_flits=4, label_char=labels_e3,
)
# --- Destination internals: queue -> drain -> processing logic ----------
# Light-green halo behind the queue area marks it as a queueing point.
C_QUEUE_BG = "#D9F0E1"
dy_main = Dst[1] - 0.10
Dst_left = Dst[0] - Dst_W / 2
# Queue (4 flits, FIFO; rightmost is the next to drain, so the
# order is set so the serve sequence after the in-flight A alternates
# A (in drain) -> B -> A -> B -> A -- giving a clean BABA queue.
qW, qH, qGap = 0.42, 0.62, 0.07
q_labels = ["A", "B", "A", "B"]
q_cols = [C_A, C_B, C_A, C_B]
q_total = len(q_labels) * qW + (len(q_labels) - 1) * qGap
q_x_start = Dst_left + 0.35
# Green halo behind the queue boxes (the queue itself is a queueing
# location -- mark it with the C_QUEUE colour family)
ax.add_patch(patches.FancyBboxPatch(
(q_x_start - 0.10, dy_main - qH / 2 - 0.08),
q_total + 0.20, qH + 0.16,
boxstyle="round,pad=0.02",
facecolor=C_QUEUE_BG, edgecolor=C_QUEUE,
linewidth=0.9, zorder=1.5,
))
for i, (lab, col) in enumerate(zip(q_labels, q_cols)):
qx = q_x_start + i * (qW + qGap)
ax.add_patch(patches.Rectangle(
(qx, dy_main - qH / 2), qW, qH,
facecolor=col, edgecolor="black", linewidth=0.4,
zorder=2,
))
ax.text(qx + qW / 2, dy_main, lab,
ha="center", va="center",
fontsize=8, color="white", weight="bold", zorder=3)
# Common y for the "queue" / "drain" labels. Matches the FIFO label
# spacing inside the Router (0.18 above the box top) and uses the same
# font size for visual consistency.
DST_LABEL_Y = dy_main + qH / 2 + 0.18
ax.text(q_x_start + q_total / 2, DST_LABEL_Y,
"queue",
ha="center", fontsize=7, style="italic", color=C_DIM)
# Drain slot (height matched to queue boxes; font matched to FIFO/queue
# labels for visual consistency)
dr_W, dr_H = 0.85, qH
dr_x = q_x_start + q_total + 0.35
dr_cx = dr_x + dr_W / 2
dr_cy = dy_main
ax.add_patch(patches.FancyBboxPatch(
(dr_x, dr_cy - dr_H / 2), dr_W, dr_H,
boxstyle="round,pad=0.03",
facecolor=C_A, edgecolor="black", linewidth=1.0,
))
ax.text(dr_cx, dr_cy, "A", ha="center", va="center",
fontsize=9, color="white", weight="bold")
ax.text(dr_cx, DST_LABEL_Y,
"drain",
ha="center", fontsize=7, style="italic", color=C_DIM)
# Drain-time double-arrow underneath the drain slot. Move the "drain
# time" text a touch further down so the descender does not collide
# with the arrowhead.
dt_y = dr_cy - dr_H / 2 - 0.28
ax.annotate(
"", xy=(dr_x + dr_W, dt_y), xytext=(dr_x, dt_y),
arrowprops=dict(arrowstyle="<->", lw=0.9, color=C_ANN),
)
ax.text(dr_cx, dt_y - 0.42, "drain time",
ha="center", fontsize=8.5, color=C_ANN, style="italic")
# Processing-logic block (after drain) — wider so the two-line text
# does not collide with the box edges
pl_W, pl_H = 1.75, 1.00
pl_x = dr_x + dr_W + 0.40
pl_cx = pl_x + pl_W / 2
pl_cy = dr_cy
ax.add_patch(patches.FancyBboxPatch(
(pl_x, pl_cy - pl_H / 2), pl_W, pl_H,
boxstyle="round,pad=0.03",
facecolor=C_PROC, edgecolor=C_BORD, linewidth=1.0,
))
ax.text(pl_cx, pl_cy, "processing\nlogic",
ha="center", va="center", fontsize=9)
# Small gray arrows along the queue -> drain -> processing chain
ax.annotate(
"", xy=(dr_x - 0.04, dr_cy),
xytext=(q_x_start + q_total + 0.13, dr_cy),
arrowprops=dict(arrowstyle="-|>", lw=0.7,
color=C_DIM, mutation_scale=7),
)
ax.annotate(
"", xy=(pl_x - 0.04, dr_cy),
xytext=(dr_x + dr_W + 0.04, dr_cy),
arrowprops=dict(arrowstyle="-|>", lw=0.7,
color=C_DIM, mutation_scale=7),
)
# --- Annotations (strictly vertical, no diagonals, no overlap) ---------
def vcallout(text, xy, x_text_top_y, fs=10, clearance=0.55,
marker_color=None):
"""Vertical dotted callout. Optional `marker_color` paints a small
coloured circle just left of the text -- used to tie the label to
a colour code in the diagram.
"""
text_y = x_text_top_y
if text_y < xy[1]:
line_top_y = text_y + clearance
else:
line_top_y = text_y - clearance
ax.plot([xy[0], xy[0]], [xy[1], line_top_y],
color=C_ANN, lw=0.9, linestyle=":", zorder=2)
if marker_color is not None:
# Text-width estimate at fs=10 (~0.18 data units per char) so
# the marker is placed clearly to the left of the text.
text_w = len(text) * 0.18
marker_x = xy[0] - text_w / 2 - 0.30
ax.add_patch(patches.Circle(
(marker_x, text_y), 0.14,
facecolor=marker_color, edgecolor=C_BORD, linewidth=0.6,
zorder=3,
))
ax.text(xy[0], text_y, text,
ha="center", va="center",
fontsize=fs, color=C_ANN, zorder=3)
# Transmission delay -> flit on Edge 2 (text BELOW the wire).
# Extra clearance ~ 2 x text height so the line stops well clear of
# the label.
e2_total = 8 * 0.66 + 7 * 0.10
e2_centre = (r1_out[0] + r2_in_bot[0]) / 2
e2_start = e2_centre - e2_total / 2
trans_idx = 5
trans_cx = e2_start + (trans_idx + 0.5) * (0.66 + 0.10)
vcallout(
"transmission delay = flit_size / BW",
xy=(trans_cx, E2_y - 0.55),
x_text_top_y=E2_y - 2.6,
clearance=0.45,
)
# Flit-level interleaving -> ABOVE the wire, anchored on a flit near
# the centre/right of the wire. Text y matches the queuing-delay
# callout so the two labels sit on the same horizontal row, separated
# horizontally so they don't collide.
int_idx = 4
int_cx = e2_start + (int_idx + 0.5) * (0.66 + 0.10)
vcallout(
"flit-level interleaving on wires",
xy=(int_cx, E2_y + 0.55),
x_text_top_y=R1[1] + R_H / 2 + 0.85,
)
# Queuing delay -> at Router 1 out-queue. Text positioned just above
# the Router-1 box.
vcallout(
"queuing delay",
xy=(r1_oq_cx, r1_oq_cy + 0.30),
x_text_top_y=R1[1] + R_H / 2 + 0.85,
marker_color=C_QUEUE,
)
# Drain -> below the drain slot (text is below).
# Extra clearance ~ 2.5 x text height so the line stops further from
# the label.
vcallout(
"drain = per-flit service occupancy",
xy=(dr_cx, dt_y - 0.66), # just *inside* the Destination Node
# bottom edge -- the dotted line then
# penetrates the box slightly rather
# than hanging below it
x_text_top_y=E2_y - 2.6,
clearance=0.60,
)
# Per-node overhead -> points at Router 1's switch block (where the
# component's fixed processing cost lives). Text on the same row as
# transmission delay and drain so the three "below the wire" callouts
# sit on one horizontal baseline.
R1_SW_CX = R1[0] - 0.55 # sw_cx for Router 1
R1_SW_BOTTOM_Y = R1[1] - 0.55 # bottom of sw box
vcallout(
"per-node overhead",
xy=(R1_SW_CX, R1_SW_BOTTOM_Y),
x_text_top_y=E2_y - 2.6,
clearance=0.60,
marker_color=C_PROC,
)
# --- Legend (close to the diagram) --------------------------------------
LX, LY = 1.0, 2.6
ax.add_patch(patches.Rectangle((LX, LY), 0.7, 0.45,
facecolor=C_A, edgecolor="black",
linewidth=0.4))
ax.text(LX + 0.95, LY + 0.22, "Transaction A flit",
va="center", fontsize=10)
ax.add_patch(patches.Rectangle((LX + 4.8, LY), 0.7, 0.45,
facecolor=C_B, edgecolor="black",
linewidth=0.4))
ax.text(LX + 5.75, LY + 0.22, "Transaction B flit",
va="center", fontsize=10)
# --- Save ----------------------------------------------------------------
fig.savefig(OUT, dpi=140, bbox_inches="tight", facecolor="white")
print(f"Wrote {OUT}")