import { useMemo } from "react"; // The weights, drawn as the mechanism rather than as a heatmap of everything. // A linear map is a bipartite graph of units; a convolution is a set of taps // over a receptive field; an SSM is a bank of states with a decay each, an // input map and an output map. Those are three different pictures because they // are three different mechanisms, and rendering all of them as coloured // rectangles would throw away the only thing the diagram is for. const POS = "#ffc061"; // a positive weight const NEG = "#6d9eff"; // a negative one const DIM = "#4a5878"; const S = { card: { background: "#101725", border: "1px solid #1f2a3d", borderRadius: 8, padding: "9px 11px", marginBottom: 9, }, head: { display: "flex", alignItems: "baseline", gap: 7, flexWrap: "wrap", marginBottom: 6, }, name: { fontSize: 12, color: "#c3cdde", fontWeight: 600 }, role: { fontSize: 10, padding: "1px 6px", borderRadius: 99, background: "#1c2740", color: "#9fb3d9", letterSpacing: 0.4, }, shape: { fontSize: 11, color: "#6b7a96", fontVariantNumeric: "tabular-nums" }, note: { fontSize: 10.5, color: "#5d6b84", marginTop: 5, lineHeight: 1.45 }, finding: (level) => ({ display: "flex", gap: 7, alignItems: "baseline", fontSize: 11.5, lineHeight: 1.45, padding: "5px 9px", borderRadius: 6, marginBottom: 4, background: level === "warn" ? "#26202c" : "#141d22", border: `1px solid ${level === "warn" ? "#5c4426" : "#22402f"}`, color: level === "warn" ? "#f0c88a" : "#9dcaa8", }), hist: { display: "block", marginTop: 6 }, empty: { color: "#5d6b84", fontSize: 12.5, padding: "18px 4px", lineHeight: 1.5, }, }; const colour = (v, absmax) => { const t = absmax ? Math.min(1, Math.abs(v) / absmax) : 0; return { stroke: v >= 0 ? POS : NEG, opacity: 0.08 + 0.9 * t ** 0.7, width: 0.4 + 1.6 * t, }; }; // --- a matrix, drawn as a matrix -------------------------------------------- // A dense map's structure is "every entry is free", and the way to show that is // every entry. Drawing it as a bipartite graph means hundreds of crossing // lines: a hairball that reads as one grey smudge, which is neither pretty nor // informative. Lines are kept only for maps small enough that the wires are // individually visible. function Matrix({ values, absmax, width = 250, maxCell = 12, gamma = 0.6 }) { const rows = values.length; const cols = values[0]?.length ?? 0; const cell = Math.max( 2, Math.min(maxCell, Math.floor(width / Math.max(cols, 1))), ); const w = cols * cell; const h = rows * cell; return ( {values.map((row, r) => row.map((v, c) => { const t = absmax ? Math.min(1, Math.abs(v) / absmax) : 0; return ( = 0 ? POS : NEG} opacity={0.04 + 0.96 * t ** gamma} /> ); }), )} ); } // Small enough that individual wires are legible — the picture "everything is // connected to everything" only lands when you can count the wires. function WireDiagram({ tensor }) { const rows = tensor.values.length; // output units const cols = tensor.values[0]?.length ?? 0; // input units const absmax = Math.max(1e-9, tensor.stats.absmax); const h = Math.max(120, Math.max(rows, cols) * 9); const w = 250; const x0 = 34; const x1 = w - 34; const yOf = (i, n) => (n <= 1 ? h / 2 : 12 + (i * (h - 24)) / (n - 1)); const edges = []; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const v = tensor.values[r][c]; const { stroke, opacity, width } = colour(v, absmax); if (opacity < 0.12) continue; // below this it is a smudge, not a weight edges.push( , ); } } return ( {edges} {Array.from({ length: cols }, (_, c) => ( ))} {Array.from({ length: rows }, (_, r) => ( ))} in out ); } // --- a convolution: the same taps applied everywhere ------------------------- function ConvDiagram({ tensor }) { const taps = tensor.values[0]?.length ?? 0; const absmax = Math.max(1e-9, tensor.stats.absmax); const row = tensor.values[0] ?? []; const w = 250; const h = 108; const y0 = 22; const y1 = h - 30; const xOf = (i) => (taps <= 1 ? w / 2 : 26 + (i * (w - 52)) / (taps - 1)); return ( {row.map((v, i) => { const { stroke, opacity, width } = colour(v, absmax); return ( ); })} {row.map((v, i) => ( ))} receptive field — {tensor.shape[tensor.shape.length - 1]} taps one output position ); } // --- an SSM: states that decay, with an input and an output map -------------- // Assembled from whichever of the roles the layer actually has, so an S4D // (decay + C + D) and a Mamba (decay + D + projections) each draw the parts // they really own instead of a common denominator that flatters both. function SSMDiagram({ decay, input, output, skip }) { const src = decay ?? input ?? output; if (!src) return null; const flat = src.values.flat(); const n = Math.min(12, flat.length); const w = 250; const h = 132; const cx = w / 2; const yOf = (i) => (n <= 1 ? h / 2 : 26 + (i * (h - 62)) / (n - 1)); // A decay parameter is stored as a log; what matters visually is how long a // state remembers, so it is mapped to a 0..1 retention rather than drawn raw. const decays = decay ? decay.values .flat() .slice(0, n) .map((v) => Math.exp(-Math.exp(Math.min(6, v)))) : new Array(n).fill(0.5); const inVals = input ? input.values.flat() : null; const outVals = output ? output.values.flat() : null; const inMax = input ? Math.max(1e-9, input.stats.absmax) : 1; const outMax = output ? Math.max(1e-9, output.stats.absmax) : 1; return ( x y {Array.from({ length: n }, (_, i) => { const y = yOf(i); const ret = decays[i]; const bIn = inVals ? colour(inVals[i % inVals.length], inMax) : null; const cOut = outVals ? colour(outVals[i % outVals.length], outMax) : null; return ( {/* the self-loop: how much of this state survives a step */} ); })} {skip && ( <> D skip )} {n} of {src.shape.reduce((a, b) => a * b, 1)} states · loop = retention ); } // --- anything with no shape worth drawing ------------------------------------ function StripDiagram({ tensor }) { const flat = tensor.values.flat(); const absmax = Math.max(1e-9, tensor.stats.absmax); const n = Math.min(flat.length, 48); const w = 250; const h = 26; const bw = w / n; return ( {flat.slice(0, n).map((v, i) => { const t = Math.min(1, Math.abs(v) / absmax); return ( = 0 ? POS : NEG} opacity={0.25 + 0.7 * t} /> ); })} ); } function LinearDiagram({ tensor }) { const rows = tensor.values.length; const cols = tensor.values[0]?.length ?? 0; if (Math.max(rows, cols) <= 12) return ; return ( ); } // --- the one picture every family shares ------------------------------------ // Each family is a map over positions along the swept axis; what differs is the // structure that map is *forced* to have. Side by side on the same axes, the // difference is the picture: a convolution is a narrow band repeated down every // diagonal, an SSM is lower-triangular and fading, a dense map fills the // square. Nothing here is inferred from the class name — it is measured off the // mixer's own impulse response. function OperatorPanel({ operator, mixer }) { if (!operator) return null; const { causal, bandwidth, tied, size } = operator; const notes = []; if (causal >= 0.99) { notes.push("causal — nothing reaches backwards in the sweep"); } else if (causal <= 0.6) { notes.push( `bidirectional — ${Math.round(100 * (1 - causal))}% of the influence runs backwards`, ); } else { notes.push(`mostly causal (${Math.round(100 * causal)}% of the influence)`); } if (bandwidth === 0) { notes.push( /attention/i.test(mixer ?? "") ? "no fixed off-diagonal structure: attention's mixing is computed from the data, so it is not in the parameters to draw" : "diagonal — this layer maps each position to itself", ); } else if (bandwidth < size - 1) { notes.push(`banded ±${bandwidth} — a position only reaches that far`); } else { notes.push("dense — every position reaches every other"); } if (tied >= 0.9 && bandwidth > 0) { notes.push( "weights tied along each diagonal: the same kernel, repeated at every position", ); } else if (tied < 0.6) { notes.push("untied — every position pair has its own weight"); } return (
position → position impulse response {size} × {size}
{/* A steep curve on purpose: an SSM's causal tail is a couple of percent of its local term, so a linear ramp renders the one structure worth seeing as black. Stated below rather than left as a flattering default. */}
Row i is what an impulse at position j does to position{" "} i. {notes.join(" · ")}. {operator.reach != null && ( <> {" "} Influence away from the diagonal is{" "} {(100 * operator.reach).toFixed(1)}% {" "} of the local term. Shading is |value|0.35, so a small tail stays visible. )}
); } // The distribution, which says things min/max/std cannot: the same three // numbers describe a healthy spread and a spike at zero with two outliers. function Histogram({ histogram }) { if (!histogram?.bins?.length) return null; const bins = histogram.bins; const peak = Math.max(...bins, 1); const w = 250; const h = 26; const bw = w / bins.length; return ( {bins.map((c, i) => { const t = c / peak; return ( 0 ? 1 : 0, t * h)} width={Math.max(1, bw - 0.6)} height={Math.max(c > 0 ? 1 : 0, t * h)} fill="#6d8ac9" opacity={0.45 + 0.5 * t} /> ); })} ); } // Findings, not a score. Each one carries the number that produced it, because // "this layer looks unhealthy" is not something anyone can act on. function Health({ notes }) { if (!notes?.length) return null; return (
{notes.map((n, i) => (
{n.level === "warn" ? "▲" : "●"} {n.text}
))}
); } function TensorCard({ tensor }) { const body = tensor.role === "linear" ? ( ) : tensor.role === "conv" ? ( ) : ( ); return (
{tensor.name} {tensor.role} {tensor.shape.join(" × ")}
{body}
{tensor.sampled ? `drawn as a ${tensor.rows}×${tensor.cols} stride-${tensor.stride.join("/")} sample of ${tensor.stats.n.toLocaleString()} weights` : `all ${tensor.stats.n.toLocaleString()} weights drawn`} {" · "}μ {tensor.stats.mean} · σ {tensor.stats.std} · |max|{" "} {tensor.stats.absmax}
); } export default function Weights({ weights, layerIndex }) { const layer = useMemo(() => { if (!weights?.layers?.length) return null; return ( weights.layers.find((l) => l.layer === layerIndex) ?? weights.layers[Math.min(layerIndex, weights.layers.length - 1)] ); }, [weights, layerIndex]); if (!weights) { return (
No weights to draw. The viewer was opened on a spec — a saved architecture, not a model — so there are no parameters to read. Open it with td.viz.show(model) to see them.
); } if (!layer) return
this layer has no parameters
; // The SSM parts are one mechanism, so they are drawn as one figure; the rest // of the tensors keep their own cards. const by = (role) => layer.tensors.find((t) => t.role === role); const ssm = { decay: by("ssm_decay"), input: by("ssm_in"), output: by("ssm_out"), skip: by("skip"), }; const hasSSM = !!(ssm.decay || ssm.input || ssm.output); const drawn = new Set( hasSSM ? [ssm.decay, ssm.input, ssm.output, ssm.skip] .filter(Boolean) .map((t) => t.name) : [], ); return (
{hasSSM && (
state-space recurrence ssm
Each circle is a state with its own decay: the loop shows how much of it survives one step. Edges in are B, edges out are C.
)} {layer.tensors .filter((t) => !drawn.has(t.name)) .map((t) => ( ))}
); }