Skip to content

v0.1.3

Latest

Choose a tag to compare

@Polichinel Polichinel released this 16 Oct 02:01
2b54560

Does this change a forecast for an existing config?

No. Every addition in 0.1.3 is opt-in and default-off. A config that does not set the new key behaves exactly as it did on 0.1.2 β€” verified by measurement, not assumption: the training loss was computed with and without the new per-task accumulator on identical input, and the total/regression/classification values matched to the bit.

(This question leads the notes because eight views-models models install this package through a range pin β€” views-hydranet~=0.1.1 β€” so they pick up a new version without choosing it. See #399.)

Why 0.1.3 and not 0.2.0

A new config field would normally argue for a minor bump. It is a patch deliberately: the pod runner pins >=0.1.1,<0.2.0, so a 0.2.0 would not be collected and would require an edit to production infrastructure in views-models. 0.1.3 is picked up with no runner change.


Added β€” body_nll_denominator

Selects what the zero-truncated negative-binomial body loss divides its per-cell losses by.

value meaning
"positives" default. The count of cells with a positive target β€” the historical reduction, unchanged.
"all_cells" Every supervised cell β€” the denominator the joint hurdle NLL actually has.

Why it exists. Setting output_distribution: 'truncated_nb' silently moved this denominator from all cells to positive cells. At a 1-in-222 positive rate that rescaled one task-loss term 65Γ—, and no config-identity check could see it, because arm_identity_check.py compares config values. Measured consequence on the shared trunk: the body's gradient-norm ratio against the gate went from 0.31 to 10.5 and cos(βˆ‡body, βˆ‡gate) flipped from +0.72 to βˆ’0.20, with 75% of training windows in conflict.

"all_cells" also replaces a random denominator with a fixed one. The per-step positive count is often 0 or 1 for a rare target (median 0.88 positives per window for non-state violence), so E[Ξ£/N_pos] β‰  E[Ξ£]/E[N_pos] β€” the "positives" gradient is biased and high-variance on rare targets.

Rejected where it would be inert, rather than accepted and ignored:

  • requires output_distribution: 'truncated_nb' β€” every other family already reduces over all cells
  • requires body_supervision: 'all' β€” under 'active' the supervision mask is a positive mask, so the denominator collapses back and the flag becomes bit-identical to "positives"
  • the rule is enforced once and invoked from two places, so a caller that bypasses the config schema (a raw dict driver) hits the same check

Added β€” per-task loss instrumentation

When trajectory_log_path is set, the trajectory CSV now carries one loss/<task> column per task (body and gate, per target) alongside the existing pooled loss_reg / loss_cls.

Previously the engine collapsed the balancer's task vector to two sums and nothing else, so a per-target loss question had to be reconstructed from saved weights after the fact. That is exactly how the 65Γ— above was found β€” weeks late.

Fixed β€” the trajectory log can come home from a pod run

Its parent directory is now created. A pod's deliver directory only contains artifact/, parquet/ and draws/, so an absolute path is the only way this file survives the run β€” and without the mkdir it raised FileNotFoundError after the data pull and the model build.


Not included

  • The balancer's learning rate never reaches its parameter group. _LRScheduler.step() zips against a length-1 get_lr(), so an unfrozen balancer trains at a constant 0.001 while the network decays. Pre-existing and latent β€” every views-models config pins freeze_multitask_balancer: True β€” and excluded deliberately rather than changing training dynamics inside a correctness release.
  • A CHANGELOG (#399).

How it was reviewed

/code-review medium β†’ /review-diff β†’ an independent guard audit in a clean context (36 mutations across 7 categories; 10 survivors, each addressed or recorded as an equivalent mutant) β†’ two further mutation rounds that each found another defect in the fixes.

That process caught, in order: the instrumentation shipping completely inert (a hand-built return dict dropped the key, so the CSV never gained a column); a guard that compared a list to itself; a validation rule stated twice unevenly so one caller skipped half of it; three stale interface contracts; and four separate guards that compared a value to itself or to the constant that produced it.

Final state: 2247 tests passing, guards mutation-verified, rehearsed on TestPyPI as 0.1.3.dev9 and installed back before this release was cut.

Full diff: v0.1.2...v0.1.3