Skip to content

feat: layer freezing PR1 — create-time trainable knob, optimizer skip, dx-only backward (#380)#392

Merged
LeoBuron merged 1 commit into
developfrom
feat/freezing-pr1-trainable-flag
Jul 21, 2026
Merged

feat: layer freezing PR1 — create-time trainable knob, optimizer skip, dx-only backward (#380)#392
LeoBuron merged 1 commit into
developfrom
feat/freezing-pr1-trainable-flag

Conversation

@LeoBuron

Copy link
Copy Markdown
Member

Part of #380 (PR1 of 3 — do not auto-close the epic).

What

Create-time layer freezing: a trainable_t tri-state knob (zero-init = trainable, mirroring bias_t) on the five parameter-layer init structs (linearInit_t, conv1dInit_t, conv1dTransposedInit_t, layerNormInit_t, groupNormInit_t). A layer built with .trainable = TRAINABLE_FALSE:

  • allocates no weight/bias grad tensors (parameter_t.grad == NULL) and no optimizer state (2 moment buffers/param saved under AdamW),
  • is invisible to the optimizer (calcNumberOfStatesByLayer → 0, collectTrainableParameters skips via the new layerIsFrozen(const layer_t*); the NULL-grad guard in validateOptimizerGradStorage stays as the mis-built-model fail-fast),
  • computes only dx in backward (weight/bias-grad blocks guarded in the GEMM family; LN/GN guard exactly the dgamma/dbeta accumulates + SYM grad-emission blocks — dx math and the SYM propLoss scale refresh are byte-identical, pinned by twin tests),
  • is skipped by traceModelGrads (sinks never receive NULL tensors).

All-frozen models fail fast at optimizer create — gated by modelHasFrozenLayer so pre-existing param-less-model configurations (Dropout/Pool-only) keep working.

Serialization: ODTS v2→v3 — per-parameter grad-presence byte (Leo's decision 2026-07-21), presence-vs-skeleton mismatch fail-fast before any tensor read; v2 files reject cleanly at the version check. Golden-bytes pin for the frozen record layout.

Out of scope (PR2 per epic): backward dx-chain truncation below the deepest trainable layer + propLoss wire elision. PR3: transfer-learning example.

Deviations from the plan (reviewed & approved)

  • The plan's unconditional sizeStates == 0 fail-fast broke shipped param-less-model tests — replaced by the modelHasFrozenLayer gate (plan's own "existing tests unchanged" constraint governs).
  • LN/GN vtable dispatchers' grad-dtype check got a minimal !cfg->frozen && short-circuit (it read grad->quantization before the statics — NULL-deref for frozen layers; behavior-preserving for non-frozen).

Known minors (deliberate, from review triage)

  • Factory doc blocks don't restate the knob (the init-struct field comment is the discovery point, matching bias_t/weightInit_t).
  • No golden-bytes pin for the hasGrad=1 record (behavioral round-trip covers it); Borrowing conv-factory variants untested for the knob (shared allocator path; Borrowing Linear covered by the e2e gate).
  • TraceApi.h: the frozen-grad-skip sentence sits in the shared doc block above traceModelWeights rather than under traceModelGrads' one-liner.
  • New Conv twin tests follow those files' pre-existing tolerant-of-leaks fixture convention.

Follow-up filed

Testing

  • TDD throughout with mutation checks per task (every new guard/branch verified to fail its test when inverted/removed).
  • Twin tests (frozen vs. trainable, identical fixtures): dx byte-identical for Linear/Conv1d/ConvT1d/LN/GN float + LN/GN SYM (incl. propLoss scale equality); frozen grads provably untouched.
  • E2E gate: frozen Linear → Relu → trainable Linear, 5 real SGD steps — frozen weights/bias byte-identical, trainable weights changed, loss decreased.
  • Full local CI green: ctest 81/81, pytest 29/29 (ci devenv script), examples preset built all targets (rot-guard, run twice during the stack). ASan/arm_cross run in GitHub CI.

🤖 Generated with Claude Code

@LeoBuron
LeoBuron force-pushed the feat/freezing-pr1-trainable-flag branch from 6c9ac7c to 29dd48e Compare July 21, 2026 15:01
…sion, optimizer skip, dx-only backward, ODTS v3

Squash of the 8-task stack + final-review fix wave (all per-task reviewed,
tree identical to CI-green 6c9ac7c):
- trainable_t tri-state (zero-init = trainable) on the 5 param-layer init
  structs; bool frozen on the config structs; layerIsFrozen(const layer_t*)
- factories elide grad tensors + optimizer states for frozen layers
- optimizer count/collection skip + gated all-frozen fail-fast
  (modelHasFrozenLayer; param-less models keep working)
- backward guards: GEMM family skips weight/bias-grad ops; LN/GN guard
  dgamma/dbeta accumulates + SYM emission blocks (dx + scale refresh
  byte-identical, twin-tested); traceModelGrads skips frozen layers
- ODTS v2->3: per-parameter grad-presence byte, skeleton-mismatch fail-fast

Part of #380 (PR2: dx-chain truncation + wire elision).

Co-Authored-By: Claude Fable 5 <[email protected]>
@LeoBuron
LeoBuron force-pushed the feat/freezing-pr1-trainable-flag branch from 29dd48e to ef9886d Compare July 21, 2026 17:50
@LeoBuron
LeoBuron merged commit ef9886d into develop Jul 21, 2026
12 checks passed
@LeoBuron
LeoBuron deleted the feat/freezing-pr1-trainable-flag branch July 21, 2026 19:02
Sign up for free to 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