diff --git a/.gitignore b/.gitignore index 472185d1..07fd374c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ pcie_schematic.png # macOS .DS_Store + +__pycache__/ diff --git a/docs/images/daqiri_rx_path.svg b/docs/images/daqiri_rx_path.svg new file mode 100644 index 00000000..75bff4ee --- /dev/null +++ b/docs/images/daqiri_rx_path.svg @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + DAQIRI RX Path — Pointer Lifecycle & Multiple Receive Modes + + + DPDK backend · ConnectX-6/7 · GPUDirect via peermem or DMA-BUF (or host_pinned on integrated GPUs) + + + + + + + + Legend + + + packet data (DMA, zero-copy) + + + descriptor / handle exchange (rte_mbuf*, BurstParams*) + + + free-return path (caller must close) + + + setup-time wiring + + + + + + Wire — mixed UDP flows + + + udp_dst=4000 + udp_dst=4001 + udp_dst=4002 + flex_item match + udp_dst=5000 (batched) + udp_dst=5001 (batched) + + + + + + + + CONNECTX-6 / -7 NIC + Flow steering engine (rte_flow) + Matches on UDP src/dst, IPv4 length, or flex-item bytes after UDP header. Action = queue.id. + + + + rule → queue 0 (HDS, 2 segments) + + + rule → queue 1 (GPU-only, batched) + + + + + + + + + + + + Queue 0 — Header-Data Split + 2 memory_regions → segment 0 (CPU hdr) + segment 1 (GPU payload) + + + + + SEGMENT 0 — CPU MEMPOOL + kind: huge (hugepages) + buf_size ≈ 64 B (Eth+IPv4+UDP) + num_bufs ≥ 3× batch_size + Pre-allocated. NIC DMAs into + these slots — never copied. + + + + + + SEGMENT 1 — GPU MEMPOOL + kind: device (GPU VRAM) + DMA-mapped via peermem/DMA-BUF + buf_size = MTU − header + GPUDirect: NIC → GPU VRAM, + no CPU bounce. + + + + + + NIC DESCRIPTOR RING (per-queue, SPSC) + rte_mbuf * — one descriptor pair per packet (seg-0 + seg-1 mbuf chain) + Filled by NIC DMA · drained by rte_eth_rx_burst() in worker + + + + + + RX worker thread + pinned to rx.queue.cpu_core (e.g. 5) + + + + + + HOST RX RING (lock-free SPSC) + BurstParams * handles (allocated from rx_meta_buffers pool) + Worker enqueues · user dequeues via get_rx_burst() + + + + + + USER READS (HDS) + get_rx_burst(&burst, port, /*q=*/0) + N = get_num_packets(burst) + hdr_ptr = get_segment_packet_ptr(burst, /*seg=*/0, i) + pay_ptr = get_segment_packet_ptr(burst, /*seg=*/1, i) + + + + + + + + + Queue 1 — Batched GPU-only + 1 memory_region → segment 0 (full packet in GPU VRAM) + + + + + SEGMENT 0 — GPU MEMPOOL + kind: device (GPU VRAM) + buf_size = MTU (full packet, headers + payload) + num_bufs ≥ 3× batch_size (auto-bumped if <1.5× ring) + DMA-mapped via peermem/DMA-BUF; full packet lands + in VRAM in one shot — no segmentation. + + + + + + NIC DESCRIPTOR RING (per-queue, SPSC) + rte_mbuf * — one descriptor per packet (single segment) + Filled by NIC DMA · drained by rte_eth_rx_burst() + + + + + + RX worker thread + pinned to rx.queue.cpu_core (e.g. 6) + + + + + + HOST RX RING (lock-free SPSC) + BurstParams * handles (from rx_meta_buffers pool) + Worker enqueues · user dequeues via get_rx_burst() + + + + + + USER READS (BATCHED) + get_rx_burst(&burst, port, /*q=*/1) + N = get_num_packets(burst) + ptr = get_packet_ptr(burst, i) // single segment + len = get_packet_length(burst, i) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User Application + Owns the BurstParams* between get_rx_burst() and the matching free. Reads from packet buffers in place — zero-copy. + + + + HDS PATH (queue 0) + hdr = get_segment_packet_ptr(burst, 0, i); pay = get_segment_packet_ptr(burst, 1, i); + + + + + BATCHED PATH (queue 1) + ptr = get_packet_ptr(burst, i); flow_id = get_packet_flow_id(burst, i); + + + + + + + + + Buffer return path — packets first, then burst (MUST NOT LEAK) + + free_rx_burst() releases only the BurstParams handle. It does NOT free packet buffers — those must be returned to the mempool first. + + + Skipping the per-packet step drains the packet mempool → NO_FREE_PACKET_BUFFERS → NIC drops new traffic. + + + + + + A · ONE-SHOT (RECOMMENDED) + Frees every packet (all segments) + the burst + handle in a single call. + + free_all_packets_and_burst_rx(burst); + // works for HDS & batched, any segment count + + + + + + B · PER-SEGMENT (HDS, FINE-GRAIN) + Free each segment's packet pool, then release + the burst handle. + + free_all_segment_packets(burst, 0); + free_all_segment_packets(burst, 1); free_rx_burst(burst); + + + + + + C · PER-PACKET (SELECTIVE) + Use when only some packets are kept (e.g. forwarded + to another stage). Then close the burst. + + for (i …) free_packet(burst, i); + free_rx_burst(burst); + + + + + Where buffers actually go back: + + • free_packet* / free_all_segment_packets → returns rte_mbuf to the segment's packet mempool (CPU hugepage / GPU device). + + + • free_rx_burst → returns the BurstParams handle to the rx_meta_buffers pool (separate from the packet pool). + + + • free_all_packets_and_burst_rx = both of the above, in order. + + + + + + + SIZING / FAILURE NOTES + + Rule of thumb: num_bufs ≥ 3× batch_size; DPDK auto-bumps to 3× ring (default 8192 → 24576) and logs WARN if too small (deadlock guard). + + + Symptoms of a missed free: NO_FREE_PACKET_BUFFERS / NO_FREE_BURST_BUFFERS, then NIC-level RX drops in imissed. + + + + + + + + + + + + packets back to mempool (seg 0/1) + + + + packets back to mempool + + + + + + Notes + + ① Number of memory_regions per queue picks the receive mode (1 = batched / CPU-only, 2 = header-data split). See docs/configuration.md §Memory Regions. + + + ② Same flow rule schema serves both columns: rx.flows[*].action.id chooses the queue; mode is determined by that queue's memory_regions list. + + + ③ On integrated GPUs (e.g. NVIDIA GB10 / DGX Spark) substitute kind: host_pinned for device — the NIC can't peer-DMA into iGPU VRAM. + + + ④ Sources: include/daqiri/common.h (API), include/daqiri/types.h (MemoryKind), src/managers/dpdk/daqiri_dpdk_mgr.cpp (rte_flow), docs/configuration.md. + + + diff --git a/docs/images/packet_diagrams/flow_steering/flow-steering-light-poster.png b/docs/images/packet_diagrams/flow_steering/flow-steering-light-poster.png new file mode 100644 index 00000000..dc2e6623 Binary files /dev/null and b/docs/images/packet_diagrams/flow_steering/flow-steering-light-poster.png differ diff --git a/docs/images/packet_diagrams/flow_steering/flow-steering-light.gif b/docs/images/packet_diagrams/flow_steering/flow-steering-light.gif new file mode 100644 index 00000000..304a9fc3 Binary files /dev/null and b/docs/images/packet_diagrams/flow_steering/flow-steering-light.gif differ diff --git a/docs/images/packet_diagrams/flow_steering/flow-steering-poster.png b/docs/images/packet_diagrams/flow_steering/flow-steering-poster.png new file mode 100644 index 00000000..9bab6e04 Binary files /dev/null and b/docs/images/packet_diagrams/flow_steering/flow-steering-poster.png differ diff --git a/docs/images/packet_diagrams/flow_steering/flow-steering.gif b/docs/images/packet_diagrams/flow_steering/flow-steering.gif new file mode 100644 index 00000000..f244af09 Binary files /dev/null and b/docs/images/packet_diagrams/flow_steering/flow-steering.gif differ diff --git a/docs/images/packet_diagrams/flow_steering_animation.py b/docs/images/packet_diagrams/flow_steering_animation.py new file mode 100644 index 00000000..3fe1eebc --- /dev/null +++ b/docs/images/packet_diagrams/flow_steering_animation.py @@ -0,0 +1,682 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +from incoming_wire import center_wire_y, draw_rx_wire, draw_rx_wire_label, path_point_and_tangent, wire_t_end + + +ROOT = Path(__file__).resolve().parent +OUTPUT_DIR = ROOT / "flow_steering" + +WIDTH = 1180 +HEIGHT = 660 +SCALE = 2 +DURATION_MS = 55 + +GPU_TRAVEL_FRAMES = 72 +PACKET_GAP = 4 +DECIDE_OFFSET = 20 + +LAYOUT_SHIFT_Y = 42 + +NIC_RECT = (382, 234, 582, 392) +HOST_RECT = (772, 112, 1118, 252) +GPU_RECT = (772, 392, 1118, 532) +HOST_ROW = (796, 192, 1092, 234) +GPU_ROW = (796, 472, 1092, 514) + +WIRE_Y = 327 +KERNEL_WIDTH = 200 + +TRANSPARENT = (0, 0, 0, 0) +GIF_TRANSPARENCY_INDEX = 255 + +ACCENTS = { + "nvidia": "#76b900", + "host": "#9b8cff", + "gpu": "#59d4ff", + "kernel": "#3b82f6", + "host_pkt": "#9b8cff", + "gpu_pkt": "#59d4ff", + "ink": "#07111f", +} + +THEMES: dict[str, dict[str, str | None]] = { + "default": { + **ACCENTS, + "bg": None, + "panel": "#0d1b2e", + "panel_2": "#101f34", + "line": "#35516f", + "text": "#f6fbff", + "muted": "#afc0cf", + "wire": "#6d7e92", + "shadow": "#03101d", + "row_bg": "#061220", + "arrow": "#ffffff", + "stroke": "#ffffff", + "bar_glow": "#ffffff", + "kernel_fill": "#0f2744", + "bypass": "#64748b", + "match_ok": "#76b900", + "match_no": "#ef4444", + }, + "light": { + **ACCENTS, + "bg": "#ffffff", + "panel": "#f5f5f5", + "panel_2": "#eeeeee", + "line": "#1a1a1a", + "text": "#1a1a1a", + "muted": "#404040", + "wire": "#404040", + "shadow": "#bdbdbd", + "row_bg": "#e8e8e8", + "arrow": "#1a1a1a", + "stroke": "#1a1a1a", + "bar_glow": "#cccccc", + "kernel_fill": "#f0f4ff", + "bypass": "#64748b", + "match_ok": "#15803d", + "match_no": "#dc2626", + }, +} + +THEME_OUTPUT_SUFFIX = { + "default": "", + "light": "-light", +} + +COLORS = THEMES["default"] + + +@dataclass(frozen=True) +class PacketSpec: + num: int + dest: str # "host" (host_flow → queue 0) or "gpu" (gpu_flow → queue 1) + start: int + decide: int + end: int + + @property + def matched(self) -> bool: + return True + + +def shift_y(y: float) -> float: + return y + LAYOUT_SHIFT_Y + + +def shift_rect(rect: tuple[float, float, float, float]) -> tuple[float, float, float, float]: + x1, y1, x2, y2 = rect + return x1, y1 + LAYOUT_SHIFT_Y, x2, y2 + LAYOUT_SHIFT_Y + + +NIC = shift_rect(NIC_RECT) +HOST = shift_rect(HOST_RECT) +GPU = shift_rect(GPU_RECT) +HOST_ROW_R = shift_rect(HOST_ROW) +GPU_ROW_R = shift_rect(GPU_ROW) +WIRE = shift_y(WIRE_Y) +KERNEL_CX = ((488 + 688) / 2 + (NIC_RECT[2] + HOST_RECT[0]) / 2) / 2 +KERNEL = shift_rect((KERNEL_CX - KERNEL_WIDTH / 2, 4, KERNEL_CX + KERNEL_WIDTH / 2, 82)) +NIC_CX = (NIC[0] + NIC[2]) / 2 + + +def rgb(hex_color: str) -> tuple[int, int, int]: + hex_color = hex_color.strip("#") + return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + + +def rgba(hex_color: str, alpha: int = 255) -> tuple[int, int, int, int]: + r, g, b = rgb(hex_color) + return r, g, b, alpha + + +def font(size: int, *, bold: bool = False) -> ImageFont.FreeTypeFont: + if bold: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", + "C:/Windows/Fonts/segoeuib.ttf", + ] + else: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "C:/Windows/Fonts/segoeui.ttf", + ] + for path in candidates: + if Path(path).exists(): + return ImageFont.truetype(path, size * SCALE) + return ImageFont.load_default() + + +FONTS = { + "label": font(18, bold=True), + "small": font(14), + "tiny": font(12), + "chip": font(21, bold=True), + "badge": font(16, bold=True), +} + + +def s(value: float) -> int: + return int(round(value * SCALE)) + + +def pt(point: tuple[float, float]) -> tuple[int, int]: + return s(point[0]), s(point[1]) + + +def box(rect: tuple[float, float, float, float]) -> tuple[int, int, int, int]: + return tuple(s(v) for v in rect) + + +def draw_text( + draw: ImageDraw.ImageDraw, + xy: tuple[float, float], + text: str, + face: ImageFont.FreeTypeFont, + fill: str | tuple[int, int, int, int] = COLORS["text"], + anchor: str = "la", +) -> None: + draw.text(pt(xy), text, font=face, fill=fill, anchor=anchor) + + +def centered_text( + draw: ImageDraw.ImageDraw, + rect: tuple[float, float, float, float], + text: str, + face: ImageFont.FreeTypeFont, + fill: str | tuple[int, int, int, int] = COLORS["text"], +) -> None: + x1, y1, x2, y2 = rect + draw.text((s((x1 + x2) / 2), s((y1 + y2) / 2)), text, font=face, fill=fill, anchor="mm") + + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + +def clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float: + return max(lo, min(hi, value)) + + +def progress_linear(frame: int, start: int, end: int) -> float: + return clamp((frame - start) / max(1, end - start)) + + +def rectangular_path(waypoints: tuple[tuple[float, float], ...], steps: int = 32) -> list[tuple[float, float]]: + if len(waypoints) < 2: + return list(waypoints) + path: list[tuple[float, float]] = [] + for i in range(len(waypoints) - 1): + x0, y0 = waypoints[i] + x1, y1 = waypoints[i + 1] + for step in range(steps + 1): + if i > 0 and step == 0: + continue + t = step / steps + path.append((lerp(x0, x1, t), lerp(y0, y1, t))) + return path + + +def draw_polyline( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + width: int = 4, +) -> None: + draw.line([pt(p) for p in points], fill=color, width=s(width)) + + +def draw_dashed_polyline( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + width: int = 3, + dash: float = 10, + gap: float = 14, +) -> None: + for i in range(len(points) - 1): + x0, y0 = points[i] + x1, y1 = points[i + 1] + seg_len = math.hypot(x1 - x0, y1 - y0) + if seg_len <= 0: + continue + ux, uy = (x1 - x0) / seg_len, (y1 - y0) / seg_len + pos = 0.0 + draw_on = True + while pos < seg_len: + step = dash if draw_on else gap + end = min(seg_len, pos + step) + if draw_on: + draw.line( + (pt((x0 + ux * pos, y0 + uy * pos)), pt((x0 + ux * end, y0 + uy * end))), + fill=color, + width=s(width), + ) + pos = end + draw_on = not draw_on + + +def draw_arrow_head( + draw: ImageDraw.ImageDraw, + end: tuple[float, float], + prev: tuple[float, float], + color: str | tuple[int, int, int, int], + size: float = 12, +) -> None: + angle = math.atan2(end[1] - prev[1], end[0] - prev[0]) + wing = math.radians(28) + p1 = (end[0] - size * math.cos(angle - wing), end[1] - size * math.sin(angle - wing)) + p2 = (end[0] - size * math.cos(angle + wing), end[1] - size * math.sin(angle + wing)) + draw.polygon([pt(end), pt(p1), pt(p2)], fill=color) + + +def draw_arrow( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + width: int = 4, + arrow_size: float = 13, +) -> None: + draw_polyline(draw, points, color, width) + if len(points) >= 2: + prev_idx = max(0, len(points) - 5) + draw_arrow_head(draw, points[-1], points[prev_idx], color, arrow_size) + + +def draw_glow_line( + base: Image.Image, + points: list[tuple[float, float]], + color: str, + alpha: int, + width: int, +) -> None: + layer = Image.new("RGBA", base.size, (0, 0, 0, 0)) + d = ImageDraw.Draw(layer) + draw_polyline(d, points, rgba(color, alpha), width) + base.alpha_composite(layer) + + +def panel_left_center(rect: tuple[float, float, float, float]) -> tuple[float, float]: + x1, y1, x2, y2 = rect + return x1, (y1 + y2) / 2 + + +def panel_top_center(rect: tuple[float, float, float, float]) -> tuple[float, float]: + x1, y1, x2, _ = rect + return (x1 + x2) / 2, y1 + + +def build_paths() -> dict[str, list[tuple[float, float]]]: + host_y = (HOST_ROW_R[1] + HOST_ROW_R[3]) / 2 + gpu_y = (GPU_ROW_R[1] + GPU_ROW_R[3]) / 2 + bus_y = (KERNEL[1] + KERNEL[3]) / 2 + kernel_in = (KERNEL[0], bus_y) + kernel_out = (KERNEL[2], bus_y) + nic_top = (NIC_CX, NIC[1]) + nic_bottom = (NIC_CX, NIC[3]) + host_entry = panel_top_center(HOST) + gpu_entry = panel_left_center(GPU) + + host_nic_to_kernel = rectangular_path((nic_top, (nic_top[0], bus_y), kernel_in)) + host_kernel_to_queue = rectangular_path((kernel_out, (host_entry[0], bus_y), host_entry)) + host_direct = host_nic_to_kernel + host_kernel_to_queue[1:] + + wire = rectangular_path(((48, WIRE), (NIC[0], WIRE))) + host_tail = rectangular_path( + ( + (NIC_CX, WIRE), + nic_top, + (nic_top[0], bus_y), + kernel_in, + kernel_out, + (host_entry[0], bus_y), + host_entry, + (host_entry[0], host_y), + (HOST_ROW_R[0], host_y), + ) + ) + gpu_tail = rectangular_path( + ( + (NIC_CX, WIRE), + nic_bottom, + (nic_bottom[0], gpu_entry[1]), + gpu_entry, + (gpu_entry[0], gpu_y), + (GPU_ROW_R[0], gpu_y), + ) + ) + return { + "wire": wire, + "host": wire + host_tail[1:], + "gpu": wire + gpu_tail[1:], + "host_nic_to_kernel": host_nic_to_kernel, + "host_kernel_to_queue": host_kernel_to_queue, + "host_direct": host_direct, + "gpu_direct": rectangular_path( + ( + nic_bottom, + (nic_bottom[0], gpu_entry[1]), + gpu_entry, + ) + ), + } + + +PATHS = build_paths() + + +def path_total(points: list[tuple[float, float]]) -> float: + total = 0.0 + for i in range(1, len(points)): + x0, y0 = points[i - 1] + x1, y1 = points[i] + total += math.hypot(x1 - x0, y1 - y0) + return total + + +def make_packets() -> tuple[PacketSpec, ...]: + gpu_len = path_total(PATHS["gpu"]) + host_len = path_total(PATHS["host"]) + px_per_frame = gpu_len / GPU_TRAVEL_FRAMES + + def travel_frames(dest: str) -> int: + length = host_len if dest == "host" else gpu_len + return max(1, int(math.ceil(length / px_per_frame))) + + # Packets 1, 4: host_flow match (UDP 5000) → RX queue 0. + # Packets 2, 3, 5: gpu_flow match (UDP 4096) → RX queue 1. + routing = ((1, "host"), (2, "gpu"), (3, "gpu"), (4, "host"), (5, "gpu")) + packets: list[PacketSpec] = [] + cursor = 8 + for num, dest in routing: + duration = travel_frames(dest) + packets.append(PacketSpec(num, dest, cursor, cursor + DECIDE_OFFSET, cursor + duration)) + cursor += duration + PACKET_GAP + return tuple(packets) + + +PACKETS = make_packets() +FRAMES = max(320, PACKETS[-1].end + 24) + + +def active_packet(frame: int) -> PacketSpec | None: + for pkt in PACKETS: + if pkt.start <= frame < pkt.end: + return pkt + return None + + +def decision_pulse(frame: int, pkt: PacketSpec) -> float: + return max(progress_linear(frame, pkt.decide, pkt.decide + 8), 1.0 - progress_linear(frame, pkt.decide + 10, pkt.decide + 18)) + + +def queued_packets(frame: int) -> tuple[list[int], list[int]]: + host_nums: list[int] = [] + gpu_nums: list[int] = [] + for pkt in PACKETS: + if frame >= pkt.end: + if pkt.dest == "host": + host_nums.append(pkt.num) + elif pkt.dest == "gpu": + gpu_nums.append(pkt.num) + return host_nums, gpu_nums + + +def draw_whole_packet( + draw: ImageDraw.ImageDraw, + cx: float, + cy: float, + width: float, + color: str, + alpha: int = 255, + *, + label: str | None = None, +) -> None: + h = 32 + x1, y1 = cx - width / 2, cy - h / 2 + x2, y2 = cx + width / 2, cy + h / 2 + draw.rounded_rectangle( + box((x1, y1, x2, y2)), + radius=s(6), + fill=rgba(color, alpha), + outline=rgba(str(COLORS["stroke"]), 100 * alpha // 255), + width=s(2), + ) + if label: + centered_text(draw, (x1, y1, x2, y2), label, FONTS["tiny"], fill=rgba(COLORS["ink"], alpha)) + + +def packet_color(_num: int, dest: str) -> str: + return COLORS["gpu_pkt"] if dest == "gpu" else COLORS["host_pkt"] + + +def queue_chip_layout(rect: tuple[float, float, float, float], count: int) -> list[tuple[float, float, float]]: + x1, y1, x2, y2 = rect + if count <= 0: + return [] + pad = 8 + gap = 8 + inner_w = x2 - x1 - 2 * pad + chip_w = (inner_w - gap * (count - 1)) / count + chip_w = min(72, chip_w) + cy = (y1 + y2) / 2 + slots: list[tuple[float, float, float]] = [] + x = x1 + pad + for _ in range(count): + slots.append((x + chip_w / 2, cy, chip_w)) + x += chip_w + gap + return slots + + +def packet_dest(num: int) -> str: + for pkt in PACKETS: + if pkt.num == num: + return pkt.dest + return "host" + + +def draw_queue_row( + draw: ImageDraw.ImageDraw, + rect: tuple[float, float, float, float], + packet_nums: list[int], + accent: str, +) -> None: + x1, y1, x2, y2 = rect + draw.rounded_rectangle(box(rect), radius=s(8), fill=rgba(COLORS["row_bg"], 235), outline=rgba(accent, 145), width=s(2)) + if not packet_nums: + return + for num, (cx, cy, chip_w) in zip(packet_nums, queue_chip_layout(rect, len(packet_nums))): + color = packet_color(num, packet_dest(num)) + draw_whole_packet(draw, cx, cy, chip_w, color, label=f"packet {num}") + + +def draw_host_route(draw: ImageDraw.ImageDraw) -> None: + color = rgba(COLORS["host_pkt"], 255) + draw_polyline(draw, PATHS["host_nic_to_kernel"], color, 3) + draw_polyline(draw, PATHS["host_kernel_to_queue"], color, 3) + + +def draw_kernel_bypass(draw: ImageDraw.ImageDraw) -> None: + x1, y1, x2, y2 = KERNEL + draw.rounded_rectangle(box(KERNEL), radius=s(12), fill=rgba(COLORS["kernel_fill"], 245), outline=rgba(COLORS["kernel"], 210), width=s(2)) + centered_text(draw, (x1, y1 + 8, x2, y2 - 8), "Linux kernel", FONTS["label"], fill=COLORS["text"]) + + +def draw_matched_routes(draw: ImageDraw.ImageDraw) -> None: + draw_polyline(draw, PATHS["gpu_direct"], rgba(COLORS["gpu_pkt"], 255), 3) + + +def draw_nic_chip(draw: ImageDraw.ImageDraw, frame: int) -> None: + x1, y1, x2, y2 = NIC + pkt = active_packet(frame) + deciding = pkt is not None and pkt.decide <= frame < pkt.decide + 18 + pulse = decision_pulse(frame, pkt) if deciding and pkt else 0.0 + accent = COLORS["nvidia"] + + glow_alpha = int(55 + 120 * pulse) if deciding else 55 + draw.rounded_rectangle(box((x1 - 10, y1 - 10, x2 + 10, y2 + 10)), radius=s(24), fill=rgba(accent, 16 + glow_alpha // 3)) + draw.rounded_rectangle(box(NIC), radius=s(18), fill=COLORS["panel"], outline=rgba(accent, 190 + int(65 * pulse)), width=s(3 + int(2 * pulse))) + for i in range(7): + y = lerp(y1 + 18, y2 - 18, i / 6) + draw.line((s(x1 - 14), s(y), s(x1), s(y)), fill=rgba(COLORS["line"], 220), width=s(3)) + draw.line((s(x2), s(y), s(x2 + 14), s(y)), fill=rgba(COLORS["line"], 220), width=s(3)) + for i in range(5): + x = lerp(x1 + 25, x2 - 25, i / 4) + draw.line((s(x), s(y1 - 12), s(x), s(y1)), fill=rgba(COLORS["line"], 220), width=s(3)) + draw.line((s(x), s(y2), s(x), s(y2 + 12)), fill=rgba(COLORS["line"], 220), width=s(3)) + centered_text(draw, (x1 + 18, y1 + 38, x2 - 18, y1 + 78), "NVIDIA NIC", FONTS["chip"], fill=COLORS["text"]) + + pill = (x1 + 18, y1 + 82, x2 - 18, y1 + 110) + if deciding and pkt: + if pkt.dest == "host": + pill_text = "✓ rx queue 0" + pill_accent = COLORS["host_pkt"] + else: + pill_text = "✓ rx queue 1" + pill_accent = COLORS["gpu_pkt"] + pill_ink = "#ffffff" + else: + pill_text = "flow match rule" + pill_ink = COLORS["ink"] + pill_accent = COLORS["nvidia"] + draw.rounded_rectangle(box(pill), radius=s(8), fill=rgba(pill_accent, 180 + int(60 * pulse)), outline=rgba(pill_accent, 220), width=s(2)) + centered_text(draw, pill, pill_text, FONTS["badge"], fill=pill_ink) + + +def draw_device_memory(draw: ImageDraw.ImageDraw, frame: int) -> None: + host_nums, gpu_nums = queued_packets(frame) + panels = ( + (HOST, HOST_ROW_R, "RX queue 0", "host_flow · UDP 5000/5000", COLORS["host"], host_nums, COLORS["host_pkt"], "host"), + (GPU, GPU_ROW_R, "RX queue 1", "gpu_flow · UDP 4096/4096", COLORS["gpu"], gpu_nums, COLORS["gpu_pkt"], "gpu"), + ) + for panel_rect, row_rect, title, subtitle, accent, nums, pkt_color, _dest in panels: + x1, y1, _, _ = panel_rect + draw.rounded_rectangle(box(panel_rect), radius=s(18), fill=COLORS["panel_2"], outline=rgba(accent, 170), width=s(3)) + draw_text(draw, (x1 + 22, y1 + 22), title, FONTS["label"], fill=COLORS["text"], anchor="lt") + draw_text(draw, (x1 + 22, y1 + 46), subtitle, FONTS["small"], fill=COLORS["muted"], anchor="lt") + draw_queue_row(draw, row_rect, nums, pkt_color) + + +def draw_wires(draw: ImageDraw.ImageDraw, frame: int) -> None: + draw_rx_wire_label(lambda xy, text: draw_text(draw, xy, text, FONTS["label"], fill=COLORS["text"]), WIRE) + draw_rx_wire(draw, frame, 48, NIC[0], WIRE, str(COLORS["wire"]), "#ffcf5a", pt, s, rgba, box) + + +def draw_route_glow(base: Image.Image, frame: int) -> None: + pkt = active_packet(frame) + if pkt and frame >= pkt.decide: + glow = progress_linear(frame, pkt.decide, pkt.end) + if pkt.dest == "host": + draw_glow_line(base, PATHS["host_direct"], COLORS["host_pkt"], int(50 + 40 * glow), 7) + else: + draw_glow_line(base, PATHS["gpu_direct"], COLORS["gpu_pkt"], int(50 + 40 * glow), 7) + + +def draw_flowing_packets(draw: ImageDraw.ImageDraw, frame: int) -> None: + pkt = active_packet(frame) + if pkt is None: + return + path = PATHS["gpu"] if pkt.dest == "gpu" else PATHS["host"] + color = str(packet_color(pkt.num, pkt.dest)) + t = progress_linear(frame, pkt.start, pkt.end) + if 0 < t < 1: + on_wire = t <= wire_t_end(PATHS["wire"], path) + cx, cy, _ = path_point_and_tangent(path, t) + if on_wire: + cy = center_wire_y(cx, 48, WIRE, frame) + draw_whole_packet(draw, cx, cy, 92, color, label=f"packet {pkt.num}") + + +def path_lengths(points: list[tuple[float, float]]) -> tuple[list[float], float]: + lengths = [0.0] + for i in range(1, len(points)): + x0, y0 = points[i - 1] + x1, y1 = points[i] + lengths.append(lengths[-1] + math.hypot(x1 - x0, y1 - y0)) + return lengths, lengths[-1] + + +def point_on_path(points: list[tuple[float, float]], t: float) -> tuple[float, float]: + if len(points) < 2: + return points[0] if points else (0.0, 0.0) + t = clamp(t) + lengths, total = path_lengths(points) + if total <= 0: + return points[0] + target = t * total + for i in range(1, len(points)): + if lengths[i] >= target: + seg_len = lengths[i] - lengths[i - 1] + local = 0.0 if seg_len <= 0 else (target - lengths[i - 1]) / seg_len + x0, y0 = points[i - 1] + x1, y1 = points[i] + return lerp(x0, x1, local), lerp(y0, y1, local) + return points[-1] + + +def rgba_frame_to_palette(frame: Image.Image) -> Image.Image: + rgba_img = frame.convert("RGBA") + alpha = rgba_img.getchannel("A") + rgb_img = Image.new("RGB", rgba_img.size, (0, 0, 0)) + rgb_img.paste(rgba_img, mask=alpha) + palette = rgb_img.quantize(colors=254, method=Image.Quantize.MEDIANCUT) + palette.paste(GIF_TRANSPARENCY_INDEX, mask=alpha.point(lambda a: 255 if a < 128 else 0)) + return palette + + +def render_frame(frame: int) -> Image.Image: + bg = COLORS.get("bg") + img = Image.new("RGBA", (WIDTH * SCALE, HEIGHT * SCALE), rgba(str(bg), 255) if bg else TRANSPARENT) + draw = ImageDraw.Draw(img) + draw_wires(draw, frame) + draw_device_memory(draw, frame) + draw_host_route(draw) + draw_kernel_bypass(draw) + draw_nic_chip(draw, frame) + draw_matched_routes(draw) + draw_route_glow(img, frame) + draw_flowing_packets(draw, frame) + return img.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS) + + +def render_theme(theme: str) -> None: + global COLORS + COLORS = THEMES[theme] + suffix = THEME_OUTPUT_SUFFIX[theme] + gif_path = OUTPUT_DIR / f"flow-steering{suffix}.gif" + poster_path = OUTPUT_DIR / f"flow-steering{suffix}-poster.png" + + frames = [render_frame(i) for i in range(FRAMES)] + gif_frames = [rgba_frame_to_palette(f) for f in frames] + gif_frames[0].save( + gif_path, + save_all=True, + append_images=gif_frames[1:], + optimize=True, + duration=DURATION_MS, + loop=0, + disposal=2, + transparency=GIF_TRANSPARENCY_INDEX, + ) + render_frame(210).save(poster_path, optimize=True) + print(f"Wrote {gif_path}") + print(f"Wrote {poster_path}") + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + for theme in THEMES: + render_theme(theme) + + +if __name__ == "__main__": + main() diff --git a/docs/images/packet_diagrams/hds/header-data-split-light-poster.png b/docs/images/packet_diagrams/hds/header-data-split-light-poster.png new file mode 100644 index 00000000..a3d5f440 Binary files /dev/null and b/docs/images/packet_diagrams/hds/header-data-split-light-poster.png differ diff --git a/docs/images/packet_diagrams/hds/header-data-split-light.gif b/docs/images/packet_diagrams/hds/header-data-split-light.gif new file mode 100644 index 00000000..97ed1f71 Binary files /dev/null and b/docs/images/packet_diagrams/hds/header-data-split-light.gif differ diff --git a/docs/images/packet_diagrams/hds/header-data-split-poster.png b/docs/images/packet_diagrams/hds/header-data-split-poster.png new file mode 100644 index 00000000..c034090b Binary files /dev/null and b/docs/images/packet_diagrams/hds/header-data-split-poster.png differ diff --git a/docs/images/packet_diagrams/hds/header-data-split.gif b/docs/images/packet_diagrams/hds/header-data-split.gif new file mode 100644 index 00000000..3ad7927e Binary files /dev/null and b/docs/images/packet_diagrams/hds/header-data-split.gif differ diff --git a/docs/images/packet_diagrams/hds_animation.py b/docs/images/packet_diagrams/hds_animation.py new file mode 100644 index 00000000..70202223 --- /dev/null +++ b/docs/images/packet_diagrams/hds_animation.py @@ -0,0 +1,608 @@ +from __future__ import annotations + +import math +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +from incoming_wire import draw_rx_wire, draw_rx_wire_label + + +ROOT = Path(__file__).resolve().parent +OUTPUT_DIR = ROOT / "hds" + +WIDTH = 1180 +HEIGHT = 660 +SCALE = 2 +FRAMES = 136 +DURATION_MS = 55 + +TOTAL_BYTES = 128 +HEADER_BYTES = 64 +PAYLOAD_BYTES = 64 + +LAYOUT_SHIFT_Y = 42 + + +def dy(y: float) -> float: + return y + LAYOUT_SHIFT_Y + + +def shift_rect(rect: tuple[float, float, float, float]) -> tuple[float, float, float, float]: + x1, y1, x2, y2 = rect + return x1, y1 + LAYOUT_SHIFT_Y, x2, y2 + LAYOUT_SHIFT_Y + + +NIC_RECT = shift_rect((382, 234, 582, 392)) +HOST_RECT = shift_rect((772, 112, 1118, 252)) +KERNEL_WIDTH = 200 +KERNEL_CX = ((488 + 688) / 2 + (NIC_RECT[2] + HOST_RECT[0]) / 2) / 2 +KERNEL_BOX = shift_rect((KERNEL_CX - KERNEL_WIDTH / 2, 4, KERNEL_CX + KERNEL_WIDTH / 2, 82)) +GPU_RECT = shift_rect((772, 392, 1118, 552)) +HEADER_ROW = shift_rect((796, 168, 1092, 210)) +PAYLOAD_ROW = shift_rect((796, 468, 1092, 520)) + +TRANSPARENT = (0, 0, 0, 0) +GIF_TRANSPARENCY_INDEX = 255 + +ACCENTS = { + "nvidia": "#76b900", + "header": "#ffcf5a", + "payload": "#78e08f", + "host": "#9b8cff", + "gpu": "#76b900", + "kernel": "#3b82f6", + "ink": "#07111f", +} + +THEMES: dict[str, dict[str, str | None]] = { + "default": { + **ACCENTS, + "bg": None, + "panel": "#0d1b2e", + "panel_2": "#101f34", + "line": "#35516f", + "line_soft": "#20384f", + "text": "#f6fbff", + "muted": "#afc0cf", + "wire": "#6d7e92", + "white": "#ffffff", + "shadow": "#03101d", + "row_bg": "#061220", + "kernel_fill": "#0f2744", + "arrow": "#ffffff", + "stroke": "#ffffff", + "bar_glow": "#ffffff", + "bypass": "#afc0cf", + }, + "light": { + **ACCENTS, + "bg": "#ffffff", + "panel": "#f5f5f5", + "panel_2": "#eeeeee", + "line": "#1a1a1a", + "line_soft": "#cccccc", + "text": "#1a1a1a", + "muted": "#404040", + "wire": "#404040", + "white": "#ffffff", + "shadow": "#bdbdbd", + "row_bg": "#e8e8e8", + "kernel_fill": "#f0f4ff", + "arrow": "#1a1a1a", + "stroke": "#1a1a1a", + "bar_glow": "#cccccc", + "bypass": "#404040", + }, +} + +THEME_OUTPUT_SUFFIX = { + "default": "", + "light": "-light", +} + +COLORS = THEMES["default"] + + +def rgb(hex_color: str) -> tuple[int, int, int]: + hex_color = hex_color.strip("#") + return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + + +def rgba(hex_color: str, alpha: int = 255) -> tuple[int, int, int, int]: + r, g, b = rgb(hex_color) + return r, g, b, alpha + + +def font(size: int, *, bold: bool = False, mono: bool = False) -> ImageFont.FreeTypeFont: + if mono: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + "/usr/share/fonts/truetype/noto/NotoSansMono-Bold.ttf" if bold else "/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf", + "C:/Windows/Fonts/consolab.ttf" if bold else "C:/Windows/Fonts/consola.ttf", + ] + elif bold: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", + "C:/Windows/Fonts/segoeuib.ttf", + ] + else: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "C:/Windows/Fonts/segoeui.ttf", + ] + for path in candidates: + if Path(path).exists(): + return ImageFont.truetype(path, size * SCALE) + return ImageFont.load_default() + + +FONTS = { + "label": font(18, bold=True), + "small": font(14), + "tiny": font(12), + "chip": font(21, bold=True), +} + + +def s(value: float) -> int: + return int(round(value * SCALE)) + + +def pt(point: tuple[float, float]) -> tuple[int, int]: + return s(point[0]), s(point[1]) + + +def box(rect: tuple[float, float, float, float]) -> tuple[int, int, int, int]: + return tuple(s(v) for v in rect) + + +def text_size(draw: ImageDraw.ImageDraw, text: str, face: ImageFont.FreeTypeFont) -> tuple[int, int]: + bbox = draw.textbbox((0, 0), text, font=face) + return bbox[2] - bbox[0], bbox[3] - bbox[1] + + +def draw_text( + draw: ImageDraw.ImageDraw, + xy: tuple[float, float], + text: str, + face: ImageFont.FreeTypeFont, + fill: str | tuple[int, int, int, int] = COLORS["text"], + anchor: str = "la", +) -> None: + draw.text(pt(xy), text, font=face, fill=fill, anchor=anchor) + + +def centered_text( + draw: ImageDraw.ImageDraw, + rect: tuple[float, float, float, float], + text: str, + face: ImageFont.FreeTypeFont, + fill: str | tuple[int, int, int, int] = COLORS["text"], +) -> None: + x1, y1, x2, y2 = rect + draw.text((s((x1 + x2) / 2), s((y1 + y2) / 2)), text, font=face, fill=fill, anchor="mm") + + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + +def clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float: + return max(lo, min(hi, value)) + + +def smoothstep(t: float) -> float: + t = clamp(t) + return t * t * (3 - 2 * t) + + +def progress(frame: int, start: int, end: int) -> float: + return smoothstep((frame - start) / max(1, end - start)) + + +def bezier_point( + p0: tuple[float, float], + p1: tuple[float, float], + p2: tuple[float, float], + p3: tuple[float, float], + t: float, +) -> tuple[float, float]: + u = 1 - t + x = u**3 * p0[0] + 3 * u**2 * t * p1[0] + 3 * u * t**2 * p2[0] + t**3 * p3[0] + y = u**3 * p0[1] + 3 * u**2 * t * p1[1] + 3 * u * t**2 * p2[1] + t**3 * p3[1] + return x, y + + +def bezier_path(points: tuple[tuple[float, float], ...], steps: int = 80) -> list[tuple[float, float]]: + p0, p1, p2, p3 = points + return [bezier_point(p0, p1, p2, p3, i / steps) for i in range(steps + 1)] + + +def rectangular_path(waypoints: tuple[tuple[float, float], ...], steps: int = 14) -> list[tuple[float, float]]: + if len(waypoints) < 2: + return list(waypoints) + path: list[tuple[float, float]] = [] + for i in range(len(waypoints) - 1): + x0, y0 = waypoints[i] + x1, y1 = waypoints[i + 1] + for step in range(steps + 1): + if i > 0 and step == 0: + continue + t = step / steps + path.append((lerp(x0, x1, t), lerp(y0, y1, t))) + return path + + +def draw_polyline( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + width: int = 4, +) -> None: + draw.line([pt(p) for p in points], fill=color, width=s(width)) + + +def draw_dotted_segment( + draw: ImageDraw.ImageDraw, + start: tuple[float, float], + end: tuple[float, float], + color: str | tuple[int, int, int, int], + *, + width: int = 2, + dash: float = 7, + gap: float = 5, +) -> None: + x0, y0 = start + x1, y1 = end + length = math.hypot(x1 - x0, y1 - y0) + if length == 0: + return + dx = (x1 - x0) / length + dy = (y1 - y0) / length + pos = 0.0 + while pos < length: + seg_end = min(pos + dash, length) + draw.line( + (pt((x0 + dx * pos, y0 + dy * pos)), pt((x0 + dx * seg_end, y0 + dy * seg_end))), + fill=color, + width=s(width), + ) + pos += dash + gap + + +def draw_dotted_polyline( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + *, + width: int = 2, + dash: float = 8, + gap: float = 6, +) -> None: + if len(points) < 2: + return + drawing = True + remaining = dash + for i in range(len(points) - 1): + x0, y0 = points[i] + x1, y1 = points[i + 1] + seg_len = math.hypot(x1 - x0, y1 - y0) + if seg_len == 0: + continue + dx = (x1 - x0) / seg_len + dy = (y1 - y0) / seg_len + pos = 0.0 + while pos < seg_len: + step = min(remaining, seg_len - pos) + if drawing: + draw.line( + ( + pt((x0 + dx * pos, y0 + dy * pos)), + pt((x0 + dx * (pos + step), y0 + dy * (pos + step))), + ), + fill=color, + width=s(width), + ) + pos += step + remaining -= step + if remaining <= 0: + drawing = not drawing + remaining = dash if drawing else gap + + +def draw_arrow_head( + draw: ImageDraw.ImageDraw, + end: tuple[float, float], + prev: tuple[float, float], + color: str | tuple[int, int, int, int], + size: float = 12, +) -> None: + angle = math.atan2(end[1] - prev[1], end[0] - prev[0]) + wing = math.radians(28) + p1 = (end[0] - size * math.cos(angle - wing), end[1] - size * math.sin(angle - wing)) + p2 = (end[0] - size * math.cos(angle + wing), end[1] - size * math.sin(angle + wing)) + draw.polygon([pt(end), pt(p1), pt(p2)], fill=color) + + +def draw_arrow( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + width: int = 4, + arrow_size: float = 13, +) -> None: + draw_polyline(draw, points, color, width) + if len(points) >= 2: + prev_idx = max(0, len(points) - 5) + draw_arrow_head(draw, points[-1], points[prev_idx], color, arrow_size) + + +def draw_glow_line( + base: Image.Image, + points: list[tuple[float, float]], + color: str, + alpha: int, + width: int, +) -> None: + layer = Image.new("RGBA", base.size, (0, 0, 0, 0)) + d = ImageDraw.Draw(layer) + draw_polyline(d, points, rgba(color, alpha), width) + base.alpha_composite(layer) + + +def point_on_path(points: list[tuple[float, float]], t: float) -> tuple[float, float]: + t = clamp(t) + idx = t * (len(points) - 1) + i = int(idx) + j = min(i + 1, len(points) - 1) + local = idx - i + return lerp(points[i][0], points[j][0], local), lerp(points[i][1], points[j][1], local) + + +def rect_at_center(cx: float, cy: float, w: float, h: float) -> tuple[float, float, float, float]: + return cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2 + + +def draw_packet_bar( + draw: ImageDraw.ImageDraw, + x: float, + y: float, + w: float, + h: float, + alpha: int = 255, + label: bool = True, +) -> None: + colors = [COLORS["header"], COLORS["payload"]] + names = ["Header", "Payload"] + sizes = [HEADER_BYTES, PAYLOAD_BYTES] + draw.rounded_rectangle(box((x - 3, y - 3, x + w + 3, y + h + 3)), radius=s(11), fill=rgba(str(COLORS["bar_glow"]), 22 * alpha // 255)) + cursor = x + for i, (name, byte_count, color) in enumerate(zip(names, sizes, colors)): + seg_w = w * byte_count / TOTAL_BYTES + rect = (cursor, y, cursor + seg_w, y + h) + radius = 9 if i in (0, len(sizes) - 1) else 2 + draw.rounded_rectangle(box(rect), radius=s(radius), fill=rgba(color, alpha), outline=rgba(COLORS["ink"], alpha), width=s(2)) + if label and seg_w > 52: + centered_text(draw, rect, name, FONTS["tiny"], fill=rgba(COLORS["ink"], alpha)) + cursor += seg_w + + +def draw_memory_row( + draw: ImageDraw.ImageDraw, + rect: tuple[float, float, float, float], + color: str, + label: str, + fill_progress: float, + byte_text: str, +) -> None: + x1, y1, x2, y2 = rect + draw.rounded_rectangle(box(rect), radius=s(8), fill=rgba(COLORS["row_bg"], 235), outline=rgba(color, 145), width=s(2)) + if fill_progress > 0: + fw = max(14, (x2 - x1) * clamp(fill_progress)) + draw.rounded_rectangle(box((x1, y1, x1 + fw, y2)), radius=s(8), fill=rgba(color, 215)) + draw_text(draw, (x1 + 13, (y1 + y2) / 2), label, FONTS["small"], fill=COLORS["text"], anchor="lm") + draw_text(draw, (x2 - 12, (y1 + y2) / 2), byte_text, FONTS["small"], fill=COLORS["text"], anchor="rm") + + +def draw_packet_segment( + draw: ImageDraw.ImageDraw, + center: tuple[float, float], + width: float, + color: str, + label: str, + byte_text: str, + alpha: int = 255, +) -> None: + cx, cy = center + rect = rect_at_center(cx, cy, width, 34) + shadow = (rect[0] + 5, rect[1] + 6, rect[2] + 5, rect[3] + 6) + draw.rounded_rectangle(box(shadow), radius=s(9), fill=rgba(COLORS["shadow"], 80 * alpha // 255)) + draw.rounded_rectangle(box(rect), radius=s(9), fill=rgba(color, alpha), outline=rgba(str(COLORS["stroke"]), 70 * alpha // 255), width=s(1)) + draw_text(draw, (cx, cy - 3), label, FONTS["tiny"], fill=rgba(COLORS["ink"], alpha), anchor="mm") + draw_text(draw, (cx, cy + 10), byte_text, FONTS["tiny"], fill=rgba(COLORS["ink"], alpha), anchor="mm") + + +def draw_chip( + draw: ImageDraw.ImageDraw, + rect: tuple[float, float, float, float], + title: str, + subtitle: str, + accent: str, + pulse: float = 0.0, +) -> None: + x1, y1, x2, y2 = rect + glow_alpha = int(55 + 95 * pulse) + draw.rounded_rectangle(box((x1 - 10, y1 - 10, x2 + 10, y2 + 10)), radius=s(24), fill=rgba(accent, 16 + glow_alpha // 4)) + draw.rounded_rectangle(box(rect), radius=s(18), fill=COLORS["panel"], outline=rgba(accent, 190), width=s(3)) + for i in range(7): + y = lerp(y1 + 18, y2 - 18, i / 6) + draw.line((s(x1 - 14), s(y), s(x1), s(y)), fill=rgba(COLORS["line"], 220), width=s(3)) + draw.line((s(x2), s(y), s(x2 + 14), s(y)), fill=rgba(COLORS["line"], 220), width=s(3)) + for i in range(5): + x = lerp(x1 + 25, x2 - 25, i / 4) + draw.line((s(x), s(y1 - 12), s(x), s(y1)), fill=rgba(COLORS["line"], 220), width=s(3)) + draw.line((s(x), s(y2), s(x), s(y2 + 12)), fill=rgba(COLORS["line"], 220), width=s(3)) + centered_text(draw, (x1 + 18, y1 + 39, x2 - 18, y1 + 75), title, FONTS["chip"], fill=COLORS["text"]) + if subtitle: + centered_text(draw, (x1 + 20, y1 + 80, x2 - 20, y1 + 111), subtitle, FONTS["small"], fill=COLORS["muted"]) + draw.rounded_rectangle(box((x1 + 18, y1 + 82, x2 - 18, y1 + 110)), radius=s(8), fill=rgba(accent, 215)) + centered_text(draw, (x1 + 18, y1 + 82, x2 - 18, y1 + 110), "buffer splitting engine", FONTS["tiny"], fill=COLORS["ink"]) + + +def draw_wire(draw: ImageDraw.ImageDraw, frame: int) -> None: + y = dy(327) + draw_rx_wire_label(lambda xy, text: draw_text(draw, xy, text, FONTS["label"], fill=COLORS["text"]), y) + draw_rx_wire(draw, frame, 48, 48 + 85 * 3.55, y, COLORS["wire"], COLORS["header"], pt, s, rgba, box) + + +def draw_device_memory(draw: ImageDraw.ImageDraw, frame: int) -> None: + for rect, title, accent in ((HOST_RECT, "Host memory", COLORS["host"]), (GPU_RECT, "NVIDIA GPU memory", COLORS["gpu"])): + x1, y1, x2, y2 = rect + draw.rounded_rectangle(box(rect), radius=s(18), fill=COLORS["panel_2"], outline=rgba(accent, 170), width=s(3)) + draw_text(draw, (x1 + 22, y1 + 24), title, FONTS["label"], fill=COLORS["text"], anchor="lt") + + header_arrival = progress(frame, 74, 100) + payload_arrival = progress(frame, 80, 106) + draw_memory_row(draw, HEADER_ROW, COLORS["header"], "header", header_arrival, f"{HEADER_BYTES} B") + draw_memory_row(draw, PAYLOAD_ROW, COLORS["payload"], "payload", payload_arrival, f"{PAYLOAD_BYTES} B") + + +def draw_static_background(draw: ImageDraw.ImageDraw) -> None: + pass + + +def draw_kernel_bypass(draw: ImageDraw.ImageDraw) -> None: + nic_top = ((NIC_RECT[0] + NIC_RECT[2]) / 2, NIC_RECT[1]) + host_entry = ((HOST_RECT[0] + HOST_RECT[2]) / 2, HOST_RECT[1]) + bus_y = (KERNEL_BOX[1] + KERNEL_BOX[3]) / 2 + kernel_in = (KERNEL_BOX[0], bus_y) + kernel_out = (KERNEL_BOX[2], bus_y) + + stack_color = rgba(str(COLORS["bypass"]), 210) + to_kernel = rectangular_path((nic_top, (nic_top[0], bus_y), kernel_in)) + from_kernel = rectangular_path((kernel_out, (host_entry[0], bus_y), host_entry)) + draw_dotted_polyline(draw, to_kernel, stack_color, width=2, dash=10, gap=16) + draw_dotted_polyline(draw, from_kernel, stack_color, width=2, dash=10, gap=16) + + x1, y1, x2, y2 = KERNEL_BOX + draw.rounded_rectangle(box(KERNEL_BOX), radius=s(12), fill=rgba(COLORS["kernel_fill"], 235), outline=rgba(COLORS["kernel"], 210), width=s(2)) + centered_text(draw, (x1, y1 + 8, x2, y2 - 8), "Linux kernel", FONTS["label"], fill=COLORS["text"]) + + +def draw_dma_paths(base: Image.Image, draw: ImageDraw.ImageDraw, frame: int) -> dict[str, list[tuple[float, float]]]: + nic_out_x = NIC_RECT[2] + header_y = (HEADER_ROW[1] + HEADER_ROW[3]) / 2 + payload_y = (PAYLOAD_ROW[1] + PAYLOAD_ROW[3]) / 2 + host_in_x = HEADER_ROW[0] + bend_x = nic_out_x + 88 + + nic_header_y = NIC_RECT[1] + 34 + nic_payload_y = NIC_RECT[3] - 44 + + paths = { + "header": rectangular_path( + ( + (nic_out_x, nic_header_y), + (bend_x, nic_header_y), + (bend_x, header_y), + (host_in_x, header_y), + ) + ), + "payload": rectangular_path( + ( + (nic_out_x, nic_payload_y), + (bend_x, nic_payload_y), + (bend_x, payload_y), + (host_in_x, payload_y), + ) + ), + } + for key, color in (("header", COLORS["header"]), ("payload", COLORS["payload"])): + draw_arrow(draw, paths[key], rgba(str(COLORS["arrow"]), 130), 3, 12) + active = progress(frame, 52, 114) + if active > 0: + draw_glow_line(base, paths[key][: max(2, int(len(paths[key]) * active))], color, 75, 9) + return paths + + +def draw_flowing_segments(draw: ImageDraw.ImageDraw, frame: int, paths: dict[str, list[tuple[float, float]]]) -> None: + incoming = progress(frame, 4, 52) + if incoming < 1: + x = lerp(76, 333, incoming) + y = dy(306) + draw_packet_bar(draw, x, y, 224, 42, alpha=255) + return + + split_flash = 1 - progress(frame, 52, 66) + if split_flash > 0: + draw_packet_bar(draw, 407, dy(303), 134, 30, alpha=int(220 * split_flash), label=False) + + moves = ( + ("header", 60, 96, COLORS["header"], "header", f"{HEADER_BYTES} B", 110), + ("payload", 68, 106, COLORS["payload"], "payload", f"{PAYLOAD_BYTES} B", 138), + ) + for key, start, end, color, label, byte_text, width in moves: + p = progress(frame, start, end) + if 0 < p < 1: + draw_packet_segment(draw, point_on_path(paths[key], p), width, color, label, byte_text) + + +def rgba_frame_to_palette(frame: Image.Image) -> Image.Image: + rgba = frame.convert("RGBA") + alpha = rgba.getchannel("A") + rgb = Image.new("RGB", rgba.size, (0, 0, 0)) + rgb.paste(rgba, mask=alpha) + palette = rgb.quantize(colors=254, method=Image.Quantize.MEDIANCUT) + palette.paste(GIF_TRANSPARENCY_INDEX, mask=alpha.point(lambda a: 255 if a < 128 else 0)) + return palette + + +def render_frame(frame: int) -> Image.Image: + bg = COLORS.get("bg") + img = Image.new("RGBA", (WIDTH * SCALE, HEIGHT * SCALE), rgba(str(bg), 255) if bg else TRANSPARENT) + draw = ImageDraw.Draw(img) + draw_static_background(draw) + draw_wire(draw, frame) + draw_kernel_bypass(draw) + draw_device_memory(draw, frame) + paths = draw_dma_paths(img, draw, frame) + draw_chip(draw, NIC_RECT, "NVIDIA NIC", "", COLORS["nvidia"], pulse=progress(frame, 22, 50)) + draw_flowing_segments(draw, frame, paths) + return img.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS) + + +def render_theme(theme: str) -> None: + global COLORS + COLORS = THEMES[theme] + suffix = THEME_OUTPUT_SUFFIX[theme] + gif_path = OUTPUT_DIR / f"header-data-split{suffix}.gif" + poster_path = OUTPUT_DIR / f"header-data-split{suffix}-poster.png" + + frames = [render_frame(i) for i in range(FRAMES)] + gif_frames = [rgba_frame_to_palette(f) for f in frames] + gif_frames[0].save( + gif_path, + save_all=True, + append_images=gif_frames[1:], + optimize=True, + duration=DURATION_MS, + loop=0, + disposal=2, + transparency=GIF_TRANSPARENCY_INDEX, + ) + render_frame(118).save(poster_path, optimize=True) + print(f"Wrote {gif_path}") + print(f"Wrote {poster_path}") + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + for theme in THEMES: + render_theme(theme) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/images/packet_diagrams/incoming_wire.py b/docs/images/packet_diagrams/incoming_wire.py new file mode 100644 index 00000000..19e62a36 --- /dev/null +++ b/docs/images/packet_diagrams/incoming_wire.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import math +from typing import Callable + +from PIL import ImageDraw + +RX_WIRE_LABEL = "RX Packets over Wire" +WIRE_LABEL_OFFSET = 95.0 + +PtFn = Callable[[tuple[float, float]], tuple[int, int]] +ScaleFn = Callable[[float], int] +RgbaFn = Callable[[str, int], tuple[int, int, int, int]] +BoxFn = Callable[[tuple[float, float, float, float]], tuple[int, int, int, int]] + + +def center_wire_y(x: float, x0: float, y_center: float, frame: int) -> float: + i = (x - x0) / 3.55 + return y_center + math.sin((i + frame * 0.95) / 5.4) * 2.4 + + +def draw_rx_wire_label( + draw_text: Callable[..., None], + wire_y: float, + *, + x: float = 48.0, + text: str = RX_WIRE_LABEL, +) -> None: + draw_text((x, wire_y - WIRE_LABEL_OFFSET), text) + + +def draw_rx_wire( + draw: ImageDraw.ImageDraw, + frame: int, + x0: float, + x1: float, + y_center: float, + wire_color: str, + marker_color: str, + pt: PtFn, + s: ScaleFn, + rgba: RgbaFn, + box: BoxFn, + *, + ambient_markers: bool = True, +) -> None: + span = max(x1 - x0, 1.0) + count = max(20, int(span / 3.55) + 1) + for offset in (-13, 0, 13): + pts: list[tuple[float, float]] = [] + for i in range(count): + x = x0 + i * (span / max(1, count - 1)) + wave = math.sin((i + frame * 0.95) / 5.4) * 2.4 + pts.append((x, y_center + offset + wave)) + draw.line([pt(p) for p in pts], fill=wire_color, width=s(2)) + + if not ambient_markers: + return + loop = max(44.0, span - 44.0) + for i in range(5): + x = x0 + 22 + ((frame * 7 + i * 68) % loop) + cy = center_wire_y(x, x0, y_center, frame) + alpha = 160 - i * 16 + draw.ellipse(box((x - 4, cy - 4, x + 4, cy + 4)), fill=rgba(marker_color, alpha)) + + +def draw_rx_packet_marker( + draw: ImageDraw.ImageDraw, + frame: int, + cx: float, + wire_y: float, + wire_x0: float, + color: str, + rgba: RgbaFn, + box: BoxFn, + *, + radius: float = 5.0, + alpha: int = 255, + snap_to_wire: bool = True, + cy: float | None = None, +) -> None: + marker_y = center_wire_y(cx, wire_x0, wire_y, frame) if snap_to_wire else (cy if cy is not None else wire_y) + draw.ellipse( + box((cx - radius, marker_y - radius, cx + radius, marker_y + radius)), + fill=rgba(color, alpha), + ) + + +def path_lengths(points: list[tuple[float, float]]) -> tuple[list[float], float]: + lengths = [0.0] + for i in range(1, len(points)): + x0, y0 = points[i - 1] + x1, y1 = points[i] + lengths.append(lengths[-1] + math.hypot(x1 - x0, y1 - y0)) + return lengths, lengths[-1] + + +def path_point_and_tangent( + points: list[tuple[float, float]], + t: float, +) -> tuple[float, float, float]: + if len(points) < 2: + p = points[0] if points else (0.0, 0.0) + return p[0], p[1], 0.0 + t = max(0.0, min(1.0, t)) + lengths, total = path_lengths(points) + if total <= 0: + return points[0][0], points[0][1], 0.0 + target = t * total + for i in range(1, len(points)): + if lengths[i] >= target: + seg_len = lengths[i] - lengths[i - 1] + local = 0.0 if seg_len <= 0 else (target - lengths[i - 1]) / seg_len + x0, y0 = points[i - 1] + x1, y1 = points[i] + cx = x0 + (x1 - x0) * local + cy = y0 + (y1 - y0) * local + return cx, cy, math.atan2(y1 - y0, x1 - x0) + x0, y0 = points[-2] + x1, y1 = points[-1] + return x1, y1, math.atan2(y1 - y0, x1 - x0) + + +def wire_t_end(wire: list[tuple[float, float]], full: list[tuple[float, float]]) -> float: + _, wire_total = path_lengths(wire) + _, full_total = path_lengths(full) + if full_total <= 0: + return 1.0 + return min(1.0, wire_total / full_total) diff --git a/docs/images/packet_diagrams/reorder/packet-reorder-light-poster.png b/docs/images/packet_diagrams/reorder/packet-reorder-light-poster.png new file mode 100644 index 00000000..bc4e44d2 Binary files /dev/null and b/docs/images/packet_diagrams/reorder/packet-reorder-light-poster.png differ diff --git a/docs/images/packet_diagrams/reorder/packet-reorder-light.gif b/docs/images/packet_diagrams/reorder/packet-reorder-light.gif new file mode 100644 index 00000000..fc07e66d Binary files /dev/null and b/docs/images/packet_diagrams/reorder/packet-reorder-light.gif differ diff --git a/docs/images/packet_diagrams/reorder/packet-reorder-poster.png b/docs/images/packet_diagrams/reorder/packet-reorder-poster.png new file mode 100644 index 00000000..2125127f Binary files /dev/null and b/docs/images/packet_diagrams/reorder/packet-reorder-poster.png differ diff --git a/docs/images/packet_diagrams/reorder/packet-reorder.gif b/docs/images/packet_diagrams/reorder/packet-reorder.gif new file mode 100644 index 00000000..19be692f Binary files /dev/null and b/docs/images/packet_diagrams/reorder/packet-reorder.gif differ diff --git a/docs/images/packet_diagrams/reorder_animation.py b/docs/images/packet_diagrams/reorder_animation.py new file mode 100644 index 00000000..07e3a365 --- /dev/null +++ b/docs/images/packet_diagrams/reorder_animation.py @@ -0,0 +1,741 @@ +from __future__ import annotations + +import math +from dataclasses import dataclass +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +from incoming_wire import center_wire_y, draw_rx_wire, draw_rx_wire_label, path_point_and_tangent, wire_t_end + + +ROOT = Path(__file__).resolve().parent +OUTPUT_DIR = ROOT / "reorder" + +WIDTH = 1180 +HEIGHT = 660 +SCALE = 2 +DURATION_MS = 55 + +PACKET_COUNT = 5 +ARRIVAL_ORDER = (3, 1, 5, 2, 4) +WIRE_FRAMES = 32 +KERNEL_TRAVEL = 24 +SHUFFLE_FRAMES = 18 +PACKET_GAP = 5 + +NIC_RECT = (300, 250, 500, 410) +REORDER_RECT = (640, 168, 860, 528) +QUEUE_COL = (668, 248, 832, 512) + +WIRE_Y = 330 + +TRANSPARENT = (0, 0, 0, 0) +GIF_TRANSPARENCY_INDEX = 255 + +PACKET_GRAD_START = "#59d4ff" +PACKET_GRAD_END = "#9b8cff" + +ACCENTS = { + "nvidia": "#76b900", + "reorder": "#76b900", + "ink": "#07111f", +} + +THEMES: dict[str, dict[str, str | None]] = { + "default": { + **ACCENTS, + "bg": None, + "panel": "#0d1b2e", + "panel_2": "#101f34", + "line": "#35516f", + "text": "#f6fbff", + "muted": "#afc0cf", + "wire": "#6d7e92", + "row_bg": "#061220", + "stroke": "#ffffff", + "slot_empty": "#64748b", + "arrow": "#ffffff", + }, + "light": { + **ACCENTS, + "bg": "#ffffff", + "panel": "#f5f5f5", + "panel_2": "#eeeeee", + "line": "#1a1a1a", + "text": "#1a1a1a", + "muted": "#404040", + "wire": "#404040", + "row_bg": "#e8e8e8", + "stroke": "#1a1a1a", + "slot_empty": "#9ca3af", + "arrow": "#1a1a1a", + }, +} + +THEME_OUTPUT_SUFFIX = { + "default": "", + "light": "-light", +} + +COLORS = THEMES["default"] + + +@dataclass(frozen=True) +class PacketSpec: + num: int + start: int + decide: int + kernel_end: int + shuffle_end: int + + +NIC = NIC_RECT +REORDER = REORDER_RECT +QUEUE_COL_R = QUEUE_COL +NIC_CX = (NIC[0] + NIC[2]) / 2 +NIC_CY = (NIC[1] + NIC[3]) / 2 +REORDER_CY = (REORDER[1] + REORDER[3]) / 2 +REORDER_ENTRY = (REORDER[0], NIC_CY) + + +def rgb(hex_color: str) -> tuple[int, int, int]: + hex_color = hex_color.strip("#") + return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + + +def rgb_to_hex(r: int, g: int, b: int) -> str: + return f"#{r:02x}{g:02x}{b:02x}" + + +def rgba(hex_color: str, alpha: int = 255) -> tuple[int, int, int, int]: + r, g, b = rgb(hex_color) + return r, g, b, alpha + + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t + + +def lerp_hex(a: str, b: str, t: float) -> str: + r1, g1, b1 = rgb(a) + r2, g2, b2 = rgb(b) + return rgb_to_hex( + int(lerp(r1, r2, t)), + int(lerp(g1, g2, t)), + int(lerp(b1, b2, t)), + ) + + +def clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float: + return max(lo, min(hi, value)) + + +def progress_linear(frame: int, start: int, end: int) -> float: + return clamp((frame - start) / max(1, end - start)) + + +def font(size: int, *, bold: bool = False) -> ImageFont.FreeTypeFont: + if bold: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", + ] + else: + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + ] + for path in candidates: + if Path(path).exists(): + return ImageFont.truetype(path, size * SCALE) + return ImageFont.load_default() + + +FONTS = { + "label": font(18, bold=True), + "small": font(14), + "tiny": font(12), + "chip": font(21, bold=True), + "badge": font(15, bold=True), + "queue": font(18, bold=True), + "packet": font(12), +} + + +def s(value: float) -> int: + return int(round(value * SCALE)) + + +def pt(point: tuple[float, float]) -> tuple[int, int]: + return s(point[0]), s(point[1]) + + +def box(rect: tuple[float, float, float, float]) -> tuple[int, int, int, int]: + return tuple(s(v) for v in rect) + + +def draw_text( + draw: ImageDraw.ImageDraw, + xy: tuple[float, float], + text: str, + face: ImageFont.FreeTypeFont, + fill: str | tuple[int, int, int, int] = COLORS["text"], + anchor: str = "la", +) -> None: + draw.text(pt(xy), text, font=face, fill=fill, anchor=anchor) + + +def centered_text( + draw: ImageDraw.ImageDraw, + rect: tuple[float, float, float, float], + text: str, + face: ImageFont.FreeTypeFont, + fill: str | tuple[int, int, int, int] = COLORS["text"], +) -> None: + x1, y1, x2, y2 = rect + draw.text((s((x1 + x2) / 2), s((y1 + y2) / 2)), text, font=face, fill=fill, anchor="mm") + + +def rectangular_path(waypoints: tuple[tuple[float, float], ...], steps: int = 28) -> list[tuple[float, float]]: + if len(waypoints) < 2: + return list(waypoints) + path: list[tuple[float, float]] = [] + for i in range(len(waypoints) - 1): + x0, y0 = waypoints[i] + x1, y1 = waypoints[i + 1] + for step in range(steps + 1): + if i > 0 and step == 0: + continue + t = step / steps + path.append((lerp(x0, x1, t), lerp(y0, y1, t))) + return path + + +def path_lengths(points: list[tuple[float, float]]) -> tuple[list[float], float]: + lengths = [0.0] + for i in range(1, len(points)): + x0, y0 = points[i - 1] + x1, y1 = points[i] + lengths.append(lengths[-1] + math.hypot(x1 - x0, y1 - y0)) + return lengths, lengths[-1] + + +def draw_polyline( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + width: int = 4, +) -> None: + draw.line([pt(p) for p in points], fill=color, width=s(width)) + + +def draw_glow_line( + base: Image.Image, + points: list[tuple[float, float]], + color: str, + alpha: int, + width: int, +) -> None: + layer = Image.new("RGBA", base.size, (0, 0, 0, 0)) + d = ImageDraw.Draw(layer) + draw_polyline(d, points, rgba(color, alpha), width) + base.alpha_composite(layer) + + +def draw_arrow_head( + draw: ImageDraw.ImageDraw, + end: tuple[float, float], + prev: tuple[float, float], + color: str | tuple[int, int, int, int], + size: float = 12, +) -> None: + angle = math.atan2(end[1] - prev[1], end[0] - prev[0]) + wing = math.radians(28) + p1 = (end[0] - size * math.cos(angle - wing), end[1] - size * math.sin(angle - wing)) + p2 = (end[0] - size * math.cos(angle + wing), end[1] - size * math.sin(angle + wing)) + draw.polygon([pt(end), pt(p1), pt(p2)], fill=color) + + +def draw_arrow( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + width: int = 4, + arrow_size: float = 13, +) -> None: + draw_polyline(draw, points, color, width) + if len(points) >= 2: + prev_idx = max(0, len(points) - 5) + draw_arrow_head(draw, points[-1], points[prev_idx], color, arrow_size) + + +def point_on_path_distance(points: list[tuple[float, float]], dist: float) -> tuple[float, float]: + if len(points) < 2: + return points[0] if points else (0.0, 0.0) + lengths, total = path_lengths(points) + if total <= 0: + return points[0] + target = clamp(dist, 0.0, total) + for i in range(1, len(points)): + if lengths[i] >= target: + seg_len = lengths[i] - lengths[i - 1] + local = 0.0 if seg_len <= 0 else (target - lengths[i - 1]) / seg_len + x0, y0 = points[i - 1] + x1, y1 = points[i] + return lerp(x0, x1, local), lerp(y0, y1, local) + return points[-1] + + +def draw_path_arrows( + draw: ImageDraw.ImageDraw, + points: list[tuple[float, float]], + color: str | tuple[int, int, int, int], + *, + width: int = 3, + arrow_size: float = 11, + spacing: float = 34, +) -> None: + if len(points) < 2: + return + _, total = path_lengths(points) + draw_polyline(draw, points, color, width) + dist = spacing * 0.6 + while dist < total - arrow_size: + tip = point_on_path_distance(points, dist) + prev = point_on_path_distance(points, max(0.0, dist - 10)) + draw_arrow_head(draw, tip, prev, color, arrow_size) + dist += spacing + if total > arrow_size: + draw_arrow_head(draw, points[-1], points[-2], color, arrow_size + 2) + + +def draw_glow_arrows( + base: Image.Image, + points: list[tuple[float, float]], + color: str, + alpha: int, + width: int, +) -> None: + layer = Image.new("RGBA", base.size, (0, 0, 0, 0)) + d = ImageDraw.Draw(layer) + draw_path_arrows(d, points, rgba(color, alpha), width=width, arrow_size=11, spacing=32) + base.alpha_composite(layer) + + +def queue_row_layout() -> list[tuple[float, float, float, float]]: + x1, y1, x2, y2 = QUEUE_COL_R + cx = (x1 + x2) / 2 + pad_y = 6 + gap = 6 + inner_h = y2 - y1 - 2 * pad_y + chip_h = (inner_h - gap * (PACKET_COUNT - 1)) / PACKET_COUNT + chip_w = x2 - x1 - 8 + rows: list[tuple[float, float, float, float]] = [] + y = y1 + pad_y + chip_h / 2 + for _ in range(PACKET_COUNT): + rows.append((cx, y, chip_w, chip_h)) + y += chip_h + gap + return rows + + +QUEUE_ROWS = queue_row_layout() + + +def queue_row_pos(row: int) -> tuple[float, float, float, float]: + return QUEUE_ROWS[row] + + +def completed_nums(frame: int) -> list[int]: + return sorted(p.num for p in PACKETS if frame >= p.shuffle_end) + + +def shuffling_packet(frame: int) -> PacketSpec | None: + for pkt in PACKETS: + if pkt.kernel_end <= frame < pkt.shuffle_end: + return pkt + return None + + +def queue_display(frame: int) -> list[int | None]: + nums = completed_nums(frame) + row: list[int | None] = [None] * PACKET_COUNT + for i, num in enumerate(nums[:PACKET_COUNT]): + row[i] = num + return row + + +def packet_label(num: int) -> str: + return f"packet {num}" + + +def before_placement(pkt: PacketSpec) -> list[int]: + return sorted(p.num for p in PACKETS if p.shuffle_end <= pkt.kernel_end) + + +def after_placement(pkt: PacketSpec) -> list[int]: + return sorted(before_placement(pkt) + [pkt.num]) + + +def placed_packet_position(frame: int, num: int) -> tuple[float, float, float, float] | None: + inflight = shuffling_packet(frame) + if inflight is None: + nums = completed_nums(frame) + if num not in nums: + return None + return queue_row_pos(nums.index(num)) + + before = before_placement(inflight) + if num not in before: + return None + + after = after_placement(inflight) + t = progress_linear(frame, inflight.kernel_end, inflight.shuffle_end) + old_row = before.index(num) + new_row = after.index(num) + cx, cy_old, chip_w, chip_h = queue_row_pos(old_row) + _, cy_new, _, _ = queue_row_pos(new_row) + cy = lerp(cy_old, cy_new, t) + return cx, cy, chip_w, chip_h + + +def shuffle_entry_for_row(row: int) -> tuple[float, float]: + _, cy, _, _ = queue_row_pos(row) + return REORDER[0] + 12, cy + + +def build_paths() -> dict[str, list[tuple[float, float]]]: + wire = rectangular_path(((48, WIRE_Y), (NIC[0], WIRE_Y))) + nic_route = rectangular_path(((NIC[2], NIC_CY), REORDER_ENTRY)) + to_reorder = rectangular_path( + ( + (48, WIRE_Y), + (NIC[0], WIRE_Y), + (NIC[2], NIC_CY), + REORDER_ENTRY, + ) + ) + return { + "wire": wire, + "to_reorder": to_reorder, + "nic_route": nic_route, + } + + +PATHS = build_paths() + + +def make_packets() -> tuple[PacketSpec, ...]: + packets: list[PacketSpec] = [] + cursor = 8 + for num in ARRIVAL_ORDER: + decide = cursor + WIRE_FRAMES + kernel_end = decide + KERNEL_TRAVEL + shuffle_end = kernel_end + SHUFFLE_FRAMES + packets.append(PacketSpec(num, cursor, decide, kernel_end, shuffle_end)) + cursor = shuffle_end + PACKET_GAP + return tuple(packets) + + +PACKETS = make_packets() +FRAMES = max(360, PACKETS[-1].shuffle_end + 40) + + +def active_packet(frame: int) -> PacketSpec | None: + for pkt in PACKETS: + if pkt.start <= frame < pkt.shuffle_end: + return pkt + return None + + +def nic_pulse(frame: int, pkt: PacketSpec) -> float: + return max( + progress_linear(frame, pkt.decide, pkt.decide + 6), + 1.0 - progress_linear(frame, pkt.decide + 8, pkt.decide + 14), + ) + + +def packet_base_color(num: int) -> str: + if PACKET_COUNT <= 1: + return PACKET_GRAD_START + t = (num - 1) / (PACKET_COUNT - 1) + return lerp_hex(PACKET_GRAD_START, PACKET_GRAD_END, t) + + +def packet_gradient_ends(num: int) -> tuple[str, str]: + base = packet_base_color(num) + return lerp_hex(base, "#ffffff", 0.22), lerp_hex(base, PACKET_GRAD_END, 0.3) + + +def point_on_path(points: list[tuple[float, float]], t: float) -> tuple[float, float]: + if len(points) < 2: + return points[0] if points else (0.0, 0.0) + t = clamp(t) + lengths, total = path_lengths(points) + if total <= 0: + return points[0] + target = t * total + for i in range(1, len(points)): + if lengths[i] >= target: + seg_len = lengths[i] - lengths[i - 1] + local = 0.0 if seg_len <= 0 else (target - lengths[i - 1]) / seg_len + x0, y0 = points[i - 1] + x1, y1 = points[i] + return lerp(x0, x1, local), lerp(y0, y1, local) + return points[-1] + + +def packet_screen_pos(frame: int, pkt: PacketSpec) -> tuple[float, float, float] | None: + if frame < pkt.start or frame >= pkt.shuffle_end: + return None + width = 72.0 + if frame < pkt.decide: + t = progress_linear(frame, pkt.start, pkt.decide) + cx, cy = point_on_path(PATHS["to_reorder"], t * 0.55) + return cx, cy, width + if frame < pkt.kernel_end: + t = progress_linear(frame, pkt.decide, pkt.kernel_end) + cx, cy = point_on_path(PATHS["to_reorder"], lerp(0.55, 1.0, t)) + return cx, cy, width + t = progress_linear(frame, pkt.kernel_end, pkt.shuffle_end) + row = after_placement(pkt).index(pkt.num) + sx, sy = shuffle_entry_for_row(row) + tx, ty, tw, _ = queue_row_pos(row) + return lerp(sx, tx, t), ty, lerp(72.0, tw, t) + + +def received_count(frame: int) -> int: + return sum(1 for p in PACKETS if frame >= p.decide) + + +def draw_gradient_packet( + base: Image.Image, + draw: ImageDraw.ImageDraw, + cx: float, + cy: float, + width: float, + num: int, + alpha: int = 255, + *, + label: str | None = None, + height: float = 28, +) -> None: + h = height + x1, y1 = cx - width / 2, cy - h / 2 + x2, y2 = cx + width / 2, cy + h / 2 + radius = s(6) + left, right = packet_gradient_ends(num) + accent = packet_base_color(num) + + chip = Image.new("RGBA", (max(1, s(x2 - x1)), max(1, s(y2 - y1))), (0, 0, 0, 0)) + cd = ImageDraw.Draw(chip) + inset = s(1) + cw, ch = chip.size + steps = 16 + for i in range(steps): + t0 = i / steps + t1 = (i + 1) / steps + color = lerp_hex(left, right, (t0 + t1) / 2) + sx1 = int(inset + (cw - 2 * inset) * t0) + sx2 = int(inset + (cw - 2 * inset) * t1) + cd.rectangle((sx1, inset, sx2, ch - inset), fill=rgba(color, alpha)) + mask = Image.new("L", chip.size, 0) + md = ImageDraw.Draw(mask) + md.rounded_rectangle((0, 0, cw - 1, ch - 1), radius=radius, fill=255) + chip.putalpha(mask) + base.paste(chip, (s(x1), s(y1)), chip) + draw.rounded_rectangle( + box((x1, y1, x2, y2)), + radius=radius, + outline=rgba(accent, min(255, 170 + alpha // 3)), + width=s(2), + ) + if label: + if label.startswith("packet "): + face = FONTS["packet"] + elif len(label) > 8: + face = FONTS["tiny"] + elif height > 30: + face = FONTS["queue"] + else: + face = FONTS["tiny"] + centered_text(draw, (x1, y1, x2, y2), label, face, fill=rgba(COLORS["ink"], alpha)) + + +def draw_wires(draw: ImageDraw.ImageDraw, frame: int) -> None: + draw_rx_wire_label(lambda xy, text: draw_text(draw, xy, text, FONTS["label"], fill=COLORS["text"]), WIRE_Y) + draw_rx_wire(draw, frame, 48, NIC[0], WIRE_Y, str(COLORS["wire"]), PACKET_GRAD_START, pt, s, rgba, box) + + +def draw_static_route(draw: ImageDraw.ImageDraw) -> None: + draw_arrow(draw, PATHS["nic_route"], rgba(COLORS["reorder"], 255), 3, 12) + + +def draw_sequence_queue(base: Image.Image, draw: ImageDraw.ImageDraw, frame: int) -> None: + inflight = shuffling_packet(frame) + + if inflight is None: + row = queue_display(frame) + for idx, value in enumerate(row): + cx, cy, chip_w, chip_h = queue_row_pos(idx) + if value is None: + centered_text(draw, (cx - chip_w / 2, cy - chip_h / 2, cx + chip_w / 2, cy + chip_h / 2), "_", FONTS["queue"], fill=COLORS["slot_empty"]) + continue + draw_gradient_packet(base, draw, cx, cy, chip_w, value, label=packet_label(value), height=chip_h) + return + + after = after_placement(inflight) + before = before_placement(inflight) + + for num in before: + pos = placed_packet_position(frame, num) + if pos is None: + continue + cx, cy, chip_w, chip_h = pos + draw_gradient_packet(base, draw, cx, cy, chip_w, num, label=packet_label(num), height=chip_h) + + for idx in range(len(after), PACKET_COUNT): + cx, cy, chip_w, chip_h = queue_row_pos(idx) + centered_text(draw, (cx - chip_w / 2, cy - chip_h / 2, cx + chip_w / 2, cy + chip_h / 2), "_", FONTS["queue"], fill=COLORS["slot_empty"]) + + +def draw_reorder_panel(base: Image.Image, draw: ImageDraw.ImageDraw, frame: int) -> None: + x1, y1, _, _ = REORDER + shuffling = any(pkt.kernel_end <= frame < pkt.shuffle_end for pkt in PACKETS) + pulse = 0.55 if shuffling else 0.0 + glow = int(40 + 55 * pulse) + draw.rounded_rectangle(box(REORDER), radius=s(18), fill=COLORS["panel_2"], outline=rgba(COLORS["reorder"], 170 + glow), width=s(3)) + draw_text(draw, (x1 + 14, y1 + 16), "GPU reorder", FONTS["label"], fill=COLORS["text"], anchor="lt") + draw_text(draw, (x1 + 14, y1 + 40), "reorder kernel", FONTS["small"], fill=COLORS["muted"], anchor="lt") + draw_sequence_queue(base, draw, frame) + + +def draw_nic_chip(draw: ImageDraw.ImageDraw, frame: int) -> None: + x1, y1, x2, y2 = NIC + pkt = active_packet(frame) + tracking = pkt is not None and pkt.decide <= frame < pkt.kernel_end + pulse = nic_pulse(frame, pkt) if tracking and pkt else 0.0 + accent = COLORS["nvidia"] + + glow_alpha = int(55 + 120 * pulse) if tracking else 55 + draw.rounded_rectangle(box((x1 - 10, y1 - 10, x2 + 10, y2 + 10)), radius=s(24), fill=rgba(accent, 16 + glow_alpha // 3)) + draw.rounded_rectangle(box(NIC), radius=s(18), fill=COLORS["panel"], outline=rgba(accent, 190 + int(65 * pulse)), width=s(3 + int(2 * pulse))) + for i in range(7): + y = lerp(y1 + 18, y2 - 18, i / 6) + draw.line((s(x1 - 14), s(y), s(x1), s(y)), fill=rgba(COLORS["line"], 220), width=s(3)) + draw.line((s(x2), s(y), s(x2 + 14), s(y)), fill=rgba(COLORS["line"], 220), width=s(3)) + for i in range(5): + x = lerp(x1 + 25, x2 - 25, i / 4) + draw.line((s(x), s(y1 - 12), s(x), s(y1)), fill=rgba(COLORS["line"], 220), width=s(3)) + draw.line((s(x), s(y2), s(x), s(y2 + 12)), fill=rgba(COLORS["line"], 220), width=s(3)) + centered_text(draw, (x1 + 18, y1 + 38, x2 - 18, y1 + 75), "NVIDIA NIC", FONTS["chip"], fill=COLORS["text"]) + + pill = (x1 + 18, y1 + 82, x2 - 18, y1 + 110) + if tracking and pkt: + pill_text = f"sequence {pkt.num}" + pill_accent = packet_base_color(pkt.num) + pill_ink = "#ffffff" + else: + count = received_count(frame) + pill_text = f"received: {count}" if count else "sequence —" + pill_accent = COLORS["nvidia"] + pill_ink = COLORS["ink"] + draw.rounded_rectangle(box(pill), radius=s(8), fill=rgba(pill_accent, 180 + int(60 * pulse)), outline=rgba(pill_accent, 220), width=s(2)) + centered_text(draw, pill, pill_text, FONTS["badge"], fill=pill_ink) + + +def incoming_path_t(frame: int, pkt: PacketSpec) -> float: + if frame < pkt.decide: + return progress_linear(frame, pkt.start, pkt.decide) * 0.55 + return lerp(0.55, 1.0, progress_linear(frame, pkt.decide, pkt.kernel_end)) + + +def draw_flowing_packets(base: Image.Image, draw: ImageDraw.ImageDraw, frame: int) -> None: + for pkt in PACKETS: + if frame < pkt.start or frame >= pkt.shuffle_end: + continue + if frame < pkt.kernel_end: + t = incoming_path_t(frame, pkt) + on_wire = t <= wire_t_end(PATHS["wire"], PATHS["to_reorder"]) + cx, cy, _ = path_point_and_tangent(PATHS["to_reorder"], t) + if on_wire: + cy = center_wire_y(cx, 48, WIRE_Y, frame) + draw_gradient_packet(base, draw, cx, cy, 92, pkt.num, label=packet_label(pkt.num), height=32) + continue + pos = packet_screen_pos(frame, pkt) + if pos is None: + continue + cx, cy, w = pos + _, _, _, chip_h = queue_row_pos(after_placement(pkt).index(pkt.num)) + draw_gradient_packet(base, draw, cx, cy, w, pkt.num, label=packet_label(pkt.num), height=chip_h) + + +def draw_route_glow(base: Image.Image, frame: int) -> None: + pkt = active_packet(frame) + if pkt and pkt.decide <= frame < pkt.kernel_end: + glow = progress_linear(frame, pkt.decide, pkt.kernel_end) + draw_glow_line(base, PATHS["nic_route"], COLORS["reorder"], int(50 + 40 * glow), 7) + elif pkt and pkt.kernel_end <= frame < pkt.shuffle_end: + glow = progress_linear(frame, pkt.kernel_end, pkt.shuffle_end) + row = after_placement(pkt).index(pkt.num) + sx, sy = shuffle_entry_for_row(row) + tx, ty, _, _ = queue_row_pos(row) + draw_glow_line(base, [(sx, sy), (tx, ty)], COLORS["reorder"], int(50 + 40 * glow), 5) + + +def rgba_frame_to_palette(frame: Image.Image) -> Image.Image: + rgba_img = frame.convert("RGBA") + alpha = rgba_img.getchannel("A") + rgb_img = Image.new("RGB", rgba_img.size, (0, 0, 0)) + rgb_img.paste(rgba_img, mask=alpha) + palette = rgb_img.quantize(colors=254, method=Image.Quantize.MEDIANCUT) + palette.paste(GIF_TRANSPARENCY_INDEX, mask=alpha.point(lambda a: 255 if a < 128 else 0)) + return palette + + +def render_frame(frame: int) -> Image.Image: + bg = COLORS.get("bg") + img = Image.new("RGBA", (WIDTH * SCALE, HEIGHT * SCALE), rgba(str(bg), 255) if bg else TRANSPARENT) + draw = ImageDraw.Draw(img) + draw_wires(draw, frame) + draw_static_route(draw) + draw_reorder_panel(img, draw, frame) + draw_nic_chip(draw, frame) + draw_route_glow(img, frame) + draw_flowing_packets(img, draw, frame) + return img.resize((WIDTH, HEIGHT), Image.Resampling.LANCZOS) + + +def render_theme(theme: str) -> None: + global COLORS + COLORS = THEMES[theme] + suffix = THEME_OUTPUT_SUFFIX[theme] + gif_path = OUTPUT_DIR / f"packet-reorder{suffix}.gif" + poster_path = OUTPUT_DIR / f"packet-reorder{suffix}-poster.png" + + frames = [render_frame(i) for i in range(FRAMES)] + gif_frames = [rgba_frame_to_palette(f) for f in frames] + gif_frames[0].save( + gif_path, + save_all=True, + append_images=gif_frames[1:], + optimize=True, + duration=DURATION_MS, + loop=0, + disposal=2, + transparency=GIF_TRANSPARENCY_INDEX, + ) + poster_frame = min(FRAMES - 1, PACKETS[-1].shuffle_end + 12) + render_frame(poster_frame).save(poster_path, optimize=True) + print(f"Wrote {gif_path}") + print(f"Wrote {poster_path}") + + +def main() -> None: + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + for theme in THEMES: + render_theme(theme) + + +if __name__ == "__main__": + main()