codemaivanngu commited on
Commit
cdb8f01
·
verified ·
1 Parent(s): 23891bb

add padding-trigger ablation for the 4096-cap parity bug

Browse files
Files changed (1) hide show
  1. diag/diagnose_padding_trigger.py +165 -0
diag/diagnose_padding_trigger.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Find which property of the capture's own arrays makes a padded forward differ.
2
+
3
+ Confirmed on `failure-zusi05xj`: a padded-batch eager forward reproduces the trainer's
4
+ stored logprobs BIT-EXACTLY (max 0.0) while a per-row forward reproduces the engine's
5
+ (max 0.33); the two differ by 1.7597 max on 150 positions. A synthetic sweep that copied
6
+ the capture's geometry (lengths 506/224/4096/644, longest at index 2, pad id 0, train
7
+ mode, position_ids = cumsum-1 with pads forced to 1) reports max|delta logit| = 0.00000
8
+ for every variant. So the trigger is a property of the capture's DATA, not its shape.
9
+
10
+ This ablates the real arrays one input at a time and reports the content-independent
11
+ statistic `max |delta logit|` between the padded forward and the per-row forward:
12
+
13
+ base ids + attention_mask exactly as stored, position_ids = cumsum-1
14
+ base_nopos same, but no position_ids passed
15
+ pad_zero padded region of every row replaced by token id 0
16
+ attn_prefix attention_mask rebuilt as a plain prefix-ones mask from its own sum
17
+ attn_all_ones attention_mask replaced by all ones
18
+ long_only_pad single-row batch of the long row, padded to the stored width
19
+ long_only the long row alone at its own length (no padding at all)
20
+
21
+ If `attn_prefix` or `pad_zero` collapses the delta, that names the trigger.
22
+
23
+ Read-only and structure-only: it prints mask shape statistics and deltas, never prompt
24
+ text or token ids.
25
+
26
+ CUDA_VISIBLE_DEVICES=0 bash $SRC/experiments/runai/python-b200-host.sh \
27
+ /tmp/diagnose_padding_trigger.py "$CAPDIR" --out /tmp/pad_trigger.json
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import argparse
33
+ import json
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+
38
+ def max_logit_delta(a, b, chunk: int = 256) -> dict[str, Any]:
39
+ """max |a - b| over the vocabulary, per position, in chunks."""
40
+ n = int(min(a.shape[0], b.shape[0]))
41
+ worst, worst_pos = 0.0, -1
42
+ means: list[float] = []
43
+ for lo in range(0, n, chunk):
44
+ hi = min(lo + chunk, n)
45
+ d = (a[lo:hi].float() - b[lo:hi].float()).abs()
46
+ means.append(float(d.mean()))
47
+ m = float(d.max())
48
+ if m > worst:
49
+ worst, worst_pos = m, lo + int(d.max(dim=1).values.argmax())
50
+ del d
51
+ return {"positions": n, "max_abs_dlogit": worst, "worst_position": worst_pos,
52
+ "mean_abs_dlogit": sum(means) / len(means) if means else 0.0}
53
+
54
+
55
+ def main() -> None:
56
+ ap = argparse.ArgumentParser()
57
+ ap.add_argument("capture", type=Path)
58
+ ap.add_argument("--out", type=Path, default=None)
59
+ ap.add_argument("--dtype", choices=("bfloat16", "float32"), default="bfloat16")
60
+ ap.add_argument("--row", type=int, default=-1, help="row to compare; -1 = longest")
61
+ args = ap.parse_args()
62
+
63
+ import torch
64
+ from transformers import AutoModelForCausalLM
65
+
66
+ meta = json.loads((args.capture / "metadata.json").read_text())
67
+ batch = torch.load(args.capture / "batch.pt", map_location="cpu", weights_only=True)
68
+ ids = batch["stu_input_ids"].long()
69
+ attn = batch["stu_attn_mask"].long()
70
+ lengths = [int(v) for v in attn.sum(-1).tolist()]
71
+ width = int(ids.shape[1])
72
+ r = args.row if args.row >= 0 else max(range(len(lengths)), key=lambda i: lengths[i])
73
+
74
+ report: dict[str, Any] = {
75
+ "capture": str(args.capture), "rows": int(ids.shape[0]), "width": width,
76
+ "lengths": lengths, "row": r,
77
+ "model_training": meta.get("model_training"),
78
+ "attention_backend": meta.get("attention_backend"),
79
+ }
80
+ print("TRIGGER_META=" + json.dumps(
81
+ {k: report[k] for k in ("rows", "width", "lengths", "row", "model_training")},
82
+ sort_keys=True), flush=True)
83
+
84
+ # Structural description of the masks only: counts and whether the ones form a
85
+ # plain prefix. No token ids and no text leave the machine.
86
+ struct = []
87
+ for i in range(ids.shape[0]):
88
+ a = attn[i]
89
+ ones = int(a.sum())
90
+ first_zero = int((a == 0).nonzero()[0]) if bool((a == 0).any()) else -1
91
+ struct.append({"row": i, "ones": ones, "first_zero": first_zero,
92
+ "is_prefix_ones": bool(int(a[:ones].sum()) == ones)})
93
+ report["mask_structure"] = struct
94
+ print("TRIGGER_MASK_STRUCTURE=" + json.dumps(struct), flush=True)
95
+
96
+ model = AutoModelForCausalLM.from_pretrained(
97
+ args.capture / "student", dtype=getattr(torch, args.dtype),
98
+ attn_implementation="eager", local_files_only=True,
99
+ trust_remote_code=False).cuda()
100
+ model.train(bool(meta.get("model_training")))
101
+
102
+ n_r = int(lengths[r])
103
+ lone_ids = ids[r:r + 1, :n_r].clone()
104
+
105
+ with torch.inference_mode():
106
+ ref = model(input_ids=lone_ids.cuda(), use_cache=False).logits[0]
107
+
108
+ def forward_row(x, a, positions):
109
+ with torch.inference_mode():
110
+ out = model(input_ids=x.cuda(),
111
+ attention_mask=(a.cuda() if a is not None else None),
112
+ position_ids=(positions.cuda() if positions is not None else None),
113
+ use_cache=False).logits
114
+ return out[r] if out.dim() == 3 else out[0]
115
+
116
+ def positions_for(a):
117
+ p = a.long().cumsum(-1) - 1
118
+ p.masked_fill_(a == 0, 1)
119
+ return p
120
+
121
+ pos_stored = positions_for(attn)
122
+ prefix = torch.zeros_like(attn)
123
+ for i, L in enumerate(lengths):
124
+ prefix[i, :L] = 1
125
+ pad_ids = torch.zeros((1, width), dtype=torch.long)
126
+ pad_attn = torch.zeros((1, width), dtype=torch.long)
127
+ pad_ids[0, :n_r] = ids[r, :n_r]
128
+ pad_attn[0, :n_r] = 1
129
+
130
+ plans: list[tuple[str, Any, Any, Any]] = [
131
+ ("base", ids, attn, pos_stored),
132
+ ("base_nopos", ids, attn, None),
133
+ ("pad_zero", torch.where(attn.bool(), ids, torch.zeros_like(ids)), attn, pos_stored),
134
+ ("attn_prefix", ids, prefix, None),
135
+ ("attn_all_ones", ids, torch.ones_like(attn), None),
136
+ ("long_only_pad", pad_ids, pad_attn, None),
137
+ ("long_only", lone_ids, torch.ones_like(lone_ids), None),
138
+ ]
139
+ variants: dict[str, dict[str, Any]] = {}
140
+ for name, x, a, p in plans:
141
+ got = forward_row(x, a, p)
142
+ if name == "long_only":
143
+ variants[name] = max_logit_delta(got, ref)
144
+ else:
145
+ variants[name] = max_logit_delta(got, ref)
146
+ del got
147
+ torch.cuda.empty_cache()
148
+ print("TRIGGER_VARIANT=" + json.dumps({"variant": name, **variants[name]},
149
+ sort_keys=True), flush=True)
150
+
151
+ report["variants"] = variants
152
+ ranked = sorted((v["max_abs_dlogit"], k) for k, v in variants.items())
153
+ report["smallest_delta_variant"] = ranked[0][1] if ranked else None
154
+ print("TRIGGER_RANKED=" + json.dumps(
155
+ {"smallest": ranked[0][1] if ranked else None,
156
+ "deltas": {k: round(v["max_abs_dlogit"], 6) for k, v in variants.items()}},
157
+ sort_keys=True), flush=True)
158
+ if args.out:
159
+ args.out.write_text(json.dumps(report, indent=2, sort_keys=True, default=str) + "\n")
160
+ print("TRIGGER_OUT=" + str(args.out), flush=True)
161
+ print("TRIGGER_DONE=1", flush=True)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()