DFS Substruct backend algorithm - #244
Open
scal444 wants to merge 18 commits into
Open
Conversation
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.
…te rationale comments
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.
Contributor
|
| 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
evasnow1992
reviewed
Jul 31, 2026
| case nvMolKit::SubstructAlgorithm::DFS: | ||
| return "dfs"; | ||
| case nvMolKit::SubstructAlgorithm::VF2: | ||
| return "vf2"; |
Collaborator
There was a problem hiding this comment.
Just to clarify, we don’t intend to expose VF2 through the Python frontend, right?
evasnow1992
approved these changes
Jul 31, 2026
evasnow1992
left a comment
Collaborator
There was a problem hiding this comment.
LGTM. Only one clarification question
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.
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.