sweedworks

← all sources

checkattn.py

Gradient checks for the attention head

123 lines. This is the file the build actually runs, copied verbatim at build time.

"""Check the attention head in the browser against the Python reference.

Same arrangement as checkmlp.py, and for the same honest reason: training is too
deep in floating point for bit-identical output across engines. So each side
checks its own analytic gradients against finite differences — the real risk in
hand-written backprop through a softmax — and the two are compared on
initialisation, forward pass and attention weights, which are shallow enough to
agree exactly.
"""

import json
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "pylib"))
ROOT = os.path.abspath(os.path.join(HERE, ".."))

import quickjs  # noqa: E402

import attn  # noqa: E402
import task  # noqa: E402
from corpora import BERRIES  # noqa: E402

TOL = 1e-9


def js_context():
    ctx = quickjs.Context()
    ctx.set_memory_limit(1 << 30)
    ctx.set_max_stack_size(1 << 22)
    ctx.eval("var globalThis = this;")
    ctx.eval(open(os.path.join(ROOT, "attention", "attn.js"),
                  encoding="utf-8").read())
    return ctx


def main():
    ctx = js_context()
    failures = []

    copy_text, _ = task.make_corpus(120, seed=1)
    cases = [("copy-task", copy_text, True), ("copy-task-plain", copy_text, False),
             ("berries", BERRIES, True)]

    for name, text, shift in cases:
        vocab = attn.build_vocab(text)
        xs, ys = attn.make_examples(text, vocab)
        model = attn.Model(vocab, seed=1, shift_values=shift)

        ctx.set("_t", text)
        ctx.set("_shift", shift)
        ctx.eval("var vocab = Attn.buildVocab(_t);"
                 "var ex = Attn.makeExamples(_t, vocab);"
                 "var m = new Attn.Model(vocab, 1, {shiftValues: _shift});")

        if json.loads(ctx.eval("JSON.stringify(vocab)")) != vocab:
            failures.append(f"{name}: vocabulary differs")
            continue
        if json.loads(ctx.eval("JSON.stringify(ex.xs)")) != xs:
            failures.append(f"{name}: examples differ")
            continue
        if int(ctx.eval("m.parameterCount()")) != model.parameter_count():
            failures.append(f"{name}: parameter count differs")

        # Initialisation
        worst_init = 0.0
        for tensor in ("C", "P", "Wq", "Wk", "Wv", "Wo"):
            js = json.loads(ctx.eval(f"JSON.stringify(m.{tensor})"))
            py = getattr(model, tensor)
            for a_row, b_row in zip(py, js):
                for a, b in zip(a_row, b_row):
                    worst_init = max(worst_init, abs(a - b))
        if worst_init > TOL:
            failures.append(f"{name}: initial weights differ by {worst_init:.2e}")

        # Forward pass and, importantly, the attention weights themselves —
        # they are what the page shows the reader.
        worst_fwd = worst_attn = 0.0
        for probe in xs[:20]:
            py_probs, py_cache = model.forward(probe)
            ctx.set("_c", json.dumps(probe))
            ctx.eval("var out = m.forward(JSON.parse(_c));")
            js_probs = json.loads(ctx.eval("JSON.stringify(out.probs)"))
            js_w = json.loads(ctx.eval("JSON.stringify(out.w)"))
            for a, b in zip(py_probs, js_probs):
                worst_fwd = max(worst_fwd, abs(a - b))
            for a, b in zip(py_cache["w"], js_w):
                worst_attn = max(worst_attn, abs(a - b))
        if worst_fwd > TOL:
            failures.append(f"{name}: forward differs by {worst_fwd:.2e}")
        if worst_attn > TOL:
            failures.append(f"{name}: attention weights differ by {worst_attn:.2e}")

        # Each side checks its own derivatives.
        py_worst, py_fail = attn.gradcheck(model, xs[:6], ys[:6])
        ctx.eval("var gc = m.gradcheck(ex.xs.slice(0,6), ex.ys.slice(0,6), "
                 "{checks: 40});")
        js_worst = float(ctx.eval("gc.worst"))
        js_fail = int(ctx.eval("gc.failures.length"))
        if py_fail:
            failures.append(f"{name}: python gradients wrong ({len(py_fail)})")
        if js_fail:
            failures.append(f"{name}: browser gradients wrong ({js_fail})")

        print(f"PASS  {name:<16} grad py {py_worst:.1e} / js {js_worst:.1e}   "
              f"init {worst_init:.1e}   fwd {worst_fwd:.1e}   "
              f"attn {worst_attn:.1e}")

    print()
    if failures:
        print(f"{len(failures)} problem(s):")
        for f in failures:
            print(f"  - {f}")
        return 1
    print("Browser attention head and Python reference agree; "
          "both gradient checks pass.")
    return 0


if __name__ == "__main__":
    sys.exit(main())