juiceb0xc0de Claude commited on
Commit
e5ce61e
·
1 Parent(s): 074aaf8

perf(sub_zero): kill AtP per-pair thrash (autograd.grad + GPU reduction)

Browse files

Root cause of 51s/pair: the backward hook did g.detach().float().cpu() on
every scored weight, forcing up to ~96 CUDA syncs + ~10-22GB PCIe transfer
per pair and serializing the backward.

Fix:
- torch.autograd.grad over exactly the scored weights (no hooks, one pass)
- grad->SV reduction on GPU; only a tiny [rank] vector crosses to CPU
- drop gradient_checkpointing_enable() (scoped grad fixed the OOM; checkpointing
was doubling backward compute for no memory benefit on 48GB/80GB)

Expected: ~0.2-2s/pair on A100 vs 51s/pair. Resume/OOM-skip/fingerprint tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>

deploy_to_hf_space.py CHANGED
@@ -41,7 +41,8 @@ def main() -> None:
41
  repo_type="space",
42
  folder_path=str(args.local_dir),
43
  path_in_repo="",
44
- ignore_patterns=["outputs/*", "atlas/*", ".venv/*", "__pycache__/*", "*.pyc", ".DS_Store"],
 
45
  )
46
  print("[hf] upload complete")
47
 
 
41
  repo_type="space",
42
  folder_path=str(args.local_dir),
43
  path_in_repo="",
44
+ ignore_patterns=["outputs/*", "atlas/*", ".venv/*", "__pycache__/*", "*.pyc", ".DS_Store",
45
+ "*.tmp", "*.npz", "*.npy", "*.sqlite", "sub_zero_ckpt/*", "l*_census_raw*"],
46
  )
47
  print("[hf] upload complete")
48
 
qwip_atlas/sub_zero_surgery.py CHANGED
@@ -589,40 +589,44 @@ def _capture_atp_gradients(
589
  "data": atp_accum}, tmp)
590
  tmp.replace(ckpt_path) # atomic; a kill mid-write can't corrupt it
591
 
592
- for idx in tqdm(range(start_idx, n_pairs), desc="AtP gradient", unit="pair",
593
- initial=start_idx, total=n_pairs):
594
- param_grads: Dict[Tuple[int, str], torch.Tensor] = {}
595
- hooks = []
 
 
 
 
 
 
 
 
 
 
 
596
 
597
- for li, layer in enumerate(layers):
598
- if li not in proj_svd:
599
- continue
600
- for pname, pmod in _get_projection_map(layer).items():
601
- if pname not in proj_svd[li]:
602
- continue
603
- def _mk(li=li, pname=pname, w=pmod.weight):
604
- def _hook(g):
605
- param_grads[(li, pname)] = g.detach().float().cpu()
606
- hooks.append(w.register_hook(_hook))
607
- _mk()
608
 
 
 
609
  enc = tokenizer(corp_prompts[idx], return_tensors="pt", truncation=True, max_length=max_length)
610
- enc = {k: v.to(device) for k, v in enc.items()}
611
- model.zero_grad()
612
  try:
613
  out = model(**enc, use_cache=False)
614
  logits = out.logits[0, :-1, :]
615
  targets = enc["input_ids"][0, 1:]
616
  loss = F.cross_entropy(logits, targets)
617
- loss.backward()
618
  except torch.cuda.OutOfMemoryError:
619
  # A single pathological pair must not wall the whole stage. Free
620
  # what we can, drop this pair's partial state, advance, and flush
621
  # so a re-run does not retry the same OOMing pair forever.
622
- for h in hooks:
623
- h.remove()
624
- param_grads.clear()
625
- enc.clear()
626
  if torch.cuda.is_available():
627
  torch.cuda.empty_cache()
628
  gc.collect()
@@ -630,33 +634,25 @@ def _capture_atp_gradients(
630
  _flush(idx + 1)
631
  continue
632
 
633
- for h in hooks:
634
- h.remove()
635
-
636
- for li in range(len(layers)):
637
- if li not in proj_svd:
 
 
638
  continue
639
- for pname in proj_svd[li]:
640
- g_w = param_grads.get((li, pname))
641
- if g_w is None:
642
- continue
643
-
644
- u, s, vh = proj_svd[li][pname]
645
- c_act = corp_proj_acts[li].get(pname)
646
- a_act = auth_proj_acts[li].get(pname)
647
-
648
- if c_act is None or a_act is None or c_act.shape[-1] != vh.shape[1]:
649
- continue
650
 
651
- c_sv = (c_act @ vh.T).mean(0)
652
- a_sv = (a_act @ vh.T).mean(0)
653
- diff = c_sv - a_sv
654
 
655
- g_sv = (g_w @ vh.T).norm(dim=0)
656
- atp_accum[li][pname].append(diff * g_sv)
 
657
 
658
  # Explicit cleanup to prevent CUDA memory fragmentation across pairs.
659
- del loss, out, logits, targets, enc, param_grads
660
  if torch.cuda.is_available():
661
  torch.cuda.empty_cache()
662
  if idx % 50 == 0:
@@ -1467,8 +1463,11 @@ def build_brain_atlas(
1467
  # requires_grad on all ~8B params allocates a .grad buffer for every
1468
  # parameter during backward, which OOMs a 48GB card already holding a 16GB
1469
  # model; scoped grad keeps peak memory to the scored MLP projections.
1470
- # Gradient checkpointing trades a little compute for not retaining every
1471
- # layer's forward activations through backward.
 
 
 
1472
  scored_weights = []
1473
  for li, layer in enumerate(layers):
1474
  if li not in proj_svd:
@@ -1478,13 +1477,6 @@ def build_brain_atlas(
1478
  pmod.weight.requires_grad_(True)
1479
  scored_weights.append(pmod.weight)
1480
 
1481
- model.train()
1482
- try:
1483
- model.gradient_checkpointing_enable()
1484
- except Exception as _e:
1485
- print(f"[sub_zero_surgery] gradient checkpointing unavailable "
1486
- f"({_e.__class__.__name__}); proceeding without")
1487
-
1488
  try:
1489
  atp_scores = _capture_atp_gradients(
1490
  model, tokenizer, corp[:32], auth[:32], layers,
@@ -1492,13 +1484,8 @@ def build_brain_atlas(
1492
  ckpt_dir=ckpt_dir, fingerprint=fp_atp, ckpt_name="stage3_atp",
1493
  )
1494
  finally:
1495
- try:
1496
- model.gradient_checkpointing_disable()
1497
- except Exception:
1498
- pass
1499
  for w in scored_weights:
1500
  w.requires_grad_(False)
1501
- model.eval()
1502
  for p in model.parameters():
1503
  p.requires_grad_(False)
1504
  print(" AtP complete")
 
589
  "data": atp_accum}, tmp)
590
  tmp.replace(ckpt_path) # atomic; a kill mid-write can't corrupt it
591
 
592
+ # Build the flat list of scored (li, pname, weight) ONCE. Using
593
+ # torch.autograd.grad over exactly these leaves (instead of a full
594
+ # loss.backward() + a register_hook per weight) is the key perf fix: the
595
+ # old hook did g.detach().float().cpu() on every scored weight, which
596
+ # forced up to ~96 individual GPU->CPU syncs per pair and serialised the
597
+ # whole backward (~51 s/pair on a 6000 Ada). Now the grads stay on GPU and
598
+ # only one tiny [rank]-sized vector crosses to CPU per projection.
599
+ scored_params: List[Tuple[int, str, torch.nn.Parameter]] = []
600
+ for li, layer in enumerate(layers):
601
+ if li not in proj_svd:
602
+ continue
603
+ for pname, pmod in _get_projection_map(layer).items():
604
+ if pname in proj_svd[li] and hasattr(pmod, "weight") and pmod.weight.requires_grad:
605
+ scored_params.append((li, pname, pmod.weight))
606
+ scored_weight_list = [w for _, _, w in scored_params]
607
 
608
+ # Pre-stage the SVD right singular vectors on the model device once, so the
609
+ # per-pair grad->SV reduction is a single small GPU matmul (not a CPU one).
610
+ vh_gpu: Dict[Tuple[int, str], torch.Tensor] = {}
611
+ for li, pname, _ in scored_params:
612
+ _, _, vh = proj_svd[li][pname]
613
+ vh_gpu[(li, pname)] = vh.to(device=device, dtype=torch.float32, non_blocking=True)
 
 
 
 
 
614
 
615
+ for idx in tqdm(range(start_idx, n_pairs), desc="AtP gradient", unit="pair",
616
+ initial=start_idx, total=n_pairs):
617
  enc = tokenizer(corp_prompts[idx], return_tensors="pt", truncation=True, max_length=max_length)
618
+ enc = {k: v.to(device, non_blocking=True) for k, v in enc.items()}
 
619
  try:
620
  out = model(**enc, use_cache=False)
621
  logits = out.logits[0, :-1, :]
622
  targets = enc["input_ids"][0, 1:]
623
  loss = F.cross_entropy(logits, targets)
624
+ grads = torch.autograd.grad(loss, scored_weight_list, allow_unused=True)
625
  except torch.cuda.OutOfMemoryError:
626
  # A single pathological pair must not wall the whole stage. Free
627
  # what we can, drop this pair's partial state, advance, and flush
628
  # so a re-run does not retry the same OOMing pair forever.
629
+ del enc
 
 
 
630
  if torch.cuda.is_available():
631
  torch.cuda.empty_cache()
632
  gc.collect()
 
634
  _flush(idx + 1)
635
  continue
636
 
637
+ for (li, pname, _w), g_w in zip(scored_params, grads):
638
+ if g_w is None:
639
+ continue
640
+ _, _, vh = proj_svd[li][pname]
641
+ c_act = corp_proj_acts[li].get(pname)
642
+ a_act = auth_proj_acts[li].get(pname)
643
+ if c_act is None or a_act is None or c_act.shape[-1] != vh.shape[1]:
644
  continue
 
 
 
 
 
 
 
 
 
 
 
645
 
646
+ c_sv = (c_act @ vh.T).mean(0) # CPU, small ([rank])
647
+ a_sv = (a_act @ vh.T).mean(0) # CPU, small
648
+ diff = c_sv - a_sv # CPU, small
649
 
650
+ # grad->SV reduction on GPU; one tiny CPU transfer at the end.
651
+ g_sv = (g_w.float() @ vh_gpu[(li, pname)].T).norm(dim=0).cpu()
652
+ atp_accum[li][pname].append(diff * g_sv)
653
 
654
  # Explicit cleanup to prevent CUDA memory fragmentation across pairs.
655
+ del loss, out, logits, targets, enc, grads
656
  if torch.cuda.is_available():
657
  torch.cuda.empty_cache()
658
  if idx % 50 == 0:
 
1463
  # requires_grad on all ~8B params allocates a .grad buffer for every
1464
  # parameter during backward, which OOMs a 48GB card already holding a 16GB
1465
  # model; scoped grad keeps peak memory to the scored MLP projections.
1466
+ # No gradient checkpointing: with scoped grad the model + ~11GB of grad
1467
+ # buffers + batch-1 activations fit in 48GB (and trivially in 80GB), so
1468
+ # checkpointing would just double the backward compute by recomputing the
1469
+ # forward. The per-pair OOM-skip guard inside _capture_atp_gradients is the
1470
+ # safety net if a pathological pair ever exceeds headroom.
1471
  scored_weights = []
1472
  for li, layer in enumerate(layers):
1473
  if li not in proj_svd:
 
1477
  pmod.weight.requires_grad_(True)
1478
  scored_weights.append(pmod.weight)
1479
 
 
 
 
 
 
 
 
1480
  try:
1481
  atp_scores = _capture_atp_gradients(
1482
  model, tokenizer, corp[:32], auth[:32], layers,
 
1484
  ckpt_dir=ckpt_dir, fingerprint=fp_atp, ckpt_name="stage3_atp",
1485
  )
1486
  finally:
 
 
 
 
1487
  for w in scored_weights:
1488
  w.requires_grad_(False)
 
1489
  for p in model.parameters():
1490
  p.requires_grad_(False)
1491
  print(" AtP complete")