Skip to content

Keep ETKDG retry batches full and bound retries per molecule - #353

Merged
scal444 merged 7 commits into
NVIDIA-BioNeMo:mainfrom
scal444:etkdg/tail-overdispatch
Oct 2, 2026
Merged

scal444 merged 7 commits into
NVIDIA-BioNeMo:mainfrom
scal444:etkdg/tail-overdispatch

Conversation

@scal444

@scal444 scal444 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

The ETKDG scheduler allowed at most one attempt in flight per conformer still needed. Once only a few molecules remain unfinished - typically ones that keep failing embedding - batches shrink to a handful of attempts, a single worker runs them while the others wait, and the remaining retry budget is spent serially on a mostly idle GPU.

The default retry limit was also 10x the atom count of the largest molecule in the input, applied to every molecule. A molecule's budget depended on what else was in the call, and small molecules that cannot be embedded got a budget sized for the largest one.

  • When the regular dispatch pass leaves a batch short, fill the remaining slots round-robin with extra attempts for unfinished molecules that still have budget. These attempts count against the per-molecule limit and get stable attempt IDs; waiting on in-flight results (Fix concurrent ETKDG retry dispatch #299) is unchanged. Surplus successes are discarded at writeback, and completed counts are clamped to the requested number of conformers.
  • Compute the default limit per molecule as 10x its own atom count, matching RDKit. An explicit maxIterations still applies to all molecules.

The ETKDG scheduler allowed at most one attempt in flight per conformer
still needed. Once only a few molecules remain unfinished - typically ones
that keep failing embedding - batches shrink to a handful of attempts, a
single worker runs them while the others wait, and the remaining retry
budget is spent serially on a mostly idle GPU.

The default retry limit was also 10x the atom count of the largest molecule
in the input, applied to every molecule. A molecule's budget depended on
what else was in the call, and small molecules that cannot be embedded got
a budget sized for the largest one.

- When the regular dispatch pass leaves a batch short, fill the remaining
  slots round-robin with extra attempts for unfinished molecules that still
  have budget. These attempts count against the per-molecule limit and get
  stable attempt IDs; waiting on in-flight results (NVIDIA-BioNeMo#299) is unchanged.
  Surplus successes are discarded at writeback, and completed counts are
  clamped to the requested number of conformers.
- Compute the default limit per molecule as 10x its own atom count,
  matching RDKit. An explicit maxIterations still applies to all molecules.
@scal444
scal444 force-pushed the etkdg/tail-overdispatch branch from 5b3274a to 5d80c26 Compare October 1, 2026 20:21
@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Adjusts conformer generation scheduling and filtering logic.

The PR is not ready to merge because device-output finalization can run out of target-GPU memory on large multi-GPU runs.

Findings

  1. P1 Full staging can exhaust GPU memory ▶
Summary

The PR fills spare ETKDG batch slots with bounded per-molecule retry attempts and retains successful conformers in attempt-ID order.

  • It also adds device-coordinate gathering and ordering before pruning.
  • Full-size remote staging can exhaust target-GPU memory when surplus conformers are dropped.

Reviews (5) · Last reviewed commit: "Stage peer-GPU collector buffers before ..."

Comment thread src/etkdg_impl.cpp
@scal444
scal444 requested a review from evasnow1992 October 1, 2026 20:36
@scal444

scal444 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

For problematic datasets - e.g. platinum, which has 2 mols that RDKIt and nvmolkit fail on 100% of the time, with default max iterations, this cuts down on runtime by 50%

Comment thread src/etkdg_impl.cpp
Speculative top-up attempts can yield more successes than a molecule needs,
and writeback kept whichever arrived first, making retained conformers
timing-dependent across workers. Tag conformers with their attempt ID and
keep the N lowest per molecule on both the host and device paths, matching
the set the in-flight-capped scheduler produced before.
Comment thread src/conformer/device_coord_collector.cpp

@evasnow1992 evasnow1992 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing my comment!

@scal444

scal444 commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Holding off until i can double-check the new greptile comment

pruneDeviceConformers now walks each molecule's conformers in conformer-index
order (attempt-ID rank for ETKDG output) and numbers survivors in that order,
so greedy RMS pruning no longer depends on which worker collected which
attempt. finalizeOnTarget ranks attempts with an O(n) per-molecule bucketing
instead of a global sort.
Dropping surplus conformers split the per-collector bulk copy into one copy
call per surviving run. When anything is dropped, gather positions with a
single kernel instead.
Comment thread src/conformer/device_coord_collector.cpp

@evasnow1992 evasnow1992 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated changes also look good to me

Stream-ordered allocations on another GPU are not covered by
cudaDeviceEnablePeerAccess, so the gather kernel must not read them directly.
Copy each such buffer to the target GPU with one peer copy and gather from
there.
Comment on lines +163 to 170
stagedPositions[c] = AsyncDeviceVector<double>(collector.positions.size(), targetStream.stream());
copyDeviceToDeviceAsync(stagedPositions[c].data(),
collector.positions.data(),
collector.positions.size() * sizeof(double),
collector.gpuId,
collector.stream,
targetGpu,
targetStream.stream());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Full staging can exhaust GPU memory

When multi-GPU device output drops surplus successful attempts, this copies every remote collector’s entire positions buffer onto the target GPU, including coordinates that will be discarded. Those copies coexist with the retained result. On a large run, finalization can run out of target-GPU memory even when the final result would fit.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really just making up stuff now

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concern is specifically about peak target-GPU memory, not the final result size. In the filtered path, result.positions is allocated first, then stagedPositions[c] allocates a full copy of every remote collector’s positions buffer on the target GPU (lines 167–177), even though the gather later keeps only a subset.

For example, if remote collectors hold 100 attempts and only 10 are retained, the target needs space for the retained result plus all 100 staged attempts. The retained result fitting does not imply those staging allocations fit. This is a potential allocation failure for retry-heavy, multi-GPU inputs—not a claim that every run will fail.

A bounded approach would stage only the source spans needed by kept entries, or gather in chunks.

@scal444
scal444 merged commit f8216a3 into NVIDIA-BioNeMo:main Oct 2, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants