Celsia's picture
Upload folder using huggingface_hub
ecc81b3 verified
Raw
History Blame Contribute Delete
18.4 kB
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 (
<svg
width="100%"
viewBox={`0 0 ${w} ${h}`}
shapeRendering="crispEdges"
style={{ display: "block", maxHeight: 260 }}
>
{values.map((row, r) =>
row.map((v, c) => {
const t = absmax ? Math.min(1, Math.abs(v) / absmax) : 0;
return (
<rect
key={`${r}-${c}`}
x={c * cell}
y={r * cell}
width={cell}
height={cell}
fill={v >= 0 ? POS : NEG}
opacity={0.04 + 0.96 * t ** gamma}
/>
);
}),
)}
</svg>
);
}
// 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(
<line
key={`${r}-${c}`}
x1={x0}
y1={yOf(c, cols)}
x2={x1}
y2={yOf(r, rows)}
stroke={stroke}
strokeOpacity={opacity}
strokeWidth={width}
/>,
);
}
}
return (
<svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
{edges}
{Array.from({ length: cols }, (_, c) => (
<circle key={`i${c}`} cx={x0} cy={yOf(c, cols)} r={2.6} fill={DIM} />
))}
{Array.from({ length: rows }, (_, r) => (
<circle
key={`o${r}`}
cx={x1}
cy={yOf(r, rows)}
r={2.6}
fill="#8fa3c8"
/>
))}
<text x={x0} y={h - 1} fill="#5d6b84" fontSize="8.5" textAnchor="middle">
in
</text>
<text x={x1} y={h - 1} fill="#5d6b84" fontSize="8.5" textAnchor="middle">
out
</text>
</svg>
);
}
// --- 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 (
<svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
{row.map((v, i) => {
const { stroke, opacity, width } = colour(v, absmax);
return (
<line
key={i}
x1={xOf(i)}
y1={y0}
x2={w / 2}
y2={y1}
stroke={stroke}
strokeOpacity={Math.max(0.18, opacity)}
strokeWidth={width}
/>
);
})}
{row.map((v, i) => (
<circle key={`t${i}`} cx={xOf(i)} cy={y0} r={3} fill={DIM} />
))}
<circle cx={w / 2} cy={y1} r={4.5} fill="#8fa3c8" />
<text x={w / 2} y={12} fill="#5d6b84" fontSize="8.5" textAnchor="middle">
receptive field — {tensor.shape[tensor.shape.length - 1]} taps
</text>
<text
x={w / 2}
y={h - 6}
fill="#5d6b84"
fontSize="8.5"
textAnchor="middle"
>
one output position
</text>
</svg>
);
}
// --- 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 (
<svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
<text x={22} y={12} fill="#5d6b84" fontSize="8.5" textAnchor="middle">
x
</text>
<text x={w - 22} y={12} fill="#5d6b84" fontSize="8.5" textAnchor="middle">
y
</text>
{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 (
<g key={i}>
<line
x1={22}
y1={h / 2}
x2={cx - 9}
y2={y}
stroke={bIn ? bIn.stroke : DIM}
strokeOpacity={bIn ? bIn.opacity : 0.35}
strokeWidth={bIn ? bIn.width : 0.7}
/>
<line
x1={cx + 9}
y1={y}
x2={w - 22}
y2={h / 2}
stroke={cOut ? cOut.stroke : DIM}
strokeOpacity={cOut ? cOut.opacity : 0.35}
strokeWidth={cOut ? cOut.width : 0.7}
/>
{/* the self-loop: how much of this state survives a step */}
<path
d={`M ${cx - 7} ${y - 4} A 8 8 0 1 1 ${cx + 7} ${y - 4}`}
fill="none"
stroke="#9b7cff"
strokeOpacity={0.2 + 0.8 * ret}
strokeWidth={0.6 + 1.8 * ret}
/>
<circle cx={cx} cy={y} r={3.4} fill="#8fa3c8" />
</g>
);
})}
<circle cx={22} cy={h / 2} r={4.5} fill={DIM} />
<circle cx={w - 22} cy={h / 2} r={4.5} fill="#8fa3c8" />
{skip && (
<>
<path
d={`M 22 ${h / 2 + 8} Q ${cx} ${h - 6} ${w - 22} ${h / 2 + 8}`}
fill="none"
stroke={POS}
strokeOpacity={0.5}
strokeWidth={1.1}
strokeDasharray="3 3"
/>
<text
x={cx}
y={h - 2}
fill="#6b7a96"
fontSize="8"
textAnchor="middle"
>
D skip
</text>
</>
)}
<text x={cx} y={12} fill="#5d6b84" fontSize="8.5" textAnchor="middle">
{n} of {src.shape.reduce((a, b) => a * b, 1)} states · loop = retention
</text>
</svg>
);
}
// --- 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 (
<svg width="100%" viewBox={`0 0 ${w} ${h}`} style={{ display: "block" }}>
{flat.slice(0, n).map((v, i) => {
const t = Math.min(1, Math.abs(v) / absmax);
return (
<rect
key={i}
x={i * bw}
y={h / 2 - (t * h) / 2}
width={Math.max(1, bw - 0.8)}
height={Math.max(1.5, t * h)}
fill={v >= 0 ? POS : NEG}
opacity={0.25 + 0.7 * t}
/>
);
})}
</svg>
);
}
function LinearDiagram({ tensor }) {
const rows = tensor.values.length;
const cols = tensor.values[0]?.length ?? 0;
if (Math.max(rows, cols) <= 12) return <WireDiagram tensor={tensor} />;
return (
<Matrix
values={tensor.values}
absmax={Math.max(1e-9, tensor.stats.absmax)}
/>
);
}
// --- 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 (
<div style={S.card}>
<div style={S.head}>
<span style={S.name}>position → position</span>
<span style={S.role}>impulse response</span>
<span style={S.shape}>
{size} × {size}
</span>
</div>
{/* 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. */}
<Matrix
values={operator.values}
absmax={Math.max(1e-9, operator.absmax)}
maxCell={14}
gamma={0.35}
/>
<div style={S.note}>
Row <i>i</i> is what an impulse at position <i>j</i> does to position{" "}
<i>i</i>. {notes.join(" · ")}.
{operator.reach != null && (
<>
{" "}
Influence away from the diagonal is{" "}
<b style={{ color: "#9fb0cc" }}>
{(100 * operator.reach).toFixed(1)}%
</b>{" "}
of the local term. Shading is |value|<sup>0.35</sup>, so a small
tail stays visible.
</>
)}
</div>
</div>
);
}
// 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 (
<svg width="100%" viewBox={`0 0 ${w} ${h}`} style={S.hist}>
{bins.map((c, i) => {
const t = c / peak;
return (
<rect
key={i}
x={i * bw}
y={h - Math.max(c > 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}
/>
);
})}
</svg>
);
}
// 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 (
<div style={{ marginBottom: 10 }}>
{notes.map((n, i) => (
<div key={i} style={S.finding(n.level)}>
<span>{n.level === "warn" ? "▲" : "●"}</span>
<span>{n.text}</span>
</div>
))}
</div>
);
}
function TensorCard({ tensor }) {
const body =
tensor.role === "linear" ? (
<LinearDiagram tensor={tensor} />
) : tensor.role === "conv" ? (
<ConvDiagram tensor={tensor} />
) : (
<StripDiagram tensor={tensor} />
);
return (
<div style={S.card}>
<div style={S.head}>
<span style={S.name}>{tensor.name}</span>
<span style={S.role}>{tensor.role}</span>
<span style={S.shape}>{tensor.shape.join(" × ")}</span>
</div>
{body}
<Histogram histogram={tensor.histogram} />
<div style={S.note}>
{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}
</div>
</div>
);
}
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 (
<div style={S.empty}>
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 <code style={{ color: "#8b95a8" }}>td.viz.show(model)</code> to see
them.
</div>
);
}
if (!layer) return <div style={S.empty}>this layer has no parameters</div>;
// 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 (
<div>
<Health notes={layer.health} />
<OperatorPanel operator={layer.operator} mixer={layer.mixer} />
{hasSSM && (
<div style={S.card}>
<div style={S.head}>
<span style={S.name}>state-space recurrence</span>
<span style={S.role}>ssm</span>
</div>
<SSMDiagram {...ssm} />
<div style={S.note}>
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.
</div>
</div>
)}
{layer.tensors
.filter((t) => !drawn.has(t.name))
.map((t) => (
<TensorCard key={t.name} tensor={t} />
))}
</div>
);
}