9.1 NNx (thekaveh-nnx) findings¶
Note (2026-06-14): ml-eng-lab switched from a git submodule at
./nnxto thethekaveh-nnxPyPI distribution. Source paths cited below (e.g.nnx/src/nnx/nn/dataset/nn_dataset.py:24) refer to the upstreamthekaveh/NNxrepo, not a local submodule.
Findings surfaced by the verify_repo.py loop in the nnx (PyPI:
thekaveh-nnx) library. This is the durable triage ledger: actionable gaps
link to upstream issues, released fixes retain release evidence, and documented
non-bugs record their explicit disposition.
9.1.1 Findings¶
Triage summary (last reviewed 2026-08-22): Triaged against NNx releases
v0.2.0 through v0.2.3 and upstream
main on 2026-08-22. "Upstream resolved" means the behavior is available in a
released NNx wheel; it does not imply availability under ml-eng-lab's retained
thekaveh-nnx[lm]==0.2.0 runtime.
| ID | Finding | Upstream disposition | Affected notebooks | Local status / remaining work |
|---|---|---|---|---|
| F1 | NNDataset uses one full-split batch for each omitted batch size |
Open documentation/API-default issue NNx #188; unchanged through v0.2.3 |
diffusion-mnist-ddpm-pytorch, moe-fmnist-mixture-of-experts-pytorch, self_supervised-fmnist-jepa-pytorch |
Resolved locally by Issue #69 with explicit public batch_sizes= values; no follow-up |
| F2 | nnx.deepen is function-preserving only for ReLU |
Released, enforced, documented design constraint; this is not an open upstream bug | model_surgery-mnist-ffnn-pytorch |
Notebook deliberately uses ReLU and documents the constraint; no follow-up |
| F3 | NNx 0.2.0 NNTabularDataset is classification-only |
Resolved by NNx #81 in v0.2.2 with target_dtype |
tabular_regression-diabetes-mlp-pytorch |
Manual float-target loader remains under 0.2.0; coordinated upgrade/migration tracked by ml-eng-lab #146 |
| F4 | EarlyStopping() defaults to a classification-only validation field that is unset (None) for regression EDPs |
Open callback-contract issue NNx #189; unchanged through v0.2.3 |
tabular_regression-diabetes-mlp-pytorch guidance |
Use monitor="val_edp.loss"; the notebook does not instantiate the callback, so no local code follow-up |
| F5 | Training completion output embeds an absolute run path | Open portability issue NNx #190; unchanged through v0.2.3 |
Any notebook that captures NNx training output | Verifier rule E13.stale_active_notebook_path rejects leaked paths; no local follow-up before an upstream release |
9.1.1.1 NNDataset default batch_size packs the whole train set into one batch¶
Upstream disposition: NNx #188 tracks prominent public documentation of the default and its training consequence without silently changing the backward-compatible runtime contract.
Release evidence: NNx v0.2.0 through v0.2.3 and current main declare
batch_sizes=(None, None, None) and resolve each None to the corresponding
complete split size. Source comments and tests state the semantic, while the
generated public class entry exposes only the signature.
Affected notebooks: diffusion-mnist-ddpm-pytorch,
moe-fmnist-mixture-of-experts-pytorch, and
self_supervised-fmnist-jepa-pytorch. text_generation-tinyshakespeare-transformer-pytorch
has the same small-batch training need, but it uses an intentional custom
sequence-window dataset rather than bypassing NNDataset.
NNDataset(ds_class=thv.datasets.MNIST, ...)'s train_loader defaults to batch_size=54000 (the whole 60k train set minus the val carve-off). For full-batch SGD on classifiers this is fine; for diffusion / MoE / transformer / JEPA / any task that needs many noise- or routing-level samples per epoch, one batch per epoch is far too few โ the train step runs ~1 time per epoch and the loss barely budges.
Before Issue #69, each affected notebook worked around this with:
from torch.utils.data import DataLoader
train_loader = DataLoader(ds.train_loader.dataset, batch_size=128, shuffle=True)
Local workaround/status: Issue #69 resolved the loader bypass.
nnx.NNDataset accepts a
batch_sizes: tuple[Optional[int], Optional[int], Optional[int]] = (None, None, None)
constructor argument (nnx/src/nnx/nn/dataset/nn_dataset.py:24). Diffusion and MoE now pass
batch_sizes=(128, None, None); JEPA passes batch_sizes=(128, 128, None) because its linear-probe
path also consumes the validation loader in 128-sample batches. All three notebooks use the
wrapper-owned loaders directly. This removes the internal .dataset bypass while preserving
batch size, shuffle, worker, split, seed, batch-count, and recorded-output semantics. The
TinyShakespeare notebook intentionally remains on its custom sequence-window dataset.
Remaining work: None in ml-eng-lab. The supported public batch_sizes=
contract fully resolves the local workaround. NNx #188 owns discoverability of
the unchanged upstream default for new callers.
9.1.1.2 nnx.deepen is function-preserving only for Activations.RELU¶
Upstream disposition: Accepted, documented design constraint; this is not an open upstream bug. Net2DeeperNet's identity insertion is exactly function-preserving only when the inserted activation is idempotent on the preceding activation's range. A different constant bias cannot make arbitrary sigmoid, tanh, or GELU compositions equal to the original function.
Release evidence: NNx v0.2.0 and every later release through v0.2.3
document the ReLU equation in src/nnx/surgery/deepen.py, state the restriction
in the public docstring, and raise a targeted ValueError for unsupported
activations. Current main retains the same contract.
Affected notebooks: model_surgery-mnist-ffnn-pytorch.
nnx.deepen(net, after_layer_name=...) inserts an identity-init Linear after a target Linear. The identity init only preserves the forward output when the activation between the original Linear and the new Linear is ReLU (since ReLU(I x) == ReLU(x) for any x; sigmoid/tanh/GELU pass non-negative and negative values through differently).
On any non-ReLU activation the surgery raises ValueError: deepen: activation is 'leaky_relu', but identity-init insertion is function-preserving only for ReLU. at construction.
Local workaround/status: The notebook deliberately constructs its baseline
with Activations.RELU, asserts post-surgery forward equivalence, and documents
the ReLU-only boundary in its README and task specification.
Remaining work: None. A future distinct non-function-preserving deepening operation would be a new NNx feature, not a fix for this contract.
9.1.1.3 NNTabularDataset coerces targets to torch.long (classification-only)¶
Upstream disposition: Resolved by NNx #81.
Release evidence: NNx v0.2.2 added
NNTabularDataset(target_dtype=torch.float32), skips classification label
validation in regression mode, sets output_dim=1, and yields targets shaped
(batch, 1). The v0.2.2 changelog and released tests cover the contract.
Affected notebooks: tabular_regression-diabetes-mlp-pytorch.
The retained NNx 0.2.0 contract's NNTabularDataset(..., target_col=...) hard-codes y = torch.tensor(..., dtype=torch.long) in __post_init__. This is correct for classification but breaks regression: Losses.MEAN_SQUARED_ERROR expects float32 targets of shape (N, 1).
Regression notebooks must build the DataLoaders manually:
DataLoader(
TensorDataset(
torch.from_numpy(X).float(),
torch.from_numpy(y).float().unsqueeze(-1),
),
...,
)
The NNx 0.2.0 NNTabularDataset docstring says "For regression, prefer to
construct the DataLoaders yourself", accurately documenting the retained
runtime but not providing the newer released API.
Local workaround/status: Issue #61 proved the 0.2.2 surface in an isolated canonical-wheel trial, then retained 0.2.0 because the default Atlas JupyterHub runtime remained on 0.2.0. The notebook therefore keeps its manual seeded split and float-target loader.
Remaining work: ml-eng-lab #146
owns a coordinated root/Atlas NNx upgrade and the subsequent migration to
target_dtype=torch.float32. Do not remove the workaround before that runtime
qualification passes.
9.1.1.4 EarlyStopping(monitor=...) default is "val_edp.error", which is unset (None) for regression EDPs¶
Upstream disposition: NNx #189 tracks an explicit regression-safe default/fallback contract.
Release evidence: NNx v0.2.0 through v0.2.3 and current main default
to monitor="val_edp.error". When the selected field is absent or None,
_lookup_monitored() returns None and on_epoch_end() returns without
advancing the best value or patience counter.
Affected notebooks: tabular_regression-diabetes-mlp-pytorch documents the
contract in ยง6 but does not instantiate EarlyStopping.
EarlyStopping's default works for classification (lower error = better). For
regression the evaluation data point has loss but may have no error value,
so monitor="val_edp.loss" must be passed explicitly. The default does not
raise a clear error; it silently skips the stopping update.
Local workaround/status: All regression guidance requires the explicit
monitor="val_edp.loss" form. No active notebook relies on the unsafe default.
Remaining work: None in current ml-eng-lab code. NNx #189 owns the upstream callback behavior; future regression notebooks must keep the explicit monitor until a fixed released contract is adopted.
9.1.1.5 training completion messages expose absolute run paths¶
Upstream disposition: NNx #190 tracks one portable display-path contract shared by both training entry points.
Release evidence: In NNx v0.2.0 through v0.2.3 and current main,
NNModel.train() and Trainer.train() compute the human-facing message with
os.path.join(os.getcwd(), "runs", run.id). NNRun.save() persists the run but
does not print this message.
Affected notebooks: Any notebook that captures NNx training output can record the executing worktree, JupyterHub mount, container path, or host-local directory.
The finding was surfaced by historical active notebook outputs carrying baked-in local paths
such as maintainer worktrees, JupyterHub mounts, removed in-repo source trees,
and host-local Python environments. The 2026-07-04 maintenance pass normalized
the remaining active-notebook artifacts and added verifier rule
E13.stale_active_notebook_path; Issue #72 extends that rule to reject the
generic Run saved to <absolute-path>/runs/<id> signature on Unix and Windows.
python scripts/verify_repo.py --check execution --fast therefore rejects
these stale path artifacts in active notebooks while allowing portable relative
run paths.
The two training entry points emit a confirmation string with the absolute filesystem path of the saved run directory. Two related issues follow:
- Execution-environment path leak: any committed notebook output can carry the path from whatever machine, container, or worktree last executed it. This is reproducibility noise because the path is meaningless to readers outside that runtime.
- CI normalization is not sufficient: CI now writes Tier-A re-execution results to a temporary artifact tree and deliberately leaves committed notebook snapshots untouched. It therefore cannot (and should not) be used to normalize machine-local paths in tracked output.
NNx #190 proposes a path relative to the effective runs root or current working directory while leaving persistence and semantically required machine-local metadata unchanged.
Local workaround/status: Keep active notebook outputs free of stale
machine-local paths through E13.stale_active_notebook_path, and avoid
claiming that a CI re-run alone makes these outputs portable. Once nnx prints a
relative run path, a maintainer can deliberately refresh the committed snapshot
with make run-tier-a without reintroducing environment-specific paths.
Remaining work: None before an upstream release. The verifier remains load-bearing while NNx #190 is open and until ml-eng-lab adopts the released change.