[RFC] kwyk pretrained TF->PyTorch weight converter with numerical parity - #385
Draft
dhritimandas wants to merge 2 commits into
Draft
[RFC] kwyk pretrained TF->PyTorch weight converter with numerical parity#385dhritimandas wants to merge 2 commits into
dhritimandas wants to merge 2 commits into
Conversation
The kwyk architecture (McClure et al. 2019) was reimplemented in PyTorch, but users could not obtain the trusted published weights -- the three SavedModels ship only inside the neuronets/kwyk Docker container, and no conversion path existed. - nobrainer/datasets/convert_kwyk.py: TF SavedModel/.npz -> KWYKMeshNet state_dict converter. Strict load, offline parity check over all 8 conv layers (including the classifier), and a .provenance.json sidecar recording the source path + SHA-256 -- the MAP and BD checkpoints are structurally indistinguishable, so the content hash is the only durable record of which model a .pth came from. - KWYKMeshNet: the output classifier is now an FFGConv3d (was plain nn.Conv3d), matching the TF logits/ layer, which is itself a full VWN conv. All five logits variables map 1:1 with no information loss; the previous mean-path collapse made MC uncertainty under-dispersed at the output (TF contributes ~11.5 of run-to-run logit variance there; the plain conv contributed 0). Also adds a bias flag threaded through the hidden layers (default False, unchanged). - ConcreteDropout3d clamp widened [0.05, 0.95] -> [0.01, 0.99] in lockstep with the converter: the published SSD checkpoint stores p up to 0.954055, so the old ceiling silently clipped 89% of trained values (598/672). A unit test pins the two clamp ranges together. - Biases are imported by default (--drop-bias is an explicit, logged opt-out): every published checkpoint carries bias_m/bias_a for every conv layer, and dropping them shifts output logits by max ~20. Every mapping claim is verified against the real container and validated numerically against the live TF graph: the converted MAP model reproduces the original's logits to max|diff| = 9.5e-05 (relative error ~4e-6) on identical input; converted SSD preserves all 672 dropout probabilities to 6e-08 and reproduces output-layer MC variance. Full verification report, including the six discrepancies found and fixed and a second independent re-verification: docs/kwyk_mapping_verification.md.
…container Neither offline_parity_check (weight mapping only) nor 05_compare_kwyk.py (GT-vs-GT, no model-to-model comparison, cannot currently load its model) verify that the converted PyTorch checkpoint reproduces the original kwyk container end to end. This adds that check: conform inside the container, z-score once on the host, feed identical input to both sides, compare raw logits (Stage A, MAP model) and MC-aggregate uncertainty (Stage B, SSD model) against a live neuronets/kwyk container.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What: This adds a converter. It imports the published kwyk TensorFlow
weights into the existing PyTorch
kwyk_meshnetarchitecture. It adds aharness. The harness checks the converted model against the original
neuronets/kwykcontainer, end to end.Why: The
kwyk_meshnetarchitecture (FFG/VWN convolutions, matchingMcClure et al. 2019) was already ported to PyTorch in an earlier change.
No PyTorch weights existed for it. This PR fills that one gap only: it
imports the three published TensorFlow checkpoints (MAP, BD, SSD) as
PyTorch state dicts, and validates that the import is numerically correct
against a live copy of the original container -- not just that the
weight-mapping code runs, but that the converted model reproduces the
original's actual output on real data.
This is opened as a draft RFC, not a mergeable-as-is change: two design
questions below need maintainer input before this is final (see "Open
questions").
The converter
nobrainer/datasets/convert_kwyk.py:.npz(preferred, noTensorFlow needed) or directly from a checkpoint (
--tf-path, requirestensorflow).v,g) and axisorder (
[k,k,k,in,out]->[out,in,k,k,k]) ontoFFGConv3d'sparameters, auto-detecting the TF layer index base (0 vs 1) rather than
assuming it.
offline_parity_checkindependently recomputes each layer's kernelfrom the raw TF
v/gand confirms the loaded model reproduces it --this gates the weight mapping, before any inference is run.
python -m nobrainer.datasets.convert_kwyk --npz ... --n-classes 50 --filters 96 --receptive-field 37 --out kwyk_map.pth, writing a.provenance.jsonsidecar (source checkpoint path + SHA256,architecture args) alongside the
.pth, since the MAP and BD checkpointsare structurally indistinguishable (identical key sets and shapes) and
the source path is the only durable record of which model a
.pthcamefrom.
Numerical parity (the part
offline_parity_checkcannot verify)offline_parity_checkgates the weight mapping only -- it says nothingabout whether the converted model reproduces the original through the
full pipeline (conform -> normalize -> block -> infer -> reassemble) on
real data.
scripts/kwyk_reproduction/07_parity_kwyk.pyruns both theconverted PyTorch model and a live
neuronets/kwykcontainer on identicalpreprocessed input and compares them directly to each other (not each to
ground truth independently, which is a different, weaker check).
Stage A (MAP model,
all_50_wn-- the only one of the threeSavedModels that is run-to-run deterministic) compares raw logits on a
real T1 volume (
sub-01, vianobrainer.utils.get_data()):Ran on a 64^3 crop (8 of 512 blocks), not the full 256^3 volume: the
container is
linux/amd64-only and runs under QEMU emulation on the arm64dev machine this was built on, making a full 512-block run impractically
slow for one sitting. The harness supports full-volume runs unmodified.
Stage B (SSD model,
all_50_bvwn_multi_prior) compares MC-aggregateuncertainty statistics on a 128^3 crop, N=20 samples (full designed scope,
~83 min wall clock under emulation) -- never per-sample, since the two
frameworks' RNGs are unrelated:
variance_scale_ratio = 0.816: PyTorch's reconciled variance runs ~18%below TF's in this crop. Plausible, unconfirmed cause: MC sample variance
is a much noisier statistic than the mean at N=20, and PyTorch's
ConcreteDropout3ddraws one dropout mask shared across the whole batchrather than independent per-sample masks -- a real implementation
difference that could suppress inter-sample variance relative to TF's
per-sample sampling. Not silently loosened to force a pass; see the second
open question below.
Open questions for maintainers
Where should the converter live? It currently sits in
nobrainer/datasets/convert_kwyk.py, alongsideopenneuro.pyandzarr_store.py-- installed package code, importable as a library. Theparity harness that validates it lives in
scripts/kwyk_reproduction/instead -- a standalone, non-packaged reproduction-pipeline directory.
Should the converter move to sit next to the harness it's validated
against, or does it belong with the other dataset/conversion utilities
regardless of where its own validation tooling lives?
What parity tolerance is actually acceptable? The thresholds above
were proposed as initial gates, explicitly meant to be pinned from a
first real observed run rather than picked in the abstract -- this PR
is that first run. Stage A's gates all clear with wide margin. Stage
B's variance gate does not (0.874 vs 0.90). Is 0.90 the right bar given
N=20 sampling noise on a second-moment statistic, should it be loosened
with N recorded as a caveat, or should the fix be a larger N (which
costs real wall-clock time under emulation) before this is called
validated?
Test plan
uv run pytest nobrainer/tests/unit/ -m "not gpu" -q-- 427 passed;one pre-existing unrelated failure
(
test_croissant.py::test_returns_true_on_valid).offline_parity_checkpasses for all 8 conv layers (incl.classifier) on both the MAP and SSD converted checkpoints.
(full 128^3/N=20 scope) -- see
docs/kwyk_parity_report.md.