Make SynDiff run on current PyTorch, and fix the validation/checkpoint/inference paths - #67
Open
mertcanozdemir wants to merge 21 commits into
Open
Make SynDiff run on current PyTorch, and fix the validation/checkpoint/inference paths#67mertcanozdemir wants to merge 21 commits into
mertcanozdemir wants to merge 21 commits into
Conversation
utils/op/fused_act.py and utils/op/upfirdn2d.py compiled their CUDA
extensions at import time via torch.utils.cpp_extension.load(). Since
`backbones` imports these transitively, the whole package was unimportable
on any machine without ninja and a matching CUDA toolchain -- including
CPU-only installs:
RuntimeError: Ninja is required to load C++ extensions
Both modules already contain pure-PyTorch fallbacks (upfirdn2d_native and
the CPU branch of fused_leaky_relu), but the unconditional load() ran
before they could ever be reached.
Build the extensions inside a try/except, warn once on failure, and route
the dispatch helpers to the native path when the extension is unavailable.
On machines that can build the kernels the behaviour is unchanged.
Also drops a leftover debug print of the module path.
train_syndiff() called torch.autograd.set_detect_anomaly(True) on every iteration, right before the generator backward pass. Anomaly mode records a stack trace for every autograd node and re-runs the backward with extra bookkeeping; it is a debugging aid, not something a training run should enable unconditionally. Left as-is it silently slows down every SynDiff training run.
load_checkpoint() rewrote every key as key[7:], assuming the checkpoint was written by a DistributedDataParallel-wrapped model. For a checkpoint saved without that wrapper the slice silently chops seven characters off every real parameter name, so load_state_dict() fails with a wall of unexpected / missing keys instead of loading the weights. Strip the prefix only when the key starts with it, which handles checkpoints from both DDP and single-process runs.
PyTorch 2.6 flipped the default of torch.load's weights_only argument to
True. content.pth stores the run's argparse.Namespace next to the tensors,
so --resume now aborts on every checkpoint the repo itself wrote:
_pickle.UnpicklingError: Weights only load failed.
... Unsupported global: GLOBAL argparse.Namespace
Load the resume checkpoint explicitly with weights_only=False. This file is
produced by the training run itself, so the relaxed unpickling is contained
to data the user already owns.
train.py hard-wired every run to CUDA + NCCL: init_processes() called
torch.cuda.set_device() and dist.init_process_group(backend='nccl')
unconditionally, train_syndiff() built device as 'cuda:{gpu}', and every
network was wrapped in DistributedDataParallel. A single-GPU run therefore
still had to stand up a NCCL process group, and a CPU-only run was
impossible -- which also made the training loop untestable without a GPU.
Set up a process group only when the run actually spans processes, fall
back to CPU when CUDA is unavailable, and wrap in DDP only when a process
group is live. Multi-process runs are unchanged: size > 1 takes exactly the
same path as before.
Because a single-process run now keeps the bare module, its state_dict has
no 'module.' prefix. load_model_state() normalises the prefix on resume so
checkpoints written by either layout keep loading.
Verified end to end on CPU with the sample data: 4 epochs of training plus
validation, then a --resume round-trip off the resulting content.pth.
val_l1_loss and val_psnr_values were allocated with args.num_epoch slots
along the epoch axis, but the training loop iterates over
range(init_epoch, args.num_epoch + 1). The last epoch therefore indexes one
past the end and the run dies right after finishing its final epoch of
training -- after the checkpoint is written, but before the metrics for that
epoch are recorded:
File "train.py", line 711, in train_syndiff
val_l1_loss[0,epoch,iteration]=abs(fake_sample1 -real_data).mean()
IndexError: index 1 is out of bounds for axis 1 with size 1
Allocate args.num_epoch + 1 slots to match the loop bound.
train_syndiff() runs two validation loops. The first feeds contrast2 as the source and reconstructs contrast1 with gen_diffusive_1. The second swaps the tuple to (y_val, x_val) so that the source is contrast1 and the target is contrast2 -- but it still sampled from gen_diffusive_1. gen_diffusive_1 is the network that produces contrast1, so the second loop asked it to synthesise the other contrast and scored the result against contrast2. Every number written to val_l1_loss[1] and val_psnr_values[1] was therefore meaningless, and gen_diffusive_2 was never validated at all. Sample from gen_diffusive_2 in the second loop.
sample_and_test() called torch.cuda.set_device() unconditionally, so
inference aborted on any machine without a visible GPU:
RuntimeError: No CUDA GPUs are available
Select the CUDA device only when one exists, mirroring what train.py now
does. Runs with a GPU are unaffected.
syn_im1/syn_im2 were preallocated as (256, 256, N) while every sample was
run through CenterCrop((256, 152)) two lines earlier, so the very first
assignment aborted:
syn_im1[:,:,iteration]=np.squeeze(fake_sample1.cpu().numpy())
ValueError: could not broadcast input array from shape (256,152)
into shape (256,256)
The crop output size is fixed, so this fired on the first test slice for
every dataset -- test.py could not write its im_syn.mat at all.
Collect the slices in a list and stack them once at the end, so the stored
volume always matches the cropped geometry. The crop itself undoes the
padding CreateDatasetSynthesis applies; its size is now exposed as
--crop_h/--crop_w, defaulting to the previous IXI/BRATS values so existing
commands keep producing the same output.
Verified end to end on CPU: test.py now completes and writes im_syn.mat
with shape (256, 152, N).
train_syndiff() set args.num_channels = 1 after both NCSNpp generators were already constructed, and define_G() takes its input_nc from its own default rather than from args -- so the assignment changed no network. What it did change is the Namespace that gets serialised into content.pth, which then recorded num_channels = 1 for a run whose diffusive generators were built with 2 channels. Rebuilding a model from those saved args produces a network whose weights will not load. Remove the assignment; content.pth now records the value the run actually used.
LoadDataSet() computed a single symmetric pad as int((256 - size) / 2) and
applied it to both sides. For an odd difference the truncation loses a
pixel, so the function silently returns something that is not 256 wide:
>>> LoadDataSet(<151-wide volume>).shape
(2, 1, 256, 255)
Nothing checks this, so the mismatch only surfaces much later as a shape
error deep inside the network. Split the padding across both sides and give
the leftover pixel to the far side, so the result is exactly 256.
The same function also failed unhelpfully on ordinary mistakes: a volume
wider than 256 produced a negative pad ("index can't contain negative
values"), a wrong variable name surfaced as a raw h5py KeyError, and a
missing file as an h5py open failure that does not mention the expected
data_<phase>_<contrast>.mat naming. Each of these now raises a message that
names the file and says what was expected.
Also close the HDF5 handle (it was never closed), reject inputs that are
neither 3D nor 4D instead of transposing them blindly, and check that the
two contrasts hold the same number of slices before they are zipped into a
TensorDataset.
Both discriminator phases ran the generators with autograd enabled and fed
the results straight into the discriminators. Nothing detached them, so
errD_fake.backward() and errD_cycle_fake.backward() propagated all the way
back through gen_diffusive_1/2 and gen_non_diffusive_1to2/2to1, filling
their .grad buffers -- which the following gen_*.zero_grad() then threw
away. Every iteration paid for a full backward pass through four generators
whose gradients were discarded by construction.
Wrap those forward passes in torch.no_grad(). The discriminator update is
unchanged mathematically: it never wanted generator gradients.
Measured on one discriminator branch (256x256, batch 1, GPU), averaged over
10 iterations after warm-up:
no_grad=True 67.3 ms/iter 0.14 GB transient
no_grad=False 156.4 ms/iter 0.56 GB transient
Peak memory for the whole training step is unchanged -- that peak is set by
the generator update, which legitimately keeps its graph. The win here is
wasted compute in the discriminator phases, not headroom.
init_net() wrapped every network it built in torch.nn.DataParallel, and train.py then wrapped the result in DistributedDataParallel. The two do not compose: DataParallel re-scatters each batch across devices inside a module that DDP already owns. train.py only ever passes a single device, so in practice the inner wrapper did no parallel work at all -- it just added a per-call replication step and a second 'module.' level to every key of the saved state_dict. Move the network to its device and leave the parallelism to DDP. Checkpoints written before this change carry 'module.module.' on the translation networks, so both loaders now strip the prefix repeatedly rather than once, and load checkpoints from any of the three layouts. Regression-checked on CPU with the sample data: training, --resume off the resulting content.pth, and test.py inference all complete.
Both discriminators reshape the last feature map into
view(group, -1, stddev_feat, channel // stddev_feat, height, width) with
group = min(batch, 4). That view is only valid when group divides the batch,
so any batch size that is neither <= 4 nor a multiple of 4 aborts:
batch=5 RuntimeError: shape '[4, -1, 1, 64, 2, 2]' is invalid for input of size 1280
batch=6 RuntimeError: shape '[4, -1, 1, 64, 2, 2]' is invalid for input of size 1536
batch=7 RuntimeError: shape '[4, -1, 1, 64, 2, 2]' is invalid for input of size 1792
The published command uses --batch_size 1 so this never surfaced there, but
it makes several perfectly ordinary batch sizes unusable.
Step the group size down to the largest value that divides the batch. Every
batch size that worked before keeps its previous grouping (1, 2, 3, 4 and
multiples of 4 are unchanged); the ones that crashed now run.
The repository had no tests, which is why several of the defects fixed in
this branch could sit in released code: nothing ever checked the values they
produced. This adds a suite that runs on CPU in a few seconds.
tests/test_diffusion.py schedule and posterior coefficient properties --
the variance-preserving identity, monotone signal
decay, non-negative posterior variance, the
collapse to x_0 at t == 0, and that
sample_posterior is deterministic there and
stochastic elsewhere.
tests/test_models.py shapes and gradients for NCSNpp, both
discriminators and the translation networks;
discriminators are exercised across batch sizes
1..8; NCSNpp must reduce a reconstruction loss
over a handful of optimiser steps.
tests/test_dataset.py padding reaches exactly 256 for odd and even
widths, stays centred, normalises to [-1, 1], and
each rejected input raises a message that names
the problem.
tests/test_checkpoint.py state_dicts load through all three 'module.'
layouts.
tests/test_ops.py the CUDA kernels agree with their native
fallbacks, forward and backward; skipped
automatically where the extensions cannot build.
59 tests pass with the CUDA extensions built, 54 pass with 5 skipped on a
CPU-only install.
The repository listed its dependencies only as prose in the README and shipped no requirements file, so there was nothing to install from. Record the actual runtime set, and note why torch>=1.13 is the floor: train.py now passes weights_only to torch.load, which that release introduced. ninja is listed separately because it is only needed to build the fused CUDA kernels; without it SynDiff falls back to the native implementations. Also drop the committed .DS_Store and extend .gitignore to cover it along with local virtualenvs and pytest caches.
Several statements in the README did not match the code: - The dependency list omitted numpy, h5py and scikit-image, all of which are imported at module scope, and pinned torch>=1.7.1, which is below the floor the resume path now needs. ninja and the CUDA toolchain moved to an optional section, since the fused kernels fall back to native PyTorch. - The dataset section did not mention that each .mat file must hold a variable called data_fs, nor that volumes are padded to 256x256 and rescaled to [-1, 1] on load, so neither dimension may exceed 256. - The sample data was described as ready to use. The folder actually holds two raw 25-slice volumes named T1.mat/T2.mat, which do not match the data_<phase>_<contrast>.mat layout the loader expects; say so. - Nothing described what --num_process_per_node changes, where test.py writes its output, or that the crop applied before saving is configurable. Also document how to run the test suite.
backbones/im2im.py holds a second copy of define_G/init_net that nothing imports; the live copy is in backbones/generator_resnet.py. Keeping both means fixes have to be applied twice, and the one just made to init_net (dropping the DataParallel wrapper) would silently not apply here. utils/utils.py holds restore_checkpoint/save_checkpoint, which nothing calls, and imports tensorflow at module scope for a single tf.io.gfile existence check. That makes TensorFlow look like a dependency of a PyTorch project. Neither file is referenced from train.py, test.py, dataset.py or backbones.
train.py and test.py each carried their own copy of var_func_vp, var_func_geometric, extract, get_time_schedule, get_sigma_schedule, Posterior_Coefficients, sample_posterior and sample_from_model -- about 120 duplicated lines defining the sampler that both the training loop and inference depend on. The two copies are currently byte-identical, so any change to the diffusion process has to be made twice and silently produces a train/test mismatch if it is not. Move them into diffusion.py, together with Diffusion_Coefficients, q_sample and q_sample_pairs, which only train.py had. The definitions are moved verbatim; both entry points now import from the single copy. Behaviour is unchanged: the extracted sources were checked to be identical between the two files before merging.
Two command-line arguments only reached part of the code they name. --ngf was passed to the two diffusive discriminators, but the translation networks and the cycle discriminators were built through define_G/define_D without it, so they always used the functions' own default of 64. Anyone training with a different --ngf silently got a model whose halves disagreed, with no way to scale the translation path at all. --image_size was passed to NCSNpp, but CreateDatasetSynthesis padded every volume to a hard-coded 256x256. Requesting any other size gave a network configured for one resolution and data at another, and SynDiff could not be used on a 256-grid-incompatible dataset without editing dataset.py. Thread both through. The published commands pass --ngf 64 and --image_size 256, which are exactly the previous hard-coded values, so they produce the same networks and the same data as before.
Both arguments reach the data loader and the translation networks as of the previous commit; say so, and note that the documented values match the constants they replaced.
Author
|
claude fable detaylı analiz sonucu. commitleri ayrı ayrı oluşturmasını istedim. dilersenin kullanabilirsiniz. -ENG- |
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.
SynDiff does not currently run on a modern PyTorch install, and a few defects
in the validation, checkpoint and inference paths survive into released code
because nothing exercises them. This branch fixes those, without touching the
training objective or the diffusion process.
Everything here was reproduced and then re-verified by actually running
train.pyandtest.pyon the bundled sample data, on CPU and on a CUDA GPU(PyTorch 2.13, NumPy 2.5, Python 3.12).
Nothing changes for the published commands. Every default was chosen to
reproduce the previous hard-coded value, and the multi-process path
(
--num_process_per_node > 1) takes exactly the code path it took before.The repository does not import
utils/op/fused_act.pyandutils/op/upfirdn2d.pyJIT-compiled their CUDAextensions at import time.
backbonesimports these transitively, so on anymachine without ninja and a matching CUDA toolchain — including every CPU-only
install — nothing could be imported at all:
Both modules already contained pure-PyTorch fallbacks (
upfirdn2d_native, andthe CPU branch of
fused_leaky_relu); the unconditionalload()simply ranbefore they could ever be reached. They are now built inside a
try/exceptthat warns once and routes to the native path.
Where the kernels do build, behaviour is unchanged —
tests/test_ops.pyasserts the CUDA and native paths agree. On this machine the forward results
are bit-identical and gradients match to
1.2e-07.Correctness fixes
The 2→1 validation direction was scored with the wrong network.
train_syndiff()runs two validation loops. The second swaps the batch to(y_val, x_val)so the target becomes contrast 2 — but still sampled fromgen_diffusive_1, the network that produces contrast 1. Every value written toval_l1_loss[1]andval_psnr_values[1]was therefore meaningless, andgen_diffusive_2was never validated. This does not affect trained weights(validation is observational), but it does affect anything selected by reading
those numbers.
Training crashed at the very end of every run. The validation buffers were
allocated with
args.num_epochepoch slots while the loop runs overrange(init_epoch, args.num_epoch + 1):The final checkpoint is written before this point, so the loss is the last
epoch's metrics plus a traceback on every run.
test.pycould not write its output.syn_im1/syn_im2were preallocatedas
(256, 256, N)while every sample is passed throughCenterCrop((256, 152))two lines earlier. The crop size is fixed, so thisfired on the first test slice for every dataset:
Slices are now collected and stacked, so the stored volume matches the cropped
geometry, and the crop size is exposed as
--crop_h/--crop_w(defaulting tothe previous values).
--resumeis dead on PyTorch ≥ 2.6.content.pthstores the run'sargparse.Namespacenext to the tensors, which theweights_only=Truedefaultrejects:
Checkpoint keys were truncated blindly.
load_checkpoint()rewrote everykey as
key[7:]on the assumption that a DDP wrapper had addedmodule.. Fora checkpoint saved without one, that removes seven characters of a real
parameter name. Both loaders now strip the prefix only where present, and
repeatedly, so all three layouts load.
Several batch sizes crash the discriminators. The minibatch-stddev block
reshapes with
group = min(batch, 4), which is only a valid view whengroupdivides the batch:
The group size now steps down to the largest divisor of the batch. Sizes that
worked before keep their previous grouping.
Padding silently missed the target size.
LoadDataSetapplied a singlesymmetric
int((256 - size) / 2)to both sides, so an odd difference lost apixel:
Nothing checked the result, so this only surfaced much later as a shape error
inside the network. Oversized inputs, a wrong variable name, a missing file and
mismatched slice counts between the two contrasts now each raise a message that
names the problem.
args.num_channels = 1was assigned after bothNCSNppgenerators werebuilt and is not read by
define_G, so it changed no network — but it wasserialised into
content.pth, recordingnum_channels = 1for a run whosegenerators were built with 2.
Arguments that did not reach the code they name
--ngfwas applied to the diffusive discriminators only; the translationnetworks and cycle discriminators used
define_G/define_D's own default of64, so a run with a different
--ngfgot a model whose halves disagreed.--image_sizewas passed toNCSNppwhileCreateDatasetSynthesispadded toa hard-coded 256. Both are threaded through. The published commands pass
--ngf 64and--image_size 256, which are the previous constants.Single-process and CPU runs
init_processes()calledtorch.cuda.set_device()anddist.init_process_group(backend='nccl')unconditionally, so a single-GPU runstill had to stand up a NCCL group and a CPU-only run was impossible — which is
also why the training loop could not be tested without a GPU. A process group
is now created only when the run spans processes, and DDP wraps only when one
is live.
size > 1is untouched.Wasted work in the discriminator phases
Both discriminator updates ran the generators with autograd enabled and never
detached the results, so
errD_fake.backward()propagated through all fourgenerators and filled
.gradbuffers that the followingzero_grad()threwaway. Wrapping those forwards in
torch.no_grad()leaves the updatemathematically identical. Measured on one branch (256×256, batch 1, GPU, 10
iterations after warm-up):
no_gradPeak memory for the whole step is unchanged — that peak is set by the generator
update, which legitimately keeps its graph.
Tests
The repository had no tests, which is why several of the defects above could
sit in released code: nothing ever checked the values they produced. The suite
runs on CPU in about five seconds.
tests/test_diffusion.py— the variance-preserving identity, monotone signaldecay, non-negative posterior variance, collapse to
x_0att == 0, andthat
sample_posterioris deterministic there and stochastic elsewhere.tests/test_models.py— shapes and gradients for NCSN++, both discriminators(batch sizes 1–8) and the translation networks; NCSN++ must reduce a
reconstruction loss over a few optimiser steps.
tests/test_dataset.py— padding reaches the target size for odd and evenwidths, stays centred, normalises to
[-1, 1], and each rejected inputraises a message naming the problem.
tests/test_checkpoint.py—state_dicts load through all threemodule.layouts.
tests/test_ops.py— the CUDA kernels agree with their native fallbacks,forward and backward; skipped where the extensions cannot build.
68 pass with the extensions built; 63 pass and 5 skip on a CPU-only install.
Housekeeping
diffusion.pycollects the ~120 lines of sampler code thattrain.pyandtest.pyeach carried a copy of. The two copies were verified byte-identicalbefore merging, and the definitions were moved verbatim — a change to the
diffusion process previously had to be made twice, or the two entry points
would silently disagree.
requirements.txtrecords the runtime set, which the README only described inprose.
torch>=1.13is the floor because that release introducedweights_only.ninjais listed separately since the fused kernels now fallback without it.
The README's dependency list omitted
numpy,h5pyandscikit-image, allimported at module scope. It also did not mention that each
.matfile musthold a variable named
data_fs, or that the sample data underSynDiff_sample_data/is two raw 25-slice volumes (T1.mat,T2.mat) ratherthan the
data_<phase>_<contrast>.matlayout the loader expects — so it doesnot run as shipped.
backbones/im2im.pyandutils/utils.pyare removed. Neither is referencedanywhere.
im2im.pyholds a second copy ofdefine_G/init_net, so theDataParallelfix in this branch would not have applied to it;utils/utils.pyimports TensorFlow at module scope for two functions nothing calls. Happy to
drop this commit if you would rather keep them.
Not addressed
was not re-derived against the paper, and no model was trained to
convergence or compared against the released pretrained weights.
--num_epoch 0still raisesZeroDivisionErrorfromCosineAnnealingLR(T_max=0). Degenerate input, left alone.