File size: 8,050 Bytes
f0975ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"""Correctness checks for the Dory PyTorch port (dory/modeling.py).

  norms     weight row/column norms (is the checkpoint unit-normalized, nGPT style?)
  nll       assistant-token NLL on held Beam SFT examples at 1-4 loops
  generate  greedy chat generation at 2 loops
  ckpt      gradient checkpointing gives identical loss and gradients
  train     a few full-parameter AdamW steps on 2 examples: loss must fall, no NaNs
"""

import argparse
import json
import sys
import time
from pathlib import Path

import torch

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from dory.data import encode_example  # noqa: E402
from dory.modeling import DoryForCausalLM  # noqa: E402
from transformers import PreTrainedTokenizerFast  # noqa: E402


def sync(dev):
    if dev == "mps":
        torch.mps.synchronize()
    elif dev == "cuda":
        torch.cuda.synchronize()


def check_norms(model):
    print("== weight norms (mean ± std); ~1.0 means unit-normalized along that dim")
    layer = model.backbone.layers
    named = {
        "embeddings [V,H]": model.backbone.embeddings.weight,
        "lm_head [V,H]": model.lm_head.weight,
        "L0 q_proj [HD,H]": layer[0].mixer.q_proj.weight,
        "L0 o_proj [H,HD]": layer[0].mixer.o_proj.weight,
        "L1 gate_proj [F,H]": layer[1].mixer.gate_proj.weight,
        "L1 down_proj [H,F]": layer[1].mixer.down_proj.weight,
        "L20 q_proj [HD,H]": layer[20].mixer.q_proj.weight,
    }
    for name, w in named.items():
        w = w.float()
        r, c = w.norm(dim=1), w.norm(dim=0)
        print(f"  {name:22s} rows {r.mean():.4f}±{r.std():.4f}   cols {c.mean():.4f}±{c.std():.4f}")


def load_examples(tok, n, max_len):
    out = []
    for line in open(ROOT / "data" / "final" / "sft_verified.jsonl"):
        ex = json.loads(line)
        enc = encode_example(tok, ex["messages"], max_len=max_len)
        if enc is not None:
            out.append((ex, *enc))
        if len(out) >= n:
            break
    return out


@torch.no_grad()
def check_nll(model, examples, dev):
    print("== assistant-token NLL on Beam traces (lower is better; garbage model ~>5)")
    model.eval()
    for loops in (1, 2, 3, 4):
        tot, cnt, t0 = 0.0, 0, time.time()
        for _, ids, labels in examples:
            loss, n = model(ids[None].to(dev), labels=labels[None].to(dev), n_loops=loops)
            tot += float(loss) * n
            cnt += n
        sync(dev)
        print(f"  loops={loops}: nll={tot / cnt:.4f} ppl={torch.tensor(tot / cnt).exp():.2f} "
              f"over {cnt} tokens ({time.time() - t0:.1f}s)")


@torch.no_grad()
def check_generate(model, tok, dev, prompt, max_new):
    print(f"== greedy generation, 2 loops: {prompt!r}")
    model.eval()
    text = tok.apply_chat_template([{"role": "user", "content": prompt}], tokenize=False, add_generation_prompt=True)
    ids = tok(text, add_special_tokens=False, return_tensors="pt")["input_ids"].to(dev)
    eos = {tok.convert_tokens_to_ids("<|im_end|>")}
    t0 = time.time()
    out = model.generate(ids, max_new_tokens=max_new, n_loops=2, eos_ids=eos)
    sync(dev)
    print(f"  ({out.shape[1]} tokens, {time.time() - t0:.1f}s)\n" + tok.decode(out[0]))


def check_ckpt(model, examples, dev):
    print("== gradient checkpointing equivalence")
    _, ids, labels = examples[0]
    ids, labels = ids[None, :256].to(dev), labels[None, :256].to(dev)
    labels[:, :8] = -100
    labels[:, 8:] = ids[:, 8:]
    layers = model.backbone.layers
    probe = [layers[2].mixer.q_proj.weight, layers[30].attn_alpha, layers[31].mixer.suv_up,
             layers[60].mixer.o_proj.weight, model.lm_head.logit_scale]
    results = []
    for ckpt in (False, True):
        model.train()
        model.backbone.gradient_checkpointing = ckpt
        model.zero_grad(set_to_none=True)
        loss, _ = model(ids, labels=labels, n_loops=2)
        loss.backward()
        results.append((float(loss), [p.grad.float().clone() for p in probe]))
    model.backbone.gradient_checkpointing = False
    model.zero_grad(set_to_none=True)
    (l0, g0), (l1, g1) = results
    diffs = [float((a - b).abs().max() / (a.abs().max() + 1e-12)) for a, b in zip(g0, g1)]
    print(f"  loss {l0:.6f} vs {l1:.6f}; max relative grad diff {max(diffs):.2e}; "
          f"grads finite: {all(torch.isfinite(g).all() for g in g0)}")


@torch.no_grad()
def check_pack(model, examples, dev):
    print("== packing equivalence: 2 docs packed with doc_ids/position_ids vs separately")
    model.eval()
    docs = [(ids[:400], labels[:400]) for _, ids, labels in examples[:2]]
    sep_sum, sep_n = 0.0, 0
    for ids, labels in docs:
        loss, n = model(ids[None].to(dev), labels=labels[None].to(dev), n_loops=2)
        sep_sum += float(loss) * n
        sep_n += n
    ids = torch.cat([d[0] for d in docs])
    labels = torch.cat([d[1] for d in docs]).clone()
    labels[len(docs[0][0])] = -100  # second doc's first token has no in-doc predecessor
    doc_ids = torch.cat([torch.full((len(d[0]),), i) for i, d in enumerate(docs)])
    pos = torch.cat([torch.arange(len(d[0])) for d in docs])
    loss, n = model(ids[None].to(dev), labels=labels[None].to(dev), n_loops=2,
                    doc_ids=doc_ids[None].to(dev), position_ids=pos[None].to(dev))
    print(f"  separate: {sep_sum / sep_n:.6f} over {sep_n} tokens; packed: {float(loss):.6f} over {n} tokens")


def check_train(model, examples, dev, steps, lr):
    print(f"== full-parameter AdamW smoke test: {steps} steps, lr={lr}, 2 loops, checkpointing on")
    model.train()
    model.gradient_checkpointing_enable()
    opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=0.0, foreach=False)
    batch = examples[:2]
    for step in range(steps):
        t0 = time.time()
        opt.zero_grad(set_to_none=True)
        tot = 0.0
        for _, ids, labels in batch:
            loss, _ = model(ids[None].to(dev), labels=labels[None].to(dev), n_loops=2)
            (loss / len(batch)).backward()
            tot += float(loss.detach()) / len(batch)
        gnorm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
        opt.step()
        model.normalize_matrices_()
        sync(dev)
        print(f"  step {step}: loss={tot:.4f} grad_norm={float(gnorm):.3f} ({time.time() - t0:.1f}s)", flush=True)
    check_norms(model)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--model", default=str(ROOT / "models" / "dory-4b"))
    ap.add_argument("--device", default="mps" if torch.backends.mps.is_available() else "cpu")
    ap.add_argument("--checks", default="norms,nll,generate,ckpt")
    ap.add_argument("--n-examples", type=int, default=6)
    ap.add_argument("--max-len", type=int, default=1536)
    ap.add_argument("--max-new", type=int, default=200)
    ap.add_argument("--prompt", default="What is 17 * 23? Reply with just the number.")
    ap.add_argument("--train-steps", type=int, default=8)
    ap.add_argument("--lr", type=float, default=1e-5)
    args = ap.parse_args()
    checks = set(args.checks.split(","))

    torch.manual_seed(0)
    t0 = time.time()
    tok = PreTrainedTokenizerFast.from_pretrained(args.model)
    model = DoryForCausalLM.from_pretrained(args.model, device=args.device)
    print(f"loaded on {args.device} in {time.time() - t0:.1f}s; "
          f"{sum(p.numel() for p in model.parameters()) / 1e9:.3f}B params")
    examples = load_examples(tok, args.n_examples, args.max_len)
    print(f"{len(examples)} SFT examples, lengths {[len(e[1]) for e in examples]}")

    if "norms" in checks:
        check_norms(model)
    if "nll" in checks:
        check_nll(model, examples, args.device)
    if "generate" in checks:
        check_generate(model, tok, args.device, args.prompt, args.max_new)
    if "pack" in checks:
        check_pack(model, examples, args.device)
    if "ckpt" in checks:
        check_ckpt(model, examples, args.device)
    if "train" in checks:
        check_train(model, examples, args.device, args.train_steps, args.lr)


if __name__ == "__main__":
    main()