Skip to content

fix: arena frees are reset-epoch guarded -- stale GC-finalizer frees dropped (Bug 11 residual) - #138

Merged
dndungu merged 1 commit into
mainfrom
fix/arena-stale-free-epoch
Jun 12, 2026
Merged

fix: arena frees are reset-epoch guarded -- stale GC-finalizer frees dropped (Bug 11 residual)#138
dndungu merged 1 commit into
mainfrom
fix/arena-stale-free-epoch

Conversation

@dndungu

Copy link
Copy Markdown
Contributor

Problem

After the host-access stream-ordering fix (#137), Wolf CrossAsset GB10 f32
still corrupted: verify7 hit adamw: NaN detected in gradient of layer0.ffnB1_biases at batch 4 (was batch 3 pre-fix); verify8 under
ZTENSOR_ARENA_POISON=1 localized it to the forward pass — softmax
attention scores all-NaN mid-batch-4, i.e. a forward tensor read NaN-filled
(reclaimed) arena memory.

Root cause

An arena allocation's lifetime cannot extend across Reset: the rewind
reclaims it wholesale and the bump allocator re-issues its bytes. But
GPUStorage frees are GC-finalizer-driven and fire arbitrarily late.
The Wolf loop resets per sample; its first big Go GC lands deterministically
around batch 3–4 (heap growth is deterministic — which is why every prior
fix shifted the failing batch by one) and releases thousands of dead
pre-Reset storages. Each stale FreeArena:

  • under poison mode, NaN-fills bytes now owned by a LIVE current-epoch
    tensor (verify8's all-NaN softmax scores), and
  • without poison, inserts the block into the free-list, double-issuing it to
    a second live tensor — silent aliasing that surfaces as the deterministic
    gradient NaN in the AdamW guard (verify5/6/7).

Fix (contract level)

ArenaPool.Reset advances an epoch. Pool-backed GPUStorage captures
the epoch at allocation time (gpuapi.EpochMemPool) and releases through
FreeAtEpoch, which drops arena-range frees whose epoch has passed — the
Reset already reclaimed them. The epoch check, poison fill, and free-list
insert run in one critical section with Reset's epoch increment, so a
concurrent Reset cannot interleave. Views propagate the allocation epoch;
TrySet resize re-captures it and keeps allocSize in sync (a stale,
larger allocSize would free-list bytes of a neighboring live allocation).
Fallback/async-overflow pointers are not Reset-reclaimed and free
unconditionally, as before.

Tests

internal/cuda/arena_epoch_test.go (host-backed arena, no GPU): stale
cross-epoch free is dropped — no free-list double-issue, no poison fill over
live data; same-epoch free still enters the free-list and is reused.
tensor/gpu_storage_epoch_test.go: storage frees carry the allocation-time
epoch, including through refcounted views. go build, CI-equivalent
go vet, go test -race ./... green locally.

Bug 11 lineage: zerfoo#850, ztensor#134, ztensor#137.

…e a no-op
Bug 11 residual (Wolf GB10 batch-3/4 corruption). An arena allocation's
lifetime cannot extend across Reset: the rewind reclaims it wholesale and
the bump allocator re-issues its bytes to new allocations. But GPUStorage
frees are GC-finalizer-driven and can fire arbitrarily late -- the Wolf
training loop's first big GC lands around batch 3-4 and releases thousands
of dead pre-Reset storages. Each such FreeArena poison-filled (under
ZTENSOR_ARENA_POISON=1, verify8: forward softmax scores all-NaN) or
free-listed (without poison: double-issued block, silent aliasing ->
gradient NaN in the AdamW guard, verify5/6/7) memory owned by LIVE
current-epoch tensors.
Contract: ArenaPool.Reset advances an epoch; pool-backed GPUStorage
captures the epoch at allocation (gpuapi.EpochMemPool) and frees through
FreeAtEpoch, which drops arena-range frees whose epoch has passed. The
check, poison fill, and free-list insert run in one critical section with
Reset's epoch increment, so no interleaving can slip a stale block in.
Views propagate the allocation epoch; TrySet resize re-captures it and
keeps allocSize in sync.
Regression tests: stale free is dropped (no free-list double-issue, no
poison of live data), same-epoch free still reuses, storage frees carry
the allocation-time epoch through refcounted views.
@dndungu
dndungu merged commit 6f571ce into mainJun 12, 2026
1 check passed
dndungu added a commit that referenced this pull request Jun 12, 2026
…dst-form accumulation policy
design.md documents the contracts shipped for the GPU training
hardening plan in general terms:
- Host-Access Synchronization: host reads/writes of device memory are
stream-ordered via per-device registered sync hooks
(tensor.RegisterHostAccessSync); the GPU engine registers a
capture-guarded stream.Synchronize() (#137).
- Arena Reset: pinning (ADR 006 reset floor / MarkStepBoundary) and
reset epochs -- FreeAtEpoch drops cross-epoch frees, making
GC-finalizer-driven frees safe across Reset (#138).
- Engine dst-form accumulation policy: results write into dst's
existing storage, never re-homed to the arena.
- Graph features: Engine() accessor and the save-for-backward
lifetime contract (ADR 006).
devlog: end-to-end GB10 validation entry for #137/#138 (two clean
runs, zero NaN, accuracy within 0.05pp of the CPU baseline).
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@dndungu