HyperPEER / PHASE1_REPORT.md
MikeyBeez's picture
Add HyperPEER pipeline, testbed code, results, docs, Gradio landing
e41a3a4 verified
|
Raw
History Blame Contribute Delete
6.72 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade

Phase 1 β€” Gemma-4-26B-A4B teacher capture: feasibility & proof

Box: pop β€” RTX 5070 Ti (16 GB, sm_120 Blackwell), 31 GB RAM, torch 2.11+cu128, transformers 5.12, bitsandbytes 0.49.2. Date: 2026-06-18.

Goal (Phase 1 only): make the Gemma-4 text decoder runnable forward-only for activation capture on this hardware, and prove the capture works. No training yet.

TL;DR

  • It runs. The 26.5B teacher's text decoder does forward-only capture on the 16 GB card via bf16 layer-streaming (accelerate offload to the fast NVMe). Capture proven correct on a 32,768-token slice: all 30 layers, correct shapes, finite, Xβ‰ Y.
  • bnb-4bit does NOT work here β€” neither fully-resident (OOMs at load) nor offloaded (meta quant_state crash). Details below.
  • The real blocker is STORAGE, not compute. Storing block I/O (X and Y) for all 30 layers costs 330 KiB/token β†’ 6.15 TiB for 20M tokens, 15.4 TiB for 50M (50M exceeds the 6.3 TB free on the cache drive). And the cache drive is a slow USB HDD (111 MB/s write), which makes the capture write-bound at ~330 tok/s β†’ ~17 h for 20M tokens. This needs a decision before a real run β€” see NEEDS_MIKEY.txt.

What is captured (verified against the reference forward)

In Gemma4TextDecoderLayer.forward the FF/MoE sub-block is, after the attention residual:

residual = hidden_states                       # X  = FF-block input (post-attention)
h  = pre_feedforward_layernorm(residual)
h  = mlp(h)                                     # shared (dense) expert
if moe:                                         # 8-of-128 routed experts (GeGLU)
    h1 = post_feedforward_layernorm_1(h)
    _, w, idx = router(residual.flatten)
    h2 = experts(pre_feedforward_layernorm_2(residual.flatten), idx, w)
    h  = h1 + post_feedforward_layernorm_2(h2)
h  = post_feedforward_layernorm(h)             # D  = FF-block delta
hidden_states = residual + h                    # Y  = X + D = FF-block output
hidden_states *= layer_scalar                   # (AFTER the block β€” NOT captured)

Capture hooks: X = forward-PRE hook on pre_feedforward_layernorm (its input); D = forward hook on post_feedforward_layernorm (its output); stored Y = X + D. hidden_size_per_layer_input = 0, so the per-layer-input branch is inert. This is the exact block the student replaces, so (X, Y) is the correct feature-distillation pair.

Load methods evaluated

method result
bf16 layer-streaming (accelerate, NVMe offload) βœ… WORKS. device_map=auto, max_memory={0:6GiB, cpu:16GiB}, offload_folder on NVMe. 4 layers GPU-resident / 9 CPU / 23 disk. Reuses the real Gemma4 forward.
bnb-4bit, fully resident on GPU ❌ text decoder ~14.4 GiB; load OOMs on the final ~484 MiB expert tensor. No room to finish loading, let alone a forward. (The bnb 4-bit kernel runs fine on sm_120 β€” verified with a standalone quantize+matmul; it's purely the size.)
bnb-4bit + accelerate CPU/disk offload ❌ loads, but forward dies: offloaded quant_state tensors stay on meta β†’ NotImplementedError: Cannot copy out of meta tensor. (Also needed a monkeypatch just to get past hook-attach, where bnb does offset.item() on a meta tensor.) bnb-4bit is fundamentally not offloadable on this stack.
--cpu-layers (per-layer device_map β†’ CPU bf16) ❌ silently ignored by transformers 5.12's new core_model_loading; the '':0 catch-all wins, GPU use unchanged (identical OOM for cpu-layers 2 and 8).
disabling warmup / serializing the loader (1 worker) ❌ no effect on the 14.4 GiB resident wall.
AWQ / compressed-tensors / FP8 prequantized checkpoint ❌ none exists for this brand-new model; 4-bit AWQ would hit the same 14 GiB wall, FP8 (26 GiB) is far too big.

Two small, documented monkeypatches live in capture_gemma.py for the 4-bit diagnostic path only (patch_disable_warmup, patch_serial_load, patch_bnb_meta_quantstate); the working bf16 path needs none of them.

Throughput (measured)

  • Forward-only (no writes), bf16 streaming, ctx=256:
    • batch 16 β†’ 153 tok/s (peak 10.4 GiB)
    • batch 64 β†’ 558 tok/s (peak 12.1 GiB) β€” bigger batch amortises the ~29 s/forward NVMe weight transfer over more tokens. This is the lever; batch is bounded by the MoE tokenΓ—top_k activation expansion.
  • Full capture incl. writes (batch 16) β†’ 97 tok/s, fwd peak 7.06 GiB (write-bound).
  • Key infra: NVMe read 3.0 GB/s (teacher); USB-HDD write 111 MB/s (cache drive).

Capture proof (validation slice)

capture_gemma.py --val --batch 16 --shard-tokens 8192 β†’ /mnt/data/cache/gemma_cap_val (32,768 tokens). Verified by inspect_capture.py:

  • 30 layer dirs; each layerNN/{input,output}_NNNNN.pt, 4 shards/tensor (sharding works).
  • Shapes [8192, 2816] bf16, all finite; Xβ‰ Y with FF-block relative delta 0.93–3.36.
  • 330 KiB/token total (X+Y over 30 layers) = 11,264 B/token/layer.

Size & wall-time estimates for a real capture

At 330 KiB/token, and (for wall-time) batchβ‰ˆ64 so forward (β‰ˆ558 tok/s) exceeds the HDD write rate β†’ write-bound at β‰ˆ330 tok/s:

corpus cache size wall-time (write-bound) fits 6.3 TB free?
20M tokens 6.15 TiB ~17 h barely
50M tokens 15.4 TiB ~43 h NO

(If left at batch 16 it is forward-bound instead: ~57 h for 20M. Use batch β‰₯48.)

Recommendation / open decision (see /tmp/NEEDS_MIKEY.txt)

The forward problem is solved; the storage cost is the gate. Options, in order of my preference:

  1. Don't pre-materialize all 30 layers' X,Y to disk. Re-stream the teacher during training (compute distillation targets on the fly), or capture/train a few layers per pass. Removes the multi-TB cache entirely. (Phase-2 design change.)
  2. fp8 storage halves the cache (165 KiB/tok β†’ 3.1 TiB @ 20M) and roughly doubles the write-bound rate; verify distillation tolerates fp8 targets.
  3. Cap the budget at ~15–20M tokens on the existing USB HDD (~6 TiB, ~17 h) β€” feasible today but uses nearly all free space.
  4. Rent a one-off 80 GB GPU for a fast capture pass β€” fixes speed (full model resident, no offload, hours not days) but not storage; you'd still need ~6–15 TB of fast scratch, so pair it with option 1 or 2.

Files

  • capture_gemma.py β€” capture (bf16-stream default; --method 4bit reproduces the OOM).
  • test_stream_bf16.py β€” standalone streaming throughput probe.
  • inspect_capture.py β€” shard validation + size/time estimator.
  • Validation output: /mnt/data/cache/gemma_cap_val/ (meta.json + sample shards).