fix(predict+pretrain): rdkit2D predict arg, issue #22, and atom/bond-mode DDP + float-loss fixes - #24
Conversation
The predict command can build rdkit_2d_normalized features (rdkit_2d_normalized_cuik_molmaker), whose featurization reads args.rdkit2D_normalization_type (kermt/data/molgraph.py) and must match the value baked into the checkpoint. The flag was defined only on the finetune parser, so `main.py predict --rdkit2D_normalization_type ...` failed with "unrecognized arguments". Add it to the predict parser, mirroring the finetune definition (choices fast/best/descriptastorus, default fast). Signed-off-by: Eva Xue <evax@nvidia.com>
…VIDIA-BioNeMo#22) predict() set args.bond_drop_rate = 0 on the shared args instance. Training and evaluation reuse one args object and graphs are rebuilt every epoch (caching off by default), so after the first validation pass bond dropout stayed disabled for every subsequent training epoch. Under DDP only rank 0 evaluates, so rank 0 trained with bond_drop_rate=0 while other ranks kept the configured rate, desyncing augmentation across ranks. Shallow-copy args before overriding bond_drop_rate so the override is local to the evaluation call and never leaks back into training or other ranks. Apply the same fix to fingerprint.do_generate, which had the identical pattern. Fixes NVIDIA-BioNeMo#22 Signed-off-by: Eva Xue <evax@nvidia.com>
…modes
The three pretrain trainers (KERMTTrainer, KERMTCMIMTrainer,
KERMTHybridTrainer) wrapped the model in DDP without find_unused_parameters.
In embedding_output_type=atom (or bond) mode the encoder produces only one
aggregation level, so the opposite branch's FFNs and the bond/atom vocab + FG
heads receive no gradient; DDP then aborts on the second iteration ("Expected
to have finished reduction ... parameters that were not used in producing
loss").
Enable find_unused_parameters only when embedding_output_type != 'both' (both
exercises every branch, so keep it off to avoid the per-iteration
graph-traversal overhead; static_graph is not viable because dynamic-depth
sampling varies the graph across steps).
Verified locally: hybrid + atom + DDP (WORLD_SIZE=1) crashed on iteration 2
before the change and trains a full epoch after it.
Signed-off-by: Eva Xue <evax@nvidia.com>
…m/bond) In embedding_output_type=atom (or bond) mode the vocab loss returns a plain float 0.0 for the branch that has no embeddings (e.g. bond-vocab and bond dist loss in atom mode). KERMTTrainer.iter called .item() on the task and dist loss components unconditionally, raising "AttributeError: 'float' object has no attribute 'item'" once the affected branch was logged. Guard every loss-component .item() with `if not isinstance(x, float) else x`, matching the idiom KERMTHybridTrainer already uses (which is why hybrid mode was unaffected). Covers the train accumulation, eval branch, and the wandb logging dict. Verified: vocab + atom + DDP (WORLD_SIZE=1) trains a full epoch + validation. Signed-off-by: Eva Xue <evax@nvidia.com>
Additional fixes on this branch: atom/bond-mode pretrainingWhile validating the predict changes I found two pre-existing bugs that break multi-GPU pretraining with 1. DDP aborts: "parameters that were not used in producing loss" (commit 87bc5a5)Symptom. Pretraining under DDP with Cause. In atom/bond mode the encoder produces only one aggregation level, so the opposite branch's FFNs and the bond/atom vocab + FG heads receive no gradient -- but all three trainers wrapped the model as 2. AttributeError: 'float' object has no attribute 'item' (commit 0898359)Symptom. Vocab-mode pretraining with Cause. In atom/bond mode the vocab loss returns a plain float Verification (local, single-GPU DDP via
|
Greptile SummaryThis PR fixes two correctness bugs in the prediction/pretrain path: bond dropout being permanently disabled (and desynced across DDP ranks) after the first validation pass, and a
Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Train as Training Loop (rank 0..N)
participant Eval as evaluate() / predict()
participant DDP as DDP Model
participant Args as shared args
Note over Train,Args: Before fix: args.bond_drop_rate mutated globally
Train->>DDP: "forward pass (bond_drop_rate=configured)"
Train->>Eval: call evaluate()
Eval->>Args: "args.bond_drop_rate = 0 ❌ mutates shared object"
Eval->>DDP: "predict with bond_drop_rate=0"
Train->>DDP: "next epoch forward (bond_drop_rate=0 ❌ leaked)"
Note over Train,Args: After fix: args shallow-copied locally
Train->>DDP: "forward pass (bond_drop_rate=configured ✅)"
Train->>Eval: call evaluate()
Eval->>Eval: "args_copy = copy.copy(args)"
Eval->>Eval: "args_copy.bond_drop_rate = 0"
Eval->>DDP: "predict with args_copy (bond_drop_rate=0 ✅)"
Train->>DDP: next epoch forward (original bond_drop_rate preserved ✅)
Reviews (3): Last reviewed commit: "fix(fingerprint+eval): unpack the (model..." | Re-trigger Greptile |
sveccham
left a comment
There was a problem hiding this comment.
Thanks for the fixes and sorry for the delay.
| @@ -564,7 +570,13 @@ def __init__(self, | |||
| self.n_iter = 0 | |||
|
|
|||
| self.model.to(self.gpu_id) | |||
There was a problem hiding this comment.
This this line still needed for DDP finetuning?
There was a problem hiding this comment.
I think so. DDP itself doesn't move the model to the target device; it only assumes the model is already on the specified device_id and operates from there.
| parser.add_argument('--features_generator', type=str, nargs='*', | ||
| choices=get_available_features_generators(), | ||
| help='Method of generating additional features') | ||
| parser.add_argument('--rdkit2D_normalization_type', type=str, choices=("fast", "best", "descriptastorus"), default='fast', |
There was a problem hiding this comment.
I think rdkit2D_normalization_type in predict should be the same as the checkpoint. Doesn't this give the user an option to use the finetuned checkpoint incorrectly?
There was a problem hiding this comment.
I dug into this a bit further and realized this is actually a bigger issue.
A couple of months ago, you checked in a commit that addressed this by introducing a stricter model loading function for prediction, with consistency checks on both rdkit2D_normalization_type and feature_generator. That change also switched main.py in prediction mode to use the new model loader.
Later, when I checked in the larger batch of changes for Contrastive KERMT, I switched back to the default model loader because of compatibility concerns, which inadvertently removed these consistency checks.
To resolve the rdkit2D_normalization_type issue, I'll revert my change here and switch prediction back to using the stricter model loader.
While investigating this, I also found a related issue with feature_generator. It should remain consistent with the fine-tuned checkpoint, but it also shouldn't be inherited blindly, since doing so can cause the feature generation step to be skipped. I'll add another commit to address this properly as well.
…args Review feedback on PR NVIDIA-BioNeMo#24: `args = copy.copy(args)` rebinds the parameter, so a later reader cannot tell from any single line whether `args` is the caller's instance or the local override. Bind the copy to `args_copy` and read every subsequent field from it. The rename has to cover the whole function body, not just the two lines at the top: had only the assignment been renamed, `MolCollator` would have been handed the caller's un-overridden args and bond dropout would have stayed enabled during evaluation -- reintroducing issue NVIDIA-BioNeMo#22 in a quieter form. Updated uses: predict() num_tasks, MolCollator(args=...), fingerprint, dataset_type do_generate() MolCollator(args=...) No behaviour change. Signed-off-by: Eva Xue <evax@nvidia.com>
…loader 60a77fd added --rdkit2D_normalization_type to add_predict_args to fix "unrecognized arguments". That fixed the error but suppressed a safeguard: make_predictions() copies every checkpoint arg the predict parser does not already define (`if not hasattr(args, key)`), so before 60a77fd the value was inherited from the checkpoint automatically. Giving argparse a default made the attribute always present, so a model finetuned with normalization "best" was featurized with "fast" on a plain `main.py predict` -- silently, because nothing downstream compares them. The value is baked into the features the checkpoint was trained on, so the only correct value is the checkpoint's and there is nothing for a user to choose. Remove the argument and let inheritance supply it. Restoring the argument was only half of what was lost. 86d4f3a ("disallow inconsistent usage between finetune and predict") added the argument, added load_checkpoint_for_prediction() with a finetune/predict consistency check, and pointed predict at it. c299c38 ("Import grover_fork base for cMIM line") reverted all three by overwriting those files with the older grover_fork versions, leaving load_checkpoint_for_prediction() defined but called from nowhere. Point predict back at it so the consistency check runs again and finetuned weights load strictly instead of being skipped on a shape mismatch. Note the return type differs: load_checkpoint_for_prediction returns the model, load_checkpoint returns (model, state). The features_generator half of the consistency check does not yet account for --features_path; that is fixed in the next commit. Signed-off-by: Eva Xue <evax@nvidia.com>
…_path Restoring load_checkpoint_for_prediction re-enabled a consistency check that compares features_generator between finetune and predict. That check assumes features are always built on the fly, which the normal inference workflow does not do: agent/scripts/run_inference.py featurizes ahead of time and passes --features_path, leaving features_generator at its argparse default of None. The check would then reject every skill-driven prediction against a checkpoint finetuned with features. Skip the features_generator comparison when --features_path is supplied. The two are mutually exclusive by construction -- MoleculeDatapoint raises "Currently cannot provide both loaded features and a features generator" -- so a mismatch there is expected rather than a misuse. Also skip keys the checkpoint predates, and correct the error message, which had the finetune and predict values the wrong way round. Guard the feature width in make_predictions. features_size is not in get_model_args(), so it is recomputed from the prediction data instead of being inherited; if it disagrees with the finetuned width the FFN input layer is the wrong shape. Previously load_checkpoint skipped that layer on a shape mismatch (strict_shape_check defaults to False) and predicted from its random initialization, exiting 0 with a debug-level note. The strict loader now raises, but only with a bare tensor-shape message, so compare features_size explicitly and name the fix: which --features_path to pass, or which --features_generator to regenerate with. Verified against the four real invocation shapes: --features_path with no generator (the workflow that would have regressed), on-the-fly with a matching generator, on-the-fly with a mismatched generator, and a checkpoint predating the argument. Also verified the removed flag is inherited from the checkpoint, and that the features_size guard fires on forgotten and wrong-sized features while staying quiet when a no-features checkpoint is used without features. Signed-off-by: Eva Xue <evax@nvidia.com>
…kpoint load_checkpoint returns (model, state), but generate_fingerprints and run_evaluation bound the whole tuple to `model` and passed it on as if it were the module. `main.py fingerprint` therefore failed at do_generate()'s first statement with "AttributeError: 'tuple' object has no attribute 'eval'", and run_evaluation failed the same way inside predict(). Same drift as the predict loader fixed in the previous commit: 86d4f3a wrote these call sites against a load_checkpoint that returned the model alone, and c299c38 ("Import grover_fork base for cMIM line") replaced utils.py with the grover_fork version returning a tuple without updating the callers. train.py was updated, these two were missed. Unpack the tuple and discard the state, matching train.py:533 and :558. Swept every load_checkpoint call site outside kermttrainer.py (which has its own unrelated 4-tuple loader); these two were the only mismatches. Signed-off-by: Eva Xue <evax@nvidia.com>
Summary
Two related correctness fixes in the prediction / evaluation path:
main.py predictrejects--rdkit2D_normalization_type— the predict command couldn't use rdkit-2D-normalized features.bond_drop_rateis silently disabled after the first validation (and diverges across ranks under DDP) #22.Both are small, self-contained changes with no behavior change for existing correct runs.
Fix 1 — accept
--rdkit2D_normalization_typeon the predict parserpredictcan build rdkit-2D-normalized features (rdkit_2d_normalized_cuik_molmaker), whose featurization readsargs.rdkit2D_normalization_type(kermt/data/molgraph.py) and must match the normalization baked into the checkpoint. The flag was defined only on the finetune parser, so any predict run passing it failed with:Added the argument to
add_predict_args, mirroring the finetune definition (choicesfast/best/descriptastorus, defaultfast).Fix 2 — don't disable bond dropout on the shared
args(Fixes #22)predict()setargs.bond_drop_rate = 0on the sharedargsinstance. Training and evaluation reuse oneargsobject, and graphs are rebuilt every epoch (caching off by default), so after the first validation pass bond dropout stayed disabled for every subsequent training epoch. Under DDP, only rank 0 evaluates, so rank 0 trained withbond_drop_rate=0while the other ranks kept the configured rate, desyncing augmentation across ranks.Fix: shallow-copy
argsbefore overridingbond_drop_rate, so the override is local to the evaluation call and never leaks back into training or other ranks. Applied the same fix tofingerprint.do_generate, which had the identical pattern (impact there is benign since it's a standalone command, fixed for consistency).Changes
kermt/util/parsing.py--rdkit2D_normalization_typeto the predict parsertask/predict.pyargsbefore disabling bond dropout (+import copy)task/fingerprint.pydo_generate(+import copy)Testing
tests/integration/test_pretrain_finetune.py::test_pretrain_ddp_finetune— now PASSES end-to-end (pretrain → finetune → predict). This test previously failed at the predict step on the unrecognized--rdkit2D_normalization_typeargument.--rdkit2D_normalization_type descriptastorus, and that bothpredict()andfingerprint.do_generate()no longer mutate the caller'sargs.Fixes #22