| """Turn an ``ArchModel`` / ``ArchDiff`` into positioned boxes. |
| |
| Layout is deterministic (stable ordering, integer coordinates) so the emitted SVG is |
| diff-able in git. The visual vocabulary is *standardized*: a given component kind always |
| maps to the same shape class and palette slot, mirroring the library's "standardize, don't |
| abstract" tenet -- an attention block looks the same across every model's diagram. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
|
|
| from .engine import text_width |
| from .introspect import ArchModel |
| from .modular import ArchDiff, ClassChange |
|
|
|
|
| _PARENT_CACHE: dict = {} |
|
|
|
|
| def _parent_arch(model_type): |
| if model_type not in _PARENT_CACHE: |
| try: |
| from .introspect import introspect |
|
|
| _PARENT_CACHE[model_type] = introspect(model_type) |
| except Exception: |
| _PARENT_CACHE[model_type] = None |
| return _PARENT_CACHE[model_type] |
|
|
|
|
| def _collect_submodule_names(nodes, out: set): |
| for nd in nodes: |
| out.add(nd["name"]) |
| if nd.get("children"): |
| _collect_submodule_names(nd["children"], out) |
|
|
|
|
| |
| KIND_CLASS = { |
| "embedding": "c-embed", |
| "attention": "c-attn", |
| "mamba": "c-mamba", |
| "linear_attention": "c-linattn", |
| "recurrent": "c-recur", |
| "moe": "c-moe", |
| "mlp": "c-mlp", |
| "norm": "c-norm", |
| "head": "c-head", |
| "config": "c-config", |
| "rope": "c-rope", |
| "layer": "c-layer", |
| } |
|
|
| |
| CHANGE_CLASS = {"added": "ch-added", "overridden": "ch-over", "deleted": "ch-deleted", None: ""} |
|
|
|
|
| @dataclass |
| class Box: |
| x: int |
| y: int |
| w: int |
| h: int |
| label: str |
| cls: str = "" |
| sublabels: list[str] = field(default_factory=list) |
| badge: str | None = None |
| ghost: bool = False |
| change: str | None = None |
| title: str | None = None |
| shape: str = "rect" |
| small: bool = False |
| glyph: str | None = None |
| grid: list[list[int]] | None = None |
| grid_split: int | None = None |
|
|
|
|
| @dataclass |
| class Arrow: |
| """A poly-line connector. ``points`` are (x, y) pairs; rendered with an arrowhead.""" |
|
|
| points: list[tuple[int, int]] |
| cls: str = "flow" |
| label: str | None = None |
| dashed: bool = False |
|
|
|
|
| @dataclass |
| class Diagram: |
| width: int |
| height: int |
| boxes: list[Box] |
| title: str |
| subtitle: str = "" |
| legend: list[tuple[str, str]] = field(default_factory=list) |
| facts: list[tuple[str, str]] = field(default_factory=list) |
| changes: list[tuple[str, str, str]] = field(default_factory=list) |
| arrows: list[Arrow] = field(default_factory=list) |
| mode: str = "full" |
| spine: bool = True |
|
|
|
|
| |
| W = 920 |
| COL_X = 150 |
| COL_W = 420 |
| PAD = 28 |
| ROW_H = 30 |
|
|
|
|
| def _fmt_val(v) -> str: |
| """Compactly format a config value -- including sequence-valued fields. |
| |
| Some (often hybrid) models carry per-layer lists for ``intermediate_size``, |
| ``head_dim`` or ``sliding_window``. We collapse those to ``[x]×N`` (all equal) or |
| ``[a … z] (N)`` rather than dumping the whole list. No per-model handling. |
| """ |
| if v is None: |
| return "—" |
| if isinstance(v, (list, tuple)): |
| if not v: |
| return "[]" |
| if len(set(v)) == 1: |
| return f"[{_fmt_val(v[0])}]×{len(v)}" |
| return f"[{_fmt_val(v[0])} … {_fmt_val(v[-1])}] ({len(v)})" |
| if isinstance(v, bool): |
| return str(v) |
| if isinstance(v, int): |
| return f"{v:,}" |
| return str(v) |
|
|
|
|
| def _facts(am: ArchModel) -> list[tuple[str, str]]: |
| fmt = _fmt_val |
|
|
| mid = am.checkpoint or am.model |
| if len(mid) > 30: |
| mid = mid[:29] + "…" |
| rows = [ |
| ("model id", mid), |
| ("model_type", am.model_type or "—"), |
| ("config", am.config_class or "—"), |
| ("decoder", am.decoder_kind), |
| ("layers", fmt(am.num_layers)), |
| ("hidden", fmt(am.hidden_size)), |
| ("intermediate", fmt(am.intermediate_size)), |
| ("heads / kv", f"{am.num_attention_heads} / {am.num_kv_heads}"), |
| ("attention", am.attn_variant or "—"), |
| ("norm", am.norm_type or "—"), |
| ("positional", am.positional or "—"), |
| ("vocab", fmt(am.vocab_size)), |
| ("max_pos", fmt(am.max_position_embeddings)), |
| ] |
| if am.sliding_window: |
| rows.append(("sliding_window", fmt(am.sliding_window))) |
| if am.is_moe: |
| rows.append(("experts (top-k)", f"{am.num_experts} (top-{am.experts_per_token})")) |
| rows.append(("tie_embeddings", str(am.tie_word_embeddings))) |
| if am.status != "ok": |
| rows.append(("status", am.status)) |
| return rows |
|
|
|
|
| def _reflow(boxes: list[Box], start_y: int = PAD + 70, gap: int = 16) -> int: |
| """Re-stack boxes vertically in list order using their (possibly grown) heights. |
| |
| Called after any box height changes so diff annotations never overlap neighbours. |
| Returns the y coordinate just below the last box. |
| """ |
| y = start_y |
| for b in boxes: |
| b.y = y |
| y += b.h + gap |
| return y |
|
|
|
|
| def _build_compact(am: ArchModel) -> Diagram: |
| """Top-down view from CONFIG alone — used when the model can't be instantiated on meta (so |
| there's no module tree to decompose) or has no transformer block (e.g. conv backbones). |
| Shows input → embedding → the per-config layer blocks (collapsed, ×N) → final norm → head, |
| family-aware, with the layer-type schedule. Far cleaner than the old bottom-up stack.""" |
| boxes: list[Box] = [] |
| x, w = COL_X, COL_W |
| y = PAD + 78 |
| modality = ( |
| "vision" |
| if am.family == "image_classification" |
| else ("audio" if am.family == "audio_classification" else "text") |
| ) |
| enc = am.view == "encoder" |
|
|
| def add(label, cls, subs=None, h=None, shape="op", badge=None): |
| nonlocal y |
| b = Box( |
| x, |
| y, |
| w, |
| h if h is not None else (30 + 15 * len(subs or [])), |
| label, |
| cls, |
| sublabels=subs or [], |
| shape=shape, |
| small=True, |
| badge=badge, |
| ) |
| boxes.append(b) |
| y = y + b.h + 16 |
| return b |
|
|
| |
| if modality == "vision": |
| add("input image", "c-io", ["pixel_values [1, 3, H, W]"], h=30, shape="io") |
| add("Patch Embedding", "c-embed", [f"→ [1, S, {am.hidden_size or '?'}]"], h=40) |
| elif modality == "audio": |
| add("input audio", "c-io", ["input_features"], h=30, shape="io") |
| add("Feature Projection", "c-embed", [f"→ [1, S, {am.hidden_size or '?'}]"], h=40) |
| else: |
| add("input_ids", "c-io", ["[1, S]"], h=30, shape="io") |
| add("Token Embedding", "c-embed", [f"[{am.vocab_size or '?'} × {am.hidden_size or '?'}]"], h=40) |
|
|
| |
| for b in am.layer_blocks: |
| inner = [f"{b.norm or 'Norm'} → {(b.attn_variant or b.kind.replace('_', '-'))}"] |
| inner.append( |
| f"{b.norm or 'Norm'} → {('MoE ' + str(am.num_experts) + 'E·top-' + str(am.experts_per_token)) if b.mlp_kind == 'moe' else 'MLP'}" |
| ) |
| cls = KIND_CLASS["moe"] if b.mlp_kind == "moe" else KIND_CLASS.get(b.kind, KIND_CLASS["layer"]) |
| add(b.layer_class, cls, inner, badge=f"×{b.count}", h=30 + 15 * len(inner)) |
|
|
| add(f"Final {am.norm_type or 'Norm'}", "c-norm", h=28) |
| if enc: |
| add("Pool + Classifier head", "c-head", [f"→ class logits [1, {am.num_labels or '?'}]"], h=40) |
| else: |
| add("LM Head", "c-head", [f"→ logits [1, S, {am.vocab_size or '?'}]"], h=40) |
|
|
| facts = _facts(am) |
| height = max(y + PAD, PAD + 78 + len(facts) * 22 + 120) |
| legend = _STD_LEGEND |
| note = ( |
| "config-only — built from config (weights not instantiable on meta)" |
| if am.status != "ok" |
| else "no transformer block (conv backbone) — shown from config" |
| ) |
| return Diagram( |
| width=W, |
| height=height, |
| boxes=boxes, |
| title=am.model, |
| subtitle=f"⚠ {note} · {am.decoder_kind} · {am.layer_summary or ''}", |
| legend=legend, |
| facts=facts, |
| mode="full", |
| spine=True, |
| ) |
|
|
|
|
| def _build_generic(am: ArchModel) -> Diagram: |
| """Universal top-down render from the real (collapsed) module tree — for any model that is |
| not a standard attention/SSM transformer: conv backbones (ResNet/ConvNeXt/MobileNet), |
| audio codecs (EnCodec/DAC), FFT mixers (FNet), detectors, pose/segmentation heads, ... |
| Every ``ModuleList``/``Sequential`` of identical blocks is collapsed to a ``×N`` section.""" |
| boxes: list[Box] = [] |
| GX, GW = 40, 850 |
| y = PAD + 78 |
| PFX = (am.config_class or "").replace("Config", "") |
|
|
| def add(label, cls, subs=None, h=30, shape="op"): |
| nonlocal y |
| b = Box(GX, y, GW, h, label, cls, sublabels=subs or [], shape=shape, small=True) |
| boxes.append(b) |
| y = b.y + b.h + 14 |
| return b |
|
|
| |
| modality = ( |
| "vision" if am.family == "image_classification" else ("audio" if am.family == "audio_classification" else None) |
| ) |
| if "pixel_values" in (am.modal_inputs or []) or modality == "vision": |
| add("input image", "c-io", ["pixel_values [1, 3, H, W]"], shape="io") |
| elif "input_features" in (am.modal_inputs or []) or modality == "audio": |
| add("input audio", "c-io", ["input_values / input_features"], shape="io") |
| else: |
| add("inputs", "c-io", ["input_ids / pixel_values / input_values"], shape="io") |
|
|
| |
| y = _emit_tree(boxes, am.generic_tree, GX, GW, y, gp=8, prefix=PFX) |
|
|
| facts = _facts(am) |
| height = max(y + PAD, PAD + 78 + len(facts) * 22 + 120) |
| legend = _STD_LEGEND |
| return Diagram( |
| width=WF, |
| height=height, |
| boxes=boxes, |
| title=am.model, |
| subtitle=f"{am.top_class or ''} · full module tree (no attention — shown structurally)", |
| legend=legend, |
| facts=facts, |
| mode="full", |
| spine=False, |
| ) |
|
|
|
|
| def _struct_hint(node: dict | None) -> str | None: |
| """A one-line structural summary of a stage's children, e.g. '4× DacEncoderBlock'.""" |
| if not node: |
| return None |
| reps = [c["cls"] for c in node.get("children", []) if "×" in c.get("cls", "")] |
| if reps: |
| return " · ".join(reps[:2]) |
| kids = node.get("children", []) |
| if kids: |
| return f"{len(kids)} submodules" |
| return None |
|
|
|
|
| def _shape_str(s) -> str: |
| return "×".join(str(d) for d in s) if s else "?" |
|
|
|
|
| def _build_flow(am: ArchModel) -> Diagram: |
| """Clean forward data-flow: the real input tensor flows top-to-bottom through each stage (in |
| call order, with the shape it produces) to the output tensor. Shapes come from one dummy |
| forward pass (meta, or a tiny CPU model). Used for codecs / conv backbones / detectors where |
| the data path — not an attention block — is the story (DAC, EnCodec, BLT, ResNet, ...).""" |
| from .introspect import _classify_mod |
|
|
| boxes: list[Box] = [] |
| arrows: list[Arrow] = [] |
| GX, GW = 300, 580 |
| y = PAD + 80 |
| PFX = (am.config_class or "").replace("Config", "") |
| tree_by = {n["name"]: n for n in am.generic_tree} |
|
|
| def place(label, cls, subs, shape="op"): |
| nonlocal y |
| b = Box(GX, y, GW, 34 + 15 * len(subs), label, cls, sublabels=subs, shape=shape, small=True) |
| boxes.append(b) |
| y = b.y + b.h |
| return b |
|
|
| |
| |
| shapes = {s["name"]: s.get("out_shape") for s in am.flow} |
| if am.flow: |
| stages = [(s["name"], s["cls"]) for s in am.flow] |
| has_shapes = True |
| else: |
| stages = [(n["name"], n["cls"]) for n in am.generic_tree] |
| has_shapes = False |
|
|
| |
| if am.flow_input: |
| in_label, in_sub = am.flow_input, [] |
| else: |
| mods = am.modal_inputs or [] |
| if "pixel_values" in mods or am.family == "image_classification": |
| in_label, in_sub = "input image", ["pixel_values [1, 3, H, W]"] |
| elif "input_features" in mods or am.family == "audio_classification": |
| in_label, in_sub = "input audio", ["input_values / input_features"] |
| else: |
| in_label, in_sub = "inputs", ["input_ids / pixel_values / input_values"] |
| prev = place(in_label, "c-io", in_sub, shape="io") |
|
|
| for name, cls in stages: |
| y += 24 |
| kind = _classify_mod(name, cls) |
| node = tree_by.get(name) |
| shp = shapes.get(name) |
| shape_lbl = f" → [{_shape_str(shp)}]" if shp else "" |
| hdr = f"{name} · {_short_cls(cls, PFX)}{shape_lbl}" |
| sec_top = y |
| if node and node.get("children"): |
| y += 28 |
| y = _emit_tree(boxes, node["children"], GX + 10, GW - 20, y, gp=7, prefix=PFX) |
| anchor = Box(GX, sec_top, GW, y - sec_top + 6, hdr, _NODE_CLS.get(kind, "c-layer"), shape="section") |
| boxes.append(anchor) |
| y += 6 |
| else: |
| anchor = place(hdr, _NODE_CLS.get(kind, "c-layer"), []) |
| arrows.append(Arrow([(prev.x + prev.w / 2, prev.y + prev.h), (anchor.x + anchor.w / 2, sec_top)], cls="flow")) |
| prev = anchor |
| if am.flow_output: |
| y += 24 |
| b = place(f"output [{_shape_str(am.flow_output)}]", "c-io", [], shape="io") |
| arrows.append(Arrow([(prev.x + prev.w / 2, prev.y + prev.h), (b.x + b.w / 2, b.y)], cls="flow")) |
|
|
| facts = _facts(am) |
| height = max(y + PAD, PAD + 80 + len(facts) * 22 + 120) |
| sub_note = "forward data flow (shapes from a dummy input pass)" if has_shapes else "staged data flow (structure)" |
| return Diagram( |
| width=WF, |
| height=height, |
| boxes=boxes, |
| arrows=arrows, |
| title=am.model, |
| subtitle=f"{am.top_class or ''} · {sub_note}", |
| legend=_STD_LEGEND, |
| facts=facts, |
| mode="full", |
| spine=False, |
| ) |
|
|
|
|
| |
|
|
| |
| WF = 1200 |
| STRIP_X = 40 |
| DCOL_X = 320 |
| DCOL_W = 330 |
| RAIL_X = 286 |
| MASK_X = 700 |
| SEQ = 12 |
|
|
| |
| _LT_CLASS = { |
| "full_attention": "c-lt-full", |
| "sliding_attention": "c-lt-sliding", |
| "chunked_attention": "c-lt-chunked", |
| "compressed_sparse_attention": "c-lt-compressed", |
| "heavily_compressed_attention": "c-lt-heavy", |
| "linear_attention": "c-lt-linear", |
| } |
|
|
|
|
| def _lt_class(t: str) -> str: |
| if t in _LT_CLASS: |
| return _LT_CLASS[t] |
| n = t.lower() |
| if "linear" in n or "delta" in n or "gated" in n: |
| return "c-lt-linear" |
| if "mamba" in n or "ssm" in n: |
| return "c-lt-mamba" |
| if "sliding" in n: |
| return "c-lt-sliding" |
| if "chunk" in n: |
| return "c-lt-chunked" |
| if "compress" in n: |
| return "c-lt-compressed" |
| return "c-lt-full" |
|
|
|
|
| def _dim(p) -> str: |
| if p is None: |
| return "" |
| return f"[{p.in_f or '?'}→{p.out_f or '?'}]" |
|
|
|
|
| |
| |
| |
| _NODE_CLS = { |
| "embedding": "c-embed", |
| "linear": "c-proj", |
| "conv": "c-conv", |
| "norm": "c-norm", |
| "act": "c-act", |
| "pool": "c-pool", |
| "dropout": "c-sub", |
| "rope": "c-rope", |
| "attention": "c-attn", |
| "mamba": "c-mamba", |
| "recurrent": "c-recur", |
| "quantizer": "c-quant", |
| "compressor": "c-lt-compressed", |
| "indexer": "c-lt-heavy", |
| "router": "c-moe", |
| "experts": "c-moe", |
| "head": "c-head", |
| "other": "c-sub", |
| } |
|
|
| |
| _STD_LEGEND = [ |
| ("c-embed", "embedding"), |
| ("c-attn", "attention"), |
| ("c-mamba", "Mamba / SSM"), |
| ("c-moe", "MoE / experts"), |
| ("c-proj", "linear / proj"), |
| ("c-conv", "convolution"), |
| ("c-norm", "normalization"), |
| ("c-act", "activation"), |
| ("c-pool", "pooling"), |
| ("c-head", "head"), |
| ] |
|
|
|
|
| def _short_cls(cls: str, prefix: str = "") -> str: |
| """Drop the model prefix and the redundant 'Activation' suffix: DeepseekV4RMSNorm→RMSNorm, |
| SiLUActivation→SiLU.""" |
| s = cls |
| if prefix and s.startswith(prefix): |
| s = s[len(prefix) :] |
| if s.endswith("Activation") and len(s) > 10: |
| s = s[: -len("Activation")] |
| return s or cls |
|
|
|
|
| def _leaf_label(nd, prefix: str) -> str: |
| """One compact line per leaf: name + dims; class shown only when it isn't a plain Linear.""" |
| sc = _short_cls(nd["cls"], prefix) |
| dim = nd.get("dim") |
| if sc == "Linear": |
| return f"{nd['name']} {dim}" if dim else nd["name"] |
| return f"{nd['name']} {sc}" + (f" {dim}" if dim else "") |
|
|
|
|
| def _emit_tree(boxes, nodes, x, w, y, gp=7, depth=0, prefix=""): |
| """Recursively lay out a module tree: leaves are single-line chips, branches become titled |
| sub-sections containing their own children. Returns the y below the laid-out tree.""" |
| i = 0 |
| while i < len(nodes): |
| nd = nodes[i] |
| if nd.get("children"): |
| sub_top = y |
| y += 25 |
| y = _emit_tree(boxes, nd["children"], x + 8, w - 16, y, gp, depth + 1, prefix) |
| label = f"{nd['name']} · {nd['cls']}" + (f" ({nd['dim']})" if nd.get("dim") else "") |
| boxes.append( |
| Box(x, sub_top, w, y - sub_top + 6, label, _NODE_CLS.get(nd["kind"], "c-sub"), shape="section") |
| ) |
| y += 6 + gp |
| i += 1 |
| else: |
| run = [] |
| while i < len(nodes) and not nodes[i].get("children"): |
| run.append(nodes[i]) |
| i += 1 |
| |
| widest = max((text_width(_leaf_label(nd2, prefix), 12.5) for nd2 in run), default=80) + 18 |
| per = max(1, min(3, int(w // max(widest, 120)))) |
| for r in range(0, len(run), per): |
| rown = run[r : r + per] |
| cw = (w - (len(rown) - 1) * gp) / len(rown) |
| |
| two = [text_width(_leaf_label(nd2, prefix), 12.5) > cw - 12 for nd2 in rown] |
| rh = 34 if any(two) else 22 |
| for j, nd2 in enumerate(rown): |
| ttl = f"{nd2['name']}: {nd2['cls']}" + (f" {nd2['dim']}" if nd2.get("dim") else "") |
| if two[j]: |
| sc = _short_cls(nd2["cls"], prefix) |
| sub = ( |
| (sc + (f" {nd2['dim']}" if nd2.get("dim") else "")) |
| if sc != "Linear" |
| else (nd2.get("dim") or "") |
| ) |
| boxes.append( |
| Box( |
| int(x + j * (cw + gp)), |
| y, |
| int(cw), |
| rh, |
| nd2["name"], |
| _NODE_CLS.get(nd2["kind"], "c-sub"), |
| sublabels=[sub] if sub else [], |
| shape="op", |
| small=True, |
| title=ttl, |
| ) |
| ) |
| else: |
| boxes.append( |
| Box( |
| int(x + j * (cw + gp)), |
| y, |
| int(cw), |
| rh, |
| _leaf_label(nd2, prefix), |
| _NODE_CLS.get(nd2["kind"], "c-sub"), |
| shape="op", |
| small=True, |
| title=ttl, |
| ) |
| ) |
| y += rh + gp |
| return y |
|
|
|
|
| def _node_sig(node): |
| """Structural signature of a module node (class + dims + recursive children).""" |
| return (node.get("cls"), node.get("dim"), tuple(_node_sig(c) for c in (node.get("children") or []))) |
|
|
|
|
| def _emit_merged(boxes, variants, types, x, w, y, gp=7, prefix=""): |
| """Render N aligned module trees, sharing identical sub-trees and branching side-by-side |
| ONLY where they actually differ. For DeepSeek-V4 attention this shows q/k/v/o once and the |
| differing `compressor` (HCA vs CSA+Indexer) side by side -- the real subtlety.""" |
| base = variants[0] |
| names = [n["name"] for n in base] |
| if any([n["name"] for n in v] != names for v in variants): |
| nn = len(variants) |
| cw = (w - (nn - 1) * 18) / nn |
| mb = y |
| for k, v in enumerate(variants): |
| mb = max(mb, _emit_tree(boxes, v, int(x + k * (cw + 18)), int(cw), y, gp, prefix=prefix)) |
| return mb |
| for idx in range(len(names)): |
| nodes = [v[idx] for v in variants] |
| if all(_node_sig(nd) == _node_sig(nodes[0]) for nd in nodes): |
| y = _emit_tree(boxes, [nodes[0]], x, w, y, gp, prefix=prefix) |
| else: |
| nn = len(nodes) |
| cw = (w - (nn - 1) * gp) / nn |
| top = y |
| mb = y |
| for k, nd in enumerate(nodes): |
| cx = int(x + k * (cw + gp)) |
| boxes.append(Box(cx, top, int(cw), 18, f"▼ {types[k]}", "c-sub", shape="op", small=True)) |
| mb = max(mb, _emit_tree(boxes, [nd], cx, int(cw), top + 22, gp, prefix=prefix)) |
| y = mb |
| return y |
|
|
|
|
| def _short_proj(name: str) -> str: |
| return name.replace("_proj", "").replace("_with_mqa", "·mqa").replace("kv_a", "kv↓").replace("kv_b", "kv↑") |
|
|
|
|
| def _chips(boxes, items, x, w, y, per_row, cls="c-sub", gp=8, h=34): |
| """Place `items` = list of (label, sublabel) as a grid of small boxes; return new y.""" |
| rows = [items[i : i + per_row] for i in range(0, len(items), per_row)] |
| for r in rows: |
| cw = (w - (len(r) - 1) * gp) / len(r) |
| for j, (lb, sub) in enumerate(r): |
| boxes.append( |
| Box( |
| int(x + j * (cw + gp)), |
| y, |
| int(cw), |
| h, |
| lb, |
| cls, |
| sublabels=([sub] if sub else []), |
| shape="op", |
| small=True, |
| ) |
| ) |
| y += h + gp |
| return y |
|
|
|
|
| def _shape3(h) -> str: |
| return f"[1, {SEQ}, {h or '?'}]" |
|
|
|
|
| def build_full(am: ArchModel) -> Diagram: |
| """Dispatch: multimodal models get the multi-tower view, everything else the LLM column.""" |
| if am.is_multimodal and am.towers and am.block is not None: |
| return build_multimodal(am) |
| |
| if am.is_pipeline and am.generic_tree: |
| return _build_flow(am) |
| return _build_column(am) |
|
|
|
|
| def _build_column(am: ArchModel) -> Diagram: |
| """Detailed, Raschka-style view, read **top-to-bottom like the code**: input_ids at the |
| top flow down through the embedding, the expanded transformer block (pre-norm → |
| self-attention with RoPE/Q/K/V/O and real dims → ⊕ residual → pre-norm → MLP or Sparse |
| MoE → ⊕ residual), final norm, LM head and softmax to the logits at the bottom. A |
| left-side strip shows the per-layer schedule (config.layer_types); attention-mask grids |
| for each distinct layer type are shown on the right. Tensor shapes use a dummy [1, 12] |
| input. Falls back to the compact view when block internals can't be extracted.""" |
| if am.block is None or (am.block.attention is None and am.block.mixer is None and am.block.mlp is None): |
| |
| |
| return _build_flow(am) if am.generic_tree else _build_compact(am) |
|
|
| blk = am.block |
| blk_is_ssm = blk.attention is None and blk.mixer is not None |
| GAP = 14 |
| boxes: list[Box] = [] |
| arrows: list[Arrow] = [] |
| y = PAD + 80 |
| H = am.hidden_size |
| PFX = (am.config_class or "").replace("Config", "") |
|
|
| def add(label, cls, subs=None, h=None, shape="op", glyph=None, w=DCOL_W, x=DCOL_X, title=None, grid=None): |
| nonlocal y |
| b = Box( |
| x, |
| y, |
| w, |
| h if h is not None else (30 + 15 * len(subs or [])), |
| label, |
| cls, |
| sublabels=subs or [], |
| shape=shape, |
| small=True, |
| glyph=glyph, |
| title=title, |
| grid=grid, |
| ) |
| boxes.append(b) |
| y = y + b.h + GAP |
| return b |
|
|
| |
| modality = ( |
| "vision" |
| if am.family == "image_classification" |
| else ("audio" if am.family == "audio_classification" else "text") |
| ) |
| enc = am.view == "encoder" |
| seq = len(am.tokens or [1] * 6) or 6 |
|
|
| def _shapeN(h): |
| return f"[1, {seq}, {h or '?'}]" |
|
|
| |
| if modality == "vision": |
| add("input image", "c-io", ["pixel_values [1, 3, H, W]"], h=32, shape="io") |
| add("Patch Embedding", "c-embed", [f"conv patches → {_shapeN(H)}", "+ position embeddings"], h=60) |
| elif modality == "audio": |
| add("input audio", "c-io", ["input_features [1, n_mels, T]"], h=32, shape="io") |
| add("Feature Projection", "c-embed", [f"conv/linear → {_shapeN(H)}", "+ position embeddings"], h=60) |
| else: |
| toks = am.tokens or ["Hey", ",", "␣how", "␣are", "␣you", "?"] |
| seq = len(toks) |
| add("input_ids", "c-io", [f'tokenize("Hey, how are you?") → [1, {seq}]'], h=32, shape="io") |
| tgp = 5 |
| tw = (DCOL_W - (seq - 1) * tgp) / seq |
| for j, t in enumerate(toks): |
| label = t if len(t) <= 6 else t[:6] + "…" |
| boxes.append( |
| Box( |
| int(DCOL_X + j * (tw + tgp)), |
| y, |
| max(14, int(tw)), |
| 26, |
| label, |
| "c-sub", |
| sublabels=[str(j)], |
| shape="op", |
| small=True, |
| title=f"token {j}: {t!r}", |
| ) |
| ) |
| y += 26 + 18 |
| pos_note = [f"+ {am.positional} positions"] if (am.positional and am.positional not in ("RoPE", "n/a")) else [] |
| add( |
| "Token Embedding", |
| "c-embed", |
| [f"weight [{am.vocab_size or '?'} × {H or '?'}] → {_shapeN(H)}"] + pos_note, |
| h=30 + 15 * (1 + len(pos_note)), |
| ) |
| body_top = boxes[-1].y |
|
|
| |
| IX, IW, GP = DCOL_X + 12, DCOL_W - 24, 8 |
|
|
| def section(label, cls, body, badge=None): |
| """Draw a filled section panel containing inner chips produced by ``body(y)``.""" |
| nonlocal y |
| sec_top = y |
| y += 26 |
| y = body(y) |
| sec = Box(DCOL_X, sec_top, DCOL_W, y - sec_top + 6, label, cls, shape="section", badge=badge) |
| boxes.append(sec) |
| y = y + 6 + GAP |
| return sec |
|
|
| |
| y += 22 |
| block_top = y |
| entry_y = block_top |
| add( |
| f"{blk.pre_attn_norm or 'Norm'}", |
| "c-norm", |
| [("pre-mixer " if blk_is_ssm else "pre-attention ") + _shapeN(H)], |
| h=28, |
| ) |
|
|
| |
| |
| a = blk.attention |
| from .introspect import _short_layer_type |
|
|
| attn_right = DCOL_X + DCOL_W |
| variants = am.attention_variants |
| if variants: |
| n = min(len(variants), 3) |
| types = [_short_layer_type(v["type"]) for v in variants[:n]] |
| sc_by_type = {sc["type"]: sc for sc in am.sparse_components} |
| diverges = n > 1 |
| Wsec = DCOL_W if not diverges else max(DCOL_W, n * 300) |
| gqa = "" |
| if a and a.n_heads and a.n_kv and a.n_kv < a.n_heads: |
| gqa = f" · GQA {a.n_heads}:{a.n_kv}" |
| elif a and a.n_kv == 1: |
| gqa = " · MQA" |
| vtop = y |
| vy = vtop + 26 |
| if n == 1: |
| vy = _emit_tree(boxes, variants[0]["children"], DCOL_X + 10, Wsec - 20, vy, gp=GP) |
| else: |
| vy = _emit_merged(boxes, [v["children"] for v in variants[:n]], types, DCOL_X + 10, Wsec - 20, vy, gp=GP) |
| |
| is_mixer = a is None and blk.mixer is not None |
| boxes.append( |
| Box( |
| DCOL_X + 10, |
| vy, |
| Wsec - 20, |
| 30, |
| "selective state-space scan (SSM)" if is_mixer else "scaled dot-product attention", |
| "c-sub", |
| sublabels=([f"{a.n_heads or '?'} heads · head_dim {a.head_dim or '?'}{gqa}"] if a else []), |
| shape="op", |
| small=True, |
| ) |
| ) |
| vy += 30 + GP |
| |
| mask_items = [ |
| (v["type"], (sc_by_type.get(v["type"], {}).get("mask") or am.attn_patterns.get(v["type"]))) |
| for v in variants[:n] |
| ] |
| mask_items = [(t, g) for t, g in mask_items if g] |
| if not mask_items: |
| sq = 14 |
| if enc: |
| grid = [[1] * sq for _ in range(sq)] |
| mask_items = [("bidirectional", grid)] |
| else: |
| grid = [[1 if j <= i else 0 for j in range(sq)] for i in range(sq)] |
| mask_items = [("causal", grid)] |
| if mask_items: |
| mw = (Wsec - 20 - (len(mask_items) - 1) * 16) / len(mask_items) |
| mtop = vy + 4 |
| mb = mtop |
| for k, (lt, grid) in enumerate(mask_items): |
| sc = sc_by_type.get(lt) |
| cols = len(grid[0]) |
| cellp = max(3, min(10, int((mw - 6) // cols))) |
| gx = int(DCOL_X + 10 + k * (mw + 16) + (mw - cellp * cols) / 2) |
| cap = ( |
| f"{_short_layer_type(lt)} mask m={sc['display_m']}·{sc['n_comp']}c" |
| if sc |
| else f"{_short_layer_type(lt)} mask" |
| ) |
| boxes.append( |
| Box( |
| gx, |
| mtop + 6, |
| cellp * cols, |
| cellp * len(grid), |
| cap, |
| _lt_class(lt), |
| shape="grid", |
| grid=grid, |
| grid_split=(sc.get("mask_split") if sc else None), |
| title=f"{lt} attention mask (q↓ × k→)", |
| ) |
| ) |
| mb = max(mb, mtop + 6 + cellp * len(grid)) |
| vy = mb |
| note = "" |
| if diverges: |
| sigs = [tuple(_node_sig(c) for c in v["children"]) for v in variants[:n]] |
| struct_diff = any(s != sigs[0] for s in sigs) |
| note = ( |
| f" ({n} layer types — modules differ ▼)" |
| if struct_diff |
| else f" ({n} layer types — same modules, mask differs)" |
| ) |
| if is_mixer: |
| hdr = f"Token Mixer (SSM) · {blk.mixer.cls}{note}" |
| else: |
| hdr = f"Self-Attention · {a.cls if a else 'attention'}{note}" |
| boxes.append(Box(DCOL_X, vtop, Wsec, vy - vtop + 6, hdr, "c-attn", shape="section")) |
| attn_right = DCOL_X + Wsec |
| y = vy + 6 + GAP |
| if a and a.rope: |
| ry = vtop + 30 |
| rope_label = f"RoPE θ={int(am.rope_theta):,}" if am.rope_theta else "RoPE" |
| rnode = Box( |
| DCOL_X - 168, |
| ry - 4, |
| 132, |
| 40, |
| rope_label, |
| "c-rope", |
| sublabels=["rotary positions → Q, K"], |
| shape="op", |
| small=True, |
| ) |
| boxes.append(rnode) |
| arrows.append(Arrow([(rnode.x + rnode.w, ry + 14), (DCOL_X, ry + 14)], cls="rope")) |
| else: |
| add("Token Mixing (linear-attention / SSM)", "c-linattn", [blk.layer_class], h=44) |
| plus_attn = add("", "c-add", h=26, shape="circle", glyph="+", title="residual add") |
|
|
| |
| |
| mm = blk.mlp |
| mvars = am.mlp_variants if len(am.mlp_variants) > 1 else None |
| has_mlp = bool(mm or (am.mlp_tree and am.mlp_tree.get("children")) or mvars) |
| if has_mlp: |
| add(f"{blk.post_attn_norm or 'Norm'}", "c-norm", ["pre-FFN " + _shapeN(H)], h=28) |
| if mvars: |
| |
| |
| nm = min(len(mvars), 3) |
| Wm = max(DCOL_W, nm * 300) |
| mtypes = [mv["type"] for mv in mvars[:nm]] |
| vtop = y |
| my = vtop + 26 |
| my = _emit_merged(boxes, [mv["children"] for mv in mvars[:nm]], mtypes, DCOL_X + 10, Wm - 20, my, gp=GP) |
| cls0 = mvars[0]["cls"] |
| boxes.append( |
| Box( |
| DCOL_X, |
| vtop, |
| Wm, |
| my - vtop + 6, |
| f"Sparse MoE · {cls0} ({nm} FFN types ▼)", |
| "c-moe", |
| shape="section", |
| badge="MoE", |
| ) |
| ) |
| attn_right = max(attn_right, DCOL_X + Wm) |
| y = my + 6 + GAP |
| elif am.mlp_tree and am.mlp_tree.get("children"): |
| cls = am.mlp_tree["cls"] |
| is_moe = bool(mm and mm.is_moe) |
| section( |
| ("Sparse MoE · " if is_moe else "MLP · ") + cls, |
| "c-moe" if is_moe else "c-mlp", |
| lambda yy: _emit_tree(boxes, am.mlp_tree["children"], IX, IW, yy, gp=GP), |
| badge="MoE" if is_moe else None, |
| ) |
| elif mm: |
|
|
| def mlp_body(y): |
| items = [] |
| if mm.gate: |
| items.append(("gate_proj", _dim(mm.gate))) |
| if mm.up: |
| items.append(("up_proj", _dim(mm.up))) |
| per = len(items) if items else 1 |
| if items: |
| y = _chips(boxes, items, IX, IW, y, per, gp=GP, h=32) |
| boxes.append( |
| Box(IX, y, IW, 28, f"{mm.act}(gate) ⊙ up" if mm.gate else mm.act, "c-sub", shape="op", small=True) |
| ) |
| y += 28 + GP |
| boxes.append(Box(IX, y, IW, 32, f"down_proj {_dim(mm.down)}", "c-sub", shape="op", small=True)) |
| y += 32 |
| return y |
|
|
| section(f"MLP · {mm.cls}", "c-mlp", mlp_body) |
| elif has_mlp: |
| add("MLP", "c-mlp", h=30) |
| |
| plus_mlp = add("", "c-add", h=26, shape="circle", glyph="+", title="residual add") if has_mlp else plus_attn |
| block_bot = y - GAP |
|
|
| container_w = max(DCOL_W, attn_right - DCOL_X) + 52 |
| container = Box( |
| DCOL_X - 26, |
| block_top - 22, |
| container_w, |
| block_bot - block_top + 30, |
| blk.layer_class, |
| "c-block", |
| shape="container", |
| badge=f"× {am.num_layers or '?'}", |
| title=f"{am.num_layers} × {blk.layer_class}", |
| ) |
|
|
| |
| add(f"Final {am.norm_type or 'Norm'}", "c-norm", [_shapeN(H)], h=28) |
| body_bot = boxes[-1].y + boxes[-1].h |
| head_title = am.head_class |
| if enc: |
| nl = am.num_labels or "?" |
| add("Pool (CLS / mean)", "c-head", [f"{_shapeN(H)} → [1, {H or '?'}]"], h=40, title=head_title) |
| add("Classifier head", "c-head", [f"Linear [{H or '?'}→{nl}]"], h=40, title=head_title) |
| add("Softmax", "c-soft", h=26) |
| add("class logits", "c-io", [f"[1, {nl}] ({nl} classes)"], h=30, shape="io") |
| else: |
| head_sub = [f"Linear [{H or '?'}→{am.vocab_size or '?'}]" + (" · tied" if am.tie_word_embeddings else "")] |
| add("LM Head", "c-head", head_sub, h=42, title=head_title) |
| add("Softmax", "c-soft", h=26) |
| add("logits", "c-io", [f"[1, {seq}, {am.vocab_size or '?'}]"], h=30, shape="io") |
|
|
| |
| |
| ww = (attn_right - DCOL_X) + 68 |
| if am.top_class: |
| boxes.append( |
| Box( |
| DCOL_X - 34, |
| body_top - 14, |
| ww, |
| body_bot - body_top + 20, |
| f"{am.top_class} · base model", |
| "c-block", |
| shape="container", |
| ) |
| ) |
| if head_title: |
| boxes.append( |
| Box( |
| DCOL_X - 34, |
| body_bot + 6, |
| ww, |
| (y - GAP) - (body_bot + 6) + 8, |
| f"{head_title} · task head", |
| "c-block", |
| shape="container", |
| ) |
| ) |
|
|
| |
| def midy(b): |
| return b.y + b.h // 2 |
|
|
| arrows.append( |
| Arrow( |
| [(DCOL_X, entry_y), (RAIL_X, entry_y), (RAIL_X, midy(plus_attn)), (plus_attn.x, midy(plus_attn))], |
| cls="residual", |
| dashed=True, |
| ) |
| ) |
| if has_mlp: |
| arrows.append( |
| Arrow( |
| [ |
| (plus_attn.x, midy(plus_attn)), |
| (RAIL_X - 16, midy(plus_attn)), |
| (RAIL_X - 16, midy(plus_mlp)), |
| (plus_mlp.x, midy(plus_mlp)), |
| ], |
| cls="residual", |
| dashed=True, |
| ) |
| ) |
|
|
| boxes.insert(0, container) |
|
|
| |
| legend = [("c-attn", "self-attention")] |
| strip_bottom = block_bot |
| |
| if am.layer_types and len(set(am.layer_types)) >= 2: |
| from .introspect import _short_layer_type |
|
|
| n = len(am.layer_types) |
| SW = 46 |
| cell_h = max(15, (block_bot - block_top) / n) |
| boxes.append(Box(STRIP_X - 4, block_top - 24, SW + 12, 16, "layers (idx)", "c-block", shape="container")) |
| for i, lt in enumerate(am.layer_types): |
| cy = int(block_top + i * cell_h) |
| boxes.append( |
| Box( |
| STRIP_X, |
| cy, |
| SW, |
| max(13, int(cell_h) - 2), |
| str(i), |
| _lt_class(lt), |
| shape="cell", |
| title=f"layer {i}: {lt}", |
| ) |
| ) |
| strip_bottom = int(block_top + n * cell_h) |
| seen = [] |
| for lt in am.layer_types: |
| if lt not in seen: |
| seen.append(lt) |
| for lt in seen: |
| legend.append((_lt_class(lt), _short_layer_type(lt))) |
|
|
| |
| gy = 0 |
| legend += [ |
| ("c-proj", "linear / proj"), |
| ("c-moe", "MoE / experts") if am.is_moe else ("c-mlp", "MLP / feed-forward"), |
| ("c-rope", "RoPE"), |
| ("c-norm", "normalization"), |
| ("residual", "residual / skip"), |
| ] |
|
|
| facts = _facts(am) |
| mask_bottom = gy |
| height = max(y + PAD, strip_bottom + PAD, mask_bottom + PAD, PAD + 80 + len(facts) * 22 + len(legend) * 18 + 120) |
| sub = f"{am.decoder_kind} · {am.layer_summary or ''}" |
| width = max(WF, attn_right + 320) |
| return Diagram( |
| width=width, |
| height=height, |
| boxes=boxes, |
| arrows=arrows, |
| title=am.model, |
| subtitle=sub, |
| legend=legend, |
| facts=facts, |
| mode="full", |
| spine=True, |
| ) |
|
|
|
|
| |
|
|
|
|
| def build_multimodal(am: ArchModel) -> Diagram: |
| """VLM / audio view: encoder tower(s) → projector → the LLM (the detailed column). |
| |
| The LLM column is built by ``_build_column`` and shifted right; the modality towers are |
| drawn in a left lane, with a fusion arrow into the LLM (merge at modality tokens), or |
| cross-attention edges into specific LLM layers when the text config declares them. |
| """ |
| d = _build_column(am) |
| dx = 300 |
| for b in d.boxes: |
| b.x += dx |
| for a in d.arrows: |
| a.points = [(x + dx, y) for (x, y) in a.points] |
| d.width += dx |
|
|
| emb = next((b for b in d.boxes if b.cls == "c-embed"), None) |
| container = next((b for b in d.boxes if b.shape == "container" and b.cls == "c-block"), None) |
|
|
| TX, TW = 36, 250 |
| ty = PAD + 92 |
| encoders = [t for t in am.towers if t["role"] in ("vision", "audio")] |
| proj = next((t for t in am.towers if t["role"] == "projector"), None) |
|
|
| mmpfx = (am.config_class or "").replace("Config", "") |
|
|
| def tb(label, cls, subs, h, badge=None, shape="op"): |
| nonlocal ty |
| b = Box(TX, ty, TW, h, label, cls, sublabels=subs or [], shape=shape, small=True, badge=badge) |
| d.boxes.append(b) |
| ty += h + 16 |
| return b |
|
|
| def tower(t, label, cls, summary, badge=None): |
| """Render a tower: fully decomposed when small; a representative block ×N for big |
| encoder stacks; else a summary box. So every tower shows what's inside it.""" |
| nonlocal ty |
| ch = t.get("children") |
| if not ch and t.get("block_children"): |
| |
| top = ty |
| inner_top = top + 26 |
| blabel = f"{t['block_class']}" |
| by = _emit_tree(d.boxes, t["block_children"], TX + 18, TW - 36, inner_top + 22, gp=6, prefix=mmpfx) |
| d.boxes.append( |
| Box( |
| TX + 10, |
| inner_top, |
| TW - 20, |
| by - inner_top + 6, |
| blabel, |
| cls, |
| shape="section", |
| badge=f"×{t['block_n']}", |
| ) |
| ) |
| b = Box(TX, top, TW, (by + 6) - top + 6, label, cls, shape="section", badge=badge) |
| d.boxes.append(b) |
| ty = by + 6 + 6 + 16 |
| return b |
| if ch: |
| top = ty |
| yy = _emit_tree(d.boxes, ch, TX + 10, TW - 20, top + 26, gp=6, prefix=mmpfx) |
| b = Box(TX, top, TW, yy - top + 6, label, cls, shape="section", badge=badge) |
| d.boxes.append(b) |
| ty = yy + 6 + 16 |
| return b |
| return tb(label, cls, summary, 52, badge=badge) |
|
|
| enc_boxes = [] |
| for t in encoders: |
| if t["role"] == "vision": |
| inp, ishape, cls, role = "pixel_values", "[1, 3, 336, 336]", "c-vision", "Vision encoder" |
| img = Box(TX + (TW - 76) // 2, ty, 76, 64, "example image", "c-vision", shape="image") |
| d.boxes.append(img) |
| ty += 64 + 22 |
| else: |
| inp, ishape, cls, role = "input_features", "[1, 128, 3000]", "c-audio", "Audio encoder" |
| img = None |
| ib = tb(inp, "c-io", [ishape], 30, shape="io") |
| if img is not None: |
| d.arrows.append(Arrow([(img.x + img.w // 2, img.y + img.h), (ib.x + ib.w // 2, ib.y)], cls="flow")) |
| cls_line = t["cls"] + (" · via AutoModel" if am.auto_classes else "") |
| eb = tower( |
| t, |
| f"{role} · {t['cls']}", |
| cls, |
| [cls_line, f"{t['model_type'] or ''} · {t['layers'] or '?'} layers · h={t['hidden'] or '?'}"], |
| badge=(f"×{t['layers']}" if t["layers"] else None), |
| ) |
| d.arrows.append(Arrow([(ib.x + ib.w // 2, ib.y + ib.h), (eb.x + eb.w // 2, eb.y)], cls="flow")) |
| enc_boxes.append(eb) |
|
|
| pj = tower( |
| proj or {}, f"Projector · {proj['cls'] if proj else 'Linear'}", "c-proj", ["align features → text hidden dim"] |
| ) |
| for eb in enc_boxes: |
| d.arrows.append(Arrow([(eb.x + eb.w // 2, eb.y + eb.h), (pj.x + pj.w // 2, pj.y)], cls="flow")) |
|
|
| |
| px = pj.x + pj.w |
| pmid = pj.y + pj.h // 2 |
| if am.cross_attention_layers and container is not None: |
| n = len(am.cross_attention_layers) |
| target_x, target_y = container.x, container.y + container.h // 2 |
| d.arrows.append( |
| Arrow( |
| [(px, pmid), ((px + target_x) // 2, pmid), ((px + target_x) // 2, target_y), (target_x, target_y)], |
| cls="xattn", |
| ) |
| ) |
| d.boxes.append( |
| Box( |
| int((px + target_x) // 2) - 70, |
| target_y - 34, |
| 150, |
| 18, |
| f"cross-attention @ {n} layers", |
| "c-proj", |
| shape="op", |
| small=True, |
| ) |
| ) |
| elif emb is not None: |
| target_x, target_y = emb.x, emb.y + emb.h // 2 |
| d.arrows.append( |
| Arrow( |
| [(px, pmid), ((px + target_x) // 2, pmid), ((px + target_x) // 2, target_y), (target_x, target_y)], |
| cls="flow", |
| ) |
| ) |
| d.boxes.append( |
| Box( |
| int((px + target_x) // 2) - 78, |
| target_y - 30, |
| 168, |
| 18, |
| "merge at modality tokens", |
| "c-proj", |
| shape="op", |
| small=True, |
| ) |
| ) |
|
|
| |
| if any(t["role"] == "vision" for t in encoders): |
| from .masks import image_text_mask |
|
|
| n_img, n_text = 5, 7 |
| grid, split = image_text_mask(n_img, n_text, image_bidirectional=am.image_bidirectional) |
| cellp = 13 |
| ty += 16 |
| mx = TX + (TW - cellp * len(grid)) // 2 |
| my = ty + 22 |
| kind = "prefix-LM: image bidirectional" if am.image_bidirectional else "fully causal" |
| d.boxes.append(Box(TX - 4, ty, TW + 8, 16, "image ⊕ text attention", "c-block", shape="container")) |
| d.boxes.append( |
| Box( |
| mx, |
| my, |
| cellp * len(grid), |
| cellp * len(grid), |
| f"{n_img} img + {n_text} text · {kind}", |
| "c-vision", |
| shape="grid", |
| grid=grid, |
| grid_split=split, |
| title="image↓text query × image|text key", |
| ) |
| ) |
| ty = my + cellp * len(grid) + 16 |
|
|
| |
| if any(t["role"] == "vision" for t in encoders): |
| d.legend.insert(0, ("c-vision", "vision encoder")) |
| if any(t["role"] == "audio" for t in encoders): |
| d.legend.insert(0, ("c-audio", "audio encoder")) |
| d.legend.append(("c-proj", "projector / fusion")) |
| if am.cross_attention_layers: |
| d.legend.append(("xattn", "cross-attention")) |
|
|
| d.subtitle = f"multimodal · {'+'.join(t['role'] for t in encoders)} → projector → LLM · {am.decoder_kind}" |
| d.facts.insert(1, ("inputs", ", ".join(am.modal_inputs))) |
| if am.auto_classes: |
| d.facts.insert(2, ("built via", ", ".join(am.auto_classes[:2]))) |
| d.height = max(d.height, ty + PAD) |
| return d |
|
|
|
|
| |
|
|
|
|
| |
| |
| |
| def _slot_for_class(name: str) -> str: |
| n = name.lower() |
| if n.endswith("config"): |
| return "config" |
| if n.endswith("rotaryembedding") or n.endswith("rotaryembeddings"): |
| return "rope" |
| if n.endswith(("router", "experts", "expert", "moeblock", "sparsemoeblock", "moe", "moemlp")): |
| return "moe" |
| if n.endswith(("mlp", "feedforward", "ffn")): |
| return "mlp" |
| if n.endswith(("attention", "attn", "sdpaattention", "flashattention2")): |
| return "attention" |
| if n.endswith(("rmsnorm", "layernorm", "norm")): |
| return "norm" |
| if ( |
| n.endswith(("pretrainedmodel",)) |
| or "forcausallm" in n |
| or "forsequence" in n |
| or "fortoken" in n |
| or "forquestion" in n |
| or "forconditional" in n |
| or "forretrieval" in n |
| or "withlmhead" in n |
| ): |
| return "head" |
| if n.endswith(("decoderlayer", "encoderlayer", "layer", "block", "mixer")): |
| return "layer" |
| if n.endswith(("embedding", "embeddings")): |
| return "embedding" |
| if n.endswith("model"): |
| return "layer" |
| return "layer" |
|
|
|
|
| def _change_for(cc: ClassChange) -> str | None: |
| if cc.relation == "new": |
| return "added" |
| if cc.deleted_methods or cc.deleted_attrs: |
| return "deleted" |
| if cc.n_changes == 0: |
| return None |
| if cc.added_methods or cc.added_attrs: |
| return "added" |
| return "overridden" |
|
|
|
|
| def _change_detail(cc: ClassChange) -> str: |
| bits = [] |
| if cc.overridden_methods: |
| bits.append("ovr " + ",".join(cc.overridden_methods)) |
| if cc.added_methods: |
| bits.append("add " + ",".join(cc.added_methods)) |
| if cc.deleted_methods: |
| bits.append("del " + ",".join(cc.deleted_methods)) |
| na = len(cc.added_attrs) + len(cc.overridden_attrs) |
| if na: |
| bits.append(f"{na} attr") |
| return "; ".join(bits) if bits else "rename only" |
|
|
|
|
| def build_diff(am: ArchModel, ad: ArchDiff) -> Diagram: |
| """Diff over the *real architecture*: render the full detailed view, then colour each |
| class-box by how it relates to the parent it inherits from -- added (green), overridden |
| (amber), deleted (red). Everything inherited unchanged / copy-pasted is greyed (ghosted), |
| so at a glance you see what the model is made of AND exactly what it changed.""" |
| d = build_full(am) |
| d.mode = "diff" |
| d.title = am.model |
|
|
| if not ad.is_modular: |
| |
| d.subtitle = (ad.note or "standalone (no modular parent)") + " · shown in full" |
| return d |
|
|
| |
| |
| |
| |
| by_cls: dict[str, str] = {} |
| detail: dict[str, ClassChange] = {} |
| changes_list: list[tuple[str, str, str]] = [] |
| rank = {"deleted": 0, "overridden": 1, "added": 2} |
| for cc in ad.changes: |
| ch = _change_for(cc) |
| detail[cc.name] = cc |
| if ch is not None: |
| by_cls[cc.name] = "deleted" if ch == "deleted" else "overridden" |
| changes_list.append((ch, cc.name, _change_detail(cc))) |
| changes_list.sort(key=lambda t: (rank.get(t[0], 9), t[1])) |
|
|
| |
| changed_names = sorted(by_cls, key=len, reverse=True) |
|
|
| def match(text: str) -> str | None: |
| for nm in changed_names: |
| if nm in text: |
| return by_cls[nm] |
| return None |
|
|
| def by_keyword(*kws: str) -> str | None: |
| |
| for nm in changed_names: |
| low = nm.lower() |
| if any(k in low for k in kws): |
| return by_cls[nm] |
| return None |
|
|
| |
| for b in d.boxes: |
| if b.shape in ("container", "io", "circle", "cell", "grid"): |
| continue |
| ch = match(f"{b.label or ''} {b.title or ''}") |
| if ch is None and b.cls == "c-embed": |
| ch = by_keyword("embedding", "embed") |
| if ch is None and b.cls == "c-head": |
| ch = by_keyword("for", "lmhead", "classifier", "classification", "head") |
| if ch: |
| b.change = ch |
| b.ghost = False |
| else: |
| b.ghost = True |
|
|
| |
| pam = _parent_arch(ad.parent_model) if ad.parent_model else None |
| if pam is not None: |
| pnames: set = set() |
| for v in pam.attention_variants: |
| _collect_submodule_names(v["children"], pnames) |
| if pam.mlp_tree: |
| _collect_submodule_names(pam.mlp_tree.get("children") or [], pnames) |
| |
| |
| if pnames: |
| for b in d.boxes: |
| if b.shape == "op" and b.title and ": " in b.title and not b.change: |
| nm, _, rest = b.title.partition(": ") |
| |
| if not rest[:1].isupper(): |
| continue |
| if nm and nm not in pnames: |
| b.change = "added" |
| b.ghost = False |
| |
| cpfx = (am.config_class or "").replace("Config", "") |
| ppfx = (pam.config_class or "").replace("Config", "") |
| if cpfx and ppfx and cpfx != ppfx: |
| for b in d.boxes: |
| if b.ghost and b.shape == "section" and cpfx in (b.label or ""): |
| b.label = b.label.replace(cpfx, ppfx) + " ↩ inherited" |
|
|
| t = ad.totals |
| extra = f" (+{','.join(p for p in ad.parent_models if p != ad.parent_model)})" if len(ad.parent_models) > 1 else "" |
| d.subtitle = ( |
| f"diff vs {ad.parent_model}{extra} · {t['overridden']} overridden · {t['added']} added · " |
| f"{t['deleted']} deleted · {t['new_classes']} new · {t['trivial']} inherited-as-is" |
| ) |
| d.changes = changes_list |
| d.legend = [ |
| ("ch-added", "new submodule (vs parent)"), |
| ("ch-over", "changed / redefined"), |
| ("ch-deleted", "deleted"), |
| ("ghost", "inherited / copy-pasted"), |
| ] |
| d.facts = [ |
| ("model id", (am.checkpoint or am.model)[:30]), |
| ("parent", ad.parent_model or "—"), |
| ("classes", str(len(ad.changes))), |
| ("overridden", str(t["overridden"])), |
| ("added", str(t["added"])), |
| ("new classes", str(t["new_classes"])), |
| ("inherited as-is", str(t["trivial"])), |
| ] |
| |
| panel_h = len(d.facts) * 22 + len(d.legend) * 18 + len(d.changes) * 28 + 200 |
| d.height = max(d.height, PAD + 80 + panel_h) |
| return d |
|
|