Skip to content

DFS Substruct backend algorithm - #244

Open
scal444 wants to merge 18 commits into
NVIDIA-BioNeMo:mainfrom
scal444:substruct_DFS
Open

DFS Substruct backend algorithm#244
scal444 wants to merge 18 commits into
NVIDIA-BioNeMo:mainfrom
scal444:substruct_DFS

Conversation

@scal444

@scal444 scal444 commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Up to 5x faster in kernel with up to 2x faster e2e substruct runtime. This will be the backend for MCS fallback match as well.

scal444 added 17 commits July 29, 2026 16:27
The batchSize search range was (128, 1024, step=128), and tuning runs were
pinning to 1024 -- the ceiling of the range, not an interior optimum. Raise
the ceiling to 8192, matching what benchmarks/substruct_config.csv already
uses, and switch to log spacing since the useful range spans two orders of
magnitude. The step=128 bought nothing: the runtime derives
targetsPerBatch = batchSize // numQueries, and that truncation destroys any
128 alignment before it reaches a kernel.

Default calibration_max_size to None (uncapped) for tune_substructure. The
2000-target cap left too few mini-batches to fill every worker's executor
ring, so pipeline fill and drain dominated the measurement and large
batchSize values were systematically under-rewarded. auto_subsample and
normalize_calibration_set now accept max_size=None; other tuners keep the
2000 default.

Also default substruct_bench.py to --sanitize. It was the only benchmark
defaulting to sanitize=False, while etkdg_bench and ff_optimize_bench both
default to True. Move the flag pair into a shared bench_utils.cli helper so
the mutually-exclusive group and the safe default stay consistent.
preprocessMiniBatch re-staged and re-uploaded the recursive SMARTS pattern
entries on every pattern sub-batch, of every mini-batch, on every worker.
The data is identical every time: MiniBatchPlanner::prepareRecursiveMiniBatch
points plan.patternsAtDepth at leafSubpatterns_.allQueriesPatternsAtDepth, a
table built once by buildAllPatterns and shared across all mini-batches.

On a 200k-target x 158-query hasSubstructMatch run that was 133,333 uploads
costing 4,979 ms of aggregate worker time -- 44% of all time the six worker
threads spent launching work, and the single largest item in the profile.
Most of it was not the 428-byte copy itself but the handshake around it: the
pinned staging area is only double-buffered, so with four sub-batches per
mini-batch acquireBufferIndex kept returning a buffer whose copy was still
queued behind a full depth level of paint kernels, and waitForBuffer blocked
the worker on cudaEventSynchronize. Those accounted for roughly 135k of the
168k blocking event syncs on worker threads; the actual result drain was only
221 ms.

Upload the table once in LeafSubpatterns::syncToDevice, which already runs on
the setup stream that callers synchronize before starting any worker. Each
sub-batch is a contiguous slice of its depth's entries, so the kernels now
take a pointer into the device table rather than a per-sub-batch staging
buffer, and the whole acquire/wait/fill/copy/record sequence is gone.

The non-pipeline preprocessRecursiveSmarts path builds its own local pattern
vectors and is left unchanged.
Template TargetMask on atom capacity instead of 64-bit word count, adding a
uint32_t form for the 32-atom shapes. The adjacency tables become a union of
mask and packed row so the General case can still cache 64-bit rows behind a
narrower mask type, leaving their shared footprint unchanged.

Eliminates all spill traffic in the 32-atom Store kernels (76/60 -> 0/0 bytes),
roughly halves their stack frames, and drops shared memory ~10%. Register count
is unchanged at 40, being launch-bounds capped. 64- and 128-atom shapes compile
byte-identical. No measurable end-to-end change on a 200k x 158 count benchmark.
Split substruct_dfs.cuh so the search engine itself is a standalone,
frontend-agnostic component under src/subgraph/, in preparation for the
MCS seed matcher adopting it as its exact-match backend:

- warp_dfs.cuh: dfsFromRoots, the lane-local backtracking search over
  injective query->target atom maps, templated on a candidates oracle,
  a terminal handler (count/store/paint/stop via DfsTerminalVerdict),
  and an abort hook for warp-wide first-match early exit.
- target_mask.cuh: the TargetMask<32/64/128> register bitsets.
- warp_reduce.cuh: all-lane warp reductions.
- occupancy.cuh: minBlocksPerSM generalized to (shared bytes, block
  size, target residency) instead of the substruct WarpSharedState.

substruct_dfs.cuh keeps everything substructure-specific -- the label
matrix transpose, bond-mask classification (Uniform/Dual/General), the
adjacency tables, and the output modes, now expressed as the terminal
handler passed to the shared core. Kernel-facing signatures are
unchanged and behavior is identical.

test_warp_dfs.cu covers the core in isolation against a host
brute-force enumerator: full counts, ring-closure backtracking, label
restriction, first-match stop, per-root existence, and the abort hook.
Core unit tests now exercise all three TargetMask widths (including the
128-atom form's lo/hi word boundary via a ring straddling atoms 58..69),
multi-root lanes (rootDone advancing to a lane's next root), a 40-deep
stack with the terminal at exactly MaxDepth-1, and the found-flag
existence race the MCS frontend will use (abort hook + atomicCAS winner).

Search-level: SizeBracketsRingWithLargeHydrocarbon pins one target in
each target bracket (28/50/100 atoms: cyclohexane + alkane tail) against
one query per query bracket (6/20/40 atoms), validated against RDKit on
all algorithms.
DFS is ~2x faster than GSI on the Enamine/PAINS benchmark (100k targets
x 480 patterns: 2692 ms vs 5281 ms), so it becomes the default for
SubstructSearchConfig.

The algorithm parameter itself is no longer part of the documented public
surface: it is dropped from the class Args and its property is marked
internal, since it exists to benchmark the two backends against each other
and goes away when GSI is retired. It remains settable, so existing
callers and the benchmark's sweep are unaffected.
@scal444
scal444 requested a review from evasnow1992 July 31, 2026 17:17
@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an adjacency-anchored DFS backend for GPU substructure matching.

  • Exposes backend selection through the Python configuration, autotuner, and benchmark.
  • Adds DFS kernels, supporting graph utilities, result handling, and validation coverage.
  • Preserves the algorithm stored in an autotuned benchmark configuration unless explicitly overridden.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported loaded-backend overwrite is fixed by retaining the serialized algorithm when no explicit CLI override is supplied.

Important Files Changed

Filename Overview
benchmarks/substruct_bench.py Adds backend selection and reporting while correctly preserving the backend loaded from an autotuned configuration.
nvmolkit/substructure.py Exposes and serializes the substructure algorithm configuration.
nvmolkit/autotune/tune_substructure.py Propagates the selected matching backend through warm-up, trials, and the resulting tuned configuration.
src/substruct/substruct_dfs.cuh Implements the new DFS substructure-matching backend.
src/substruct/substruct_kernels.cu Integrates DFS execution into the GPU substructure kernels.
tests/test_substruct_search.cu Extends substructure search coverage for the new backend.

Reviews (2): Last reviewed commit: "Fix config reloading" | Re-trigger Greptile

Comment thread benchmarks/substruct_bench.py Outdated
Comment thread nvmolkit/substructure.cpp
case nvMolKit::SubstructAlgorithm::DFS:
return "dfs";
case nvMolKit::SubstructAlgorithm::VF2:
return "vf2";

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.

Just to clarify, we don’t intend to expose VF2 through the Python frontend, right?

@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.

LGTM. Only one clarification question

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