fix(sp4): migrate winsorized adaptive grad-clip update to GPU per feedback_no_cpu_compute_strict

Layer C close-out C4 — the most architecturally substantive site of the
feedback_no_cpu_compute_strict sweep. GpuDqnTrainer::update_adaptive_clip
was running a 6-step host-side compute chain on GPU-produced inputs:
winsor (1) + cold-start sentinel + EMA (3) + scalar reduction (4) + ISV
upper-bound clamp (5) + mapped-pinned write (6). Per
feedback_no_partial_refactor the chain must migrate coherently — splitting
EMA-only into a kernel and leaving the surrounding scalar reductions on
host would be a partial migration violating the rule.

Migration: new update_adaptive_clip_kernel.cu (single-thread, single-block).
Takes host-passed observed_grad_norm (already a mapped-pinned readback)
+ 6 fixed structural constants (legacy values preserved per
feedback_no_quickfixes) + 4 mapped-pinned dev_ptrs + ISV[GRAD_CLIP_BOUND_INDEX].
Writes the full output chain (adaptive_clip_pinned, grad_norm_ema_pinned,
outlier_diag_pinned).

Storage migration:
- grad_norm_ema migrated from host-resident f32 to mapped-pinned scalar
  (matches C1/C2/C3 pattern).
- New outlier_diag_pinned mapped-pinned slot for the GRAD_CLIP_OUTLIER warn
  diagnostic. The kernel writes `delta = observed - clamped` and the host
  reads it post-launch to format the warn log without running scalar
  arithmetic on the host.

All structural constants preserved: EMA_BETA=0.95, CLIP_MULTIPLIER=2.0,
MIN_CLIP=1.0, GRAD_CLIP_OUTLIER_K=100, EPS_CLAMP_FLOOR (SP4). Same cold-start
sentinel `prev_ema <= 0.0 ⇒ assign clamped directly`; same Mech 6 (SP3) +
Layer B (SP4) bound design. Same outlier-warn log format reconstructed from
the mapped-pinned diagnostic slot.

Host-side early-return guard preserved (the pre-existing pattern from
C1 redesigned). grad_norm_emas_step_count counter unchanged (scalar
control-flow metadata, not compute, per the rule's explicit carve-out).

Verification: SP4 lib tests + 16 SP4 GPU producer unit tests pass on RTX 3050 Ti.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-01 15:09:57 +02:00
parent ca63158606
commit 1112abc2a4
4 changed files with 397 additions and 113 deletions

View File

@@ -3046,3 +3046,47 @@ Discovered during the C3 sweep (deferred):
- `gpu_experience_collector::set_utilization_ema` (gpu_experience_collector.rs:1886) — was originally listed as a 9th site but is just a setter `self.util_ema = util.clamp(0.0, 1.0)`, NOT EMA arithmetic (caller passes the already-EMA'd value). Misclassification in the original audit grid; no migration needed.
Refs: SP4 Layer C close-out C3 — third site of the `feedback_no_cpu_compute_strict` sweep.
### Layer C Task C4 — winsorized adaptive grad-clip update migrated to GPU (2026-05-01)
Fourth site of the `feedback_no_cpu_compute_strict` sweep, the most complex by structure: `GpuDqnTrainer::update_adaptive_clip` was running a 6-step host-side compute chain on GPU-produced inputs:
```
1. winsor: clamped = min(observed, K × max(prev_clip, 1e-3))
2. cold-start: if ema <= 0.0 { ema = clamped }
3. EMA: ema = β × ema + (1 - β) × clamped
4. scalar reduction: new_clip = max(ema × CLIP_MULTIPLIER, MIN_CLIP)
5. ISV upper bound: new_clip = min(new_clip, max(ISV[GRAD_CLIP_BOUND], EPS_CLAMP_FLOOR))
6. mapped-pinned write: adaptive_clip_pinned[0] = new_clip
```
All inputs are mapped-pinned scalars (raw_grad_norm from the gpu_training_guard readback, prev_clip from `adaptive_clip_pinned`, ISV[168] from `launch_sp4_grad_norm_p99`). Per `feedback_no_cpu_compute_strict` the entire chain belongs on GPU; per `feedback_no_partial_refactor` it must migrate coherently rather than splitting EMA-only into a kernel and leaving the surrounding scalar reductions on host.
Migrated:
- New `update_adaptive_clip_kernel.cu` — single-thread, single-block. Takes the host-passed `observed_grad_norm` (already a mapped-pinned readback) plus 6 fixed structural constants (legacy values preserved per `feedback_no_quickfixes`) and the 4 mapped-pinned dev_ptrs + ISV[GRAD_CLIP_BOUND_INDEX]; writes the full output chain (`adaptive_clip_pinned[0]`, `grad_norm_ema_pinned[0]`, `outlier_diag_pinned[0]`).
- Storage migration: `grad_norm_ema` migrated from host-resident `f32` field to mapped-pinned device-mapped scalar. New `outlier_diag_pinned` mapped-pinned slot for the GRAD_CLIP_OUTLIER warn diagnostic. Constructor allocates 2 new `cuMemHostAlloc(DEVICEMAP)` slots; Drop frees them.
- New `launch_update_adaptive_clip` Rust launcher chained on the trainer's stream — all four pinned slots are owned by the trainer.
- `update_adaptive_clip` host-side compute chain replaced with the launcher call + post-launch outlier-diag readback for the warn log (mapped-pinned + `__threadfence_system()` ⇒ value visible without explicit host sync).
- `grad_norm_ema_value()` accessor migrated to read through the pinned host_ptr.
Preserved (no behaviour change):
- Same fixed-α formula `EMA_BETA=0.95` and structural constants (`CLIP_MULTIPLIER=2.0`, `MIN_CLIP=1.0`, `GRAD_CLIP_OUTLIER_K=100`, `EPS_CLAMP_FLOOR` from `sp4_wiener_ema`).
- Same cold-start sentinel `prev_ema <= 0.0 ⇒ assign clamped directly`.
- Same winsor formula and ISV upper-bound clamp semantics — Mech 6 (SP3) + Layer B (SP4) bound design unchanged.
- Same outlier-warn log message format; recovered host-side from the `delta = observed - clamped` mapped-pinned diagnostic slot the kernel writes.
- Host-side early-return guard `!observed_grad_norm.is_finite() || observed_grad_norm <= 0.0` — kernel only launches when value is valid (matches the pre-existing pattern that `update_grad_norm_emas_kernel` from C1 redesigned uses).
- `grad_norm_emas_step_count` host counter unchanged (scalar control-flow metadata, not compute, per the rule's explicit carve-out).
- Downstream consumer chain unchanged: `adaptive_clip_pinned` still consumed by Adam clip kernel via `adaptive_clip_dev_ptr`; the fast/slow grad-norm EMAs (driving fold-warmup factor) still updated by the C1 redesigned launcher.
Verification:
- `cargo check -p ml --offline`: clean, same pre-existing warnings.
- `cargo test -p ml --lib --offline -- sp4 state_reset_registry`: 11 passed.
- `cargo test -p ml --test sp4_producer_unit_tests --offline --release -- --ignored`: 16/16 SP4 GPU tests pass on RTX 3050 Ti.
Files touched (Layer C C4):
- `crates/ml/src/cuda_pipeline/update_adaptive_clip_kernel.cu` — new kernel.
- `crates/ml/build.rs` — 1 new kernel registration.
- `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` — 1 new CUBIN static + 1 new struct field + 2 new mapped-pinned slot pairs (grad_norm_ema, outlier_diag) + 1 new constructor loader + 2 new pinned allocations + 2 new Drop entries + 1 new launcher (`launch_update_adaptive_clip`); host-side compute chain in `update_adaptive_clip` replaced with launcher call + post-launch warn readback; `grad_norm_ema_value()` accessor migrated to mapped-pinned read.
- `docs/dqn-wire-up-audit.md` — this entry.
Refs: SP4 Layer C close-out C4 — fourth site of the `feedback_no_cpu_compute_strict` sweep. The most architecturally substantive close-out so far (full 6-step compute chain migration vs. C1/C2/C3 single-EMA migrations).