Instructions to use brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Trellis
How to use brandonmusic/GLM-5.2-EXL3-TR3v4-3.5bpw-MTP78 with Trellis:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
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 +86 -0
- r7_encoder/glm52_backend.py +51 -6
- r7_encoder/search.py +21 -8
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
[
|
| 706 |
-
self._split(capture.layer, expert, int(
|
| 707 |
-
|
|
|
|
| 708 |
],
|
| 709 |
dtype=torch.bool,
|
| 710 |
)
|
| 711 |
-
|
| 712 |
-
if
|
| 713 |
route_weight = (weights * chosen).sum(dim=1)
|
| 714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
for index in range(512):
|
| 276 |
coordinate = derive_seed(seed, index, bits=64)
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
denominator = max(absolute * absolute, 1e-30)
|
| 283 |
-
return
|
| 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
|