brandonmusic commited on
Commit
d096124
·
verified ·
1 Parent(s): 9a22f03

Field notes + I/O fix: RAM-resident calibration state, per-expert row memo, selected-rows-only split hashing, vectorized sketch

Browse files
FIELD_NOTES.md ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Field notes: making this run on real hardware
2
+
3
+ This file records every problem hit while running the encoder on rented GPUs
4
+ for the first time, what caused it, and what fixed it. All fixes are already
5
+ applied in the code and scripts in this repository. If you are reproducing
6
+ this work, you should not have to rediscover any of these.
7
+
8
+ Two runs were used: 8x B200 (vast.ai) and 6x B300 SXM6 (runpod).
9
+
10
+ ## Environment problems
11
+
12
+ **Torch 2.11 is gone from the CUDA 13.2 index.** The original build used
13
+ torch 2.11.0 with a prebuilt exllamav3 0.0.43 wheel. Those wheels are no
14
+ longer downloadable, and the prebuilt extension will not load against a newer
15
+ torch (it fails with an undefined symbol on at::cuda::getCurrentCUDABlasHandle).
16
+ Fix: build the extension from the v0.0.43 source tag against whatever torch you
17
+ are on. The source archive is in run_scripts/. Set TORCH_CUDA_ARCH_LIST to your
18
+ architecture: 10.0 for B200, 10.3 for B300.
19
+
20
+ **Transformers must be pinned to 5.12.1.** The runtime adapter imports
21
+ GlmMoeDsaNaiveMoe from transformers.models.glm_moe_dsa.modeling_glm_moe_dsa.
22
+ That class was refactored away in later 5.x releases, so 5.14.1 fails at
23
+ import. setup_box.sh pins the correct version.
24
+
25
+ **Remove torchvision and torchaudio before running.** Cloud images commonly
26
+ ship them built against an older torch. Transformers imports torchvision if it
27
+ is present, and the ABI mismatch raises "operator torchvision::nms does not
28
+ exist" during model import. setup_box.sh uninstalls both and asserts they are
29
+ gone. On Ubuntu you need --break-system-packages or the uninstall silently
30
+ does nothing.
31
+
32
+ **NVIDIA apt repo conflicts.** Some images already carry the CUDA repository.
33
+ Adding the keyring package again produces a Signed-By conflict that breaks
34
+ apt entirely. Check for an existing entry before adding one.
35
+
36
+ ## Code problems found by running it
37
+
38
+ **The provenance collector initialized CUDA.** Reading the cuDNN version
39
+ through torch.backends.cudnn.version() initializes a CUDA context. The runtime
40
+ then refuses to start, correctly, because its determinism contract requires a
41
+ clean CUDA state. Both behaviors were intentional and they collided. Fix: the
42
+ collector now parses the cuDNN build string from torch.__config__.show(),
43
+ which touches no device.
44
+
45
+ **Startup verification was single threaded.** Every launch re-hashes every
46
+ shard of both checkpoints before doing any work. On 1.7 TB that took about 26
47
+ minutes each time, including every failed attempt. Fix: the shard hashing now
48
+ runs 16 ways in parallel, which brings it to roughly 3 minutes with identical
49
+ failure semantics.
50
+
51
+ **The search phase re-read the entire calibration state from disk.** This was
52
+ the expensive one. Layer 3 sat for over 90 minutes with all six GPUs idle and
53
+ about 70 CPU cores busy. The process had read 607 GB against a 13 GB working
54
+ set, meaning the same state files were being re-read roughly 47 times: once
55
+ per row request, and the search phase makes dozens of those per layer. On top
56
+ of that, the fit/holdout split was computed by hashing every row in the corpus
57
+ for every expert, when only the rows that expert actually selected matter
58
+ (about 3 percent of them). Fix: bound batches are loaded once per layer and
59
+ held in RAM, per-expert row selections are memoized, and the split hash runs
60
+ only on selected rows. The diagnosis tool that found it was reading
61
+ /proc/PID/io on the live process; if a phase looks slow with idle GPUs, check
62
+ read_bytes before assuming the math is slow.
63
+
64
+ ## Things that are slow by design, not bugs
65
+
66
+ The carried-prefix pass at the start of the walk runs the corpus through the
67
+ three carried dense layers on one GPU. It is bounded, one time, and writes
68
+ crash-consistent state for every prompt, so it is dominated by hashing and
69
+ file writes rather than GPU math.
70
+
71
+ The inventory hashing is single threaded per file stream. It is the price of
72
+ the provenance guarantee that the walk encoded exactly the bytes it claims.
73
+
74
+ ## Performance measured on real hardware
75
+
76
+ Encode cost per tensor is about 0.85 s on B300 and 0.74 s on a clean B200, and
77
+ it is flat across 3, 4, and 5 bits. The trellis dynamic program is integer and
78
+ occupancy bound, so B300 gives no per-GPU advantage over B200 on this workload
79
+ despite its much faster FP4 and FP8 tensor cores. Plan capacity by GPU count,
80
+ not by generation.
81
+
82
+ One GPU on a contended host was measured at 0.84 s/tensor while a foreign
83
+ workload held it at 100 percent utilization, versus 0.74 s clean. If you find
84
+ GPUs busy that you did not start, they are probably another tenant's; you
85
+ cannot clear them from inside a container, and the cost is smaller than you
86
+ would expect, but you should still open a support ticket.
r7_encoder/glm52_backend.py CHANGED
@@ -661,7 +661,29 @@ class GLM52Backend(Round7Backend):
661
  else "fit"
662
  )
663
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
  def _iter_bound_batches(self, capture: LayerCapture, shards: Iterable[StateShard]):
 
 
 
665
  manifest = read_json(capture.routing_dir / "CAPTURE.json")
666
  records = manifest["sidecars"]
667
  for shard in tuple(shards):
@@ -696,22 +718,45 @@ class GLM52Backend(Round7Backend):
696
 
697
  if split not in ("fit", "holdout"):
698
  raise ValueError("row split must be fit or holdout")
 
 
 
 
 
 
 
 
 
699
  for _, batch, ids, weights, row_ids in self._iter_bound_batches(
700
  capture, shards
701
  ):
702
  chosen = ids == expert
703
  selected = chosen.any(dim=1)
704
- split_mask = torch.tensor(
 
 
 
 
 
 
705
  [
706
- self._split(capture.layer, expert, int(value)) == split
707
- for value in row_ids
 
708
  ],
709
  dtype=torch.bool,
710
  )
711
- mask = selected & split_mask
712
- if mask.any():
713
  route_weight = (weights * chosen).sum(dim=1)
714
- yield ExpertRows(batch.hidden[mask], route_weight[mask], row_ids[mask])
 
 
 
 
 
 
 
715
 
716
  def _fallback_locations(
717
  self,
 
661
  else "fit"
662
  )
663
 
664
+ def _bound_batches_cached(self, capture: LayerCapture, shards):
665
+ """Load the layer's bound batches ONCE into RAM and reuse.
666
+
667
+ The prior design re-read 1,773 state files plus 1,773 routing
668
+ sidecars from disk on every row request; the search phase issues
669
+ dozens of such requests per layer. One resident copy (~13 GB
670
+ against 2 TB of RAM) serves them all. Keyed by capture digest so
671
+ layer N+1 replaces layer N; contents are treated as read-only.
672
+ """
673
+ key = (capture.digest, tuple(shard.shard_id for shard in shards))
674
+ cached = getattr(self, "_bound_cache", None)
675
+ if cached is not None and cached[0] == key:
676
+ return cached[1]
677
+ batches = list(self._iter_bound_batches_raw(capture, shards))
678
+ self._bound_cache = (key, batches)
679
+ # a new layer invalidates every per-expert row memo as well
680
+ self._expert_rows_memo = {}
681
+ return batches
682
+
683
  def _iter_bound_batches(self, capture: LayerCapture, shards: Iterable[StateShard]):
684
+ yield from self._bound_batches_cached(capture, tuple(shards))
685
+
686
+ def _iter_bound_batches_raw(self, capture: LayerCapture, shards: Iterable[StateShard]):
687
  manifest = read_json(capture.routing_dir / "CAPTURE.json")
688
  records = manifest["sidecars"]
689
  for shard in tuple(shards):
 
718
 
719
  if split not in ("fit", "holdout"):
720
  raise ValueError("row split must be fit or holdout")
721
+ memo = getattr(self, "_expert_rows_memo", None)
722
+ if memo is None:
723
+ memo = self._expert_rows_memo = {}
724
+ memo_key = (capture.digest, expert, split)
725
+ cached_rows = memo.get(memo_key)
726
+ if cached_rows is not None:
727
+ yield from cached_rows
728
+ return
729
+ collected = []
730
  for _, batch, ids, weights, row_ids in self._iter_bound_batches(
731
  capture, shards
732
  ):
733
  chosen = ids == expert
734
  selected = chosen.any(dim=1)
735
+ if not selected.any():
736
+ continue
737
+ # The split only matters where the expert selected the row, so
738
+ # compute the per-row hash on the ~selected subset instead of the
739
+ # full batch. Identical outcome: mask == selected & split_mask.
740
+ sel_index = selected.nonzero(as_tuple=True)[0]
741
+ keep = torch.tensor(
742
  [
743
+ self._split(capture.layer, expert, int(row_ids[int(i)]))
744
+ == split
745
+ for i in sel_index
746
  ],
747
  dtype=torch.bool,
748
  )
749
+ final_index = sel_index[keep]
750
+ if final_index.numel():
751
  route_weight = (weights * chosen).sum(dim=1)
752
+ rows = ExpertRows(
753
+ batch.hidden[final_index],
754
+ route_weight[final_index],
755
+ row_ids[final_index],
756
+ )
757
+ collected.append(rows)
758
+ yield rows
759
+ memo[memo_key] = tuple(collected)
760
 
761
  def _fallback_locations(
762
  self,
r7_encoder/search.py CHANGED
@@ -270,17 +270,30 @@ class SearchRunner:
270
 
271
  matrix = torch.as_tensor(weight).detach().to("cpu", dtype=torch.float32)
272
  n, k = matrix.shape
273
- buckets = [0.0] * 32
274
- absolute = 0.0
 
 
 
 
275
  for index in range(512):
276
  coordinate = derive_seed(seed, index, bits=64)
277
- row = coordinate % n
278
- column = (coordinate // n) % k
279
- value = float(matrix[row, column]) * float(suh[column]) * float(svh[row])
280
- buckets[index % len(buckets)] += value
281
- absolute += abs(value)
 
 
 
 
 
 
 
 
 
282
  denominator = max(absolute * absolute, 1e-30)
283
- return sum(value * value for value in buckets) / denominator
284
 
285
  def _proxy_expert(
286
  self, search: LayerSearch, expert: int, diagonal: Sequence[float] | None = None
 
270
 
271
  matrix = torch.as_tensor(weight).detach().to("cpu", dtype=torch.float32)
272
  n, k = matrix.shape
273
+ # Same 512 seeded coordinates and same arithmetic as the original
274
+ # scalar loop, gathered in ONE batched read. The scalar version issued
275
+ # 512 individual element reads per call (a device sync each when the
276
+ # weight lived on CUDA), which dominated the entire search phase.
277
+ rows = []
278
+ cols = []
279
  for index in range(512):
280
  coordinate = derive_seed(seed, index, bits=64)
281
+ rows.append(coordinate % n)
282
+ cols.append((coordinate // n) % k)
283
+ row_t = torch.tensor(rows, dtype=torch.long)
284
+ col_t = torch.tensor(cols, dtype=torch.long)
285
+ suh_t = torch.as_tensor(list(suh), dtype=torch.float32)
286
+ svh_t = torch.as_tensor(list(svh), dtype=torch.float32)
287
+ values = (
288
+ matrix[row_t, col_t].double()
289
+ * suh_t[col_t].double()
290
+ * svh_t[row_t].double()
291
+ )
292
+ buckets = torch.zeros(32, dtype=torch.float64)
293
+ buckets.index_add_(0, torch.arange(512, dtype=torch.long) % 32, values)
294
+ absolute = float(values.abs().sum())
295
  denominator = max(absolute * absolute, 1e-30)
296
+ return float(buckets.square().sum()) / denominator
297
 
298
  def _proxy_expert(
299
  self, search: LayerSearch, expert: int, diagonal: Sequence[float] | None = None