Skip to content

Improve Vxc Screening - #100

Closed
Jens (JensWehner) wants to merge 39 commits into
mainfrom
screening
Closed

Jens (JensWehner) wants to merge 39 commits into
mainfrom
screening

Conversation

@JensWehner

@JensWehner Jens (JensWehner) commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Branch changes compared with main

This summary compares the current screening branch with origin/main. The local
main branch is one commit behind origin/main; using it literally would also include
an unrelated upstream allocator commit. Uncommitted files are excluded.

  1. Automatic AO screening for large systems

    XC evaluation now switches from dense AO contractions to screened contractions when
    the functional supports atom-aligned grids and mol.nao_nr() exceeds PySCF's
    SWITCH_SIZE. Smaller systems and unsupported functionals retain the dense path.

  2. Spatially localized integration grids

    Grid points are recursively grouped into spatial blocks so each block touches fewer
    basis functions. Forward and inverse permutations restore the atom-major ordering
    expected by Skala.

  3. Reusable, backend-aware screening metadata

    A SpatialGridLayout is cached on the original grid and shared across SkalaNumInt
    instances. CPU evaluation rebuilds PySCF shell masks, while GPU evaluation invalidates
    GPU4PySCF's AO-index cache for the reordered grid.

  4. Sparse blockwise AO autograd

    New custom PyTorch autograd functions evaluate only active AO submatrices and scatter
    derivatives back into full density-matrix shapes. They handle CPU shell masks, GPU AO
    sorting, empty blocks, VJPs, JVPs, and higher-order differentiation.

  5. Screened energy and potential integration

    The screened first-order path evaluates raw AO features globally once, processes the
    neural functional in memory-sized atom chunks, collects feature cotangents, and
    performs one final sparse VJP to obtain the XC potential.

  6. Screened response and Hessian-vector products

    Response evaluation computes a screened feature JVP, chunk-local model Hessian
    actions, and a final sparse AO VJP. XCIntegrator returns the XC-only response, while
    SkalaNumInt adds the Coulomb response required by PySCF.

  7. AO traversal and model chunking are decoupled

    AO screening traverses the spatial grid independently of neural-model batching. Model
    chunks preserve complete atomic grids and are sized using available memory after
    reserving space for global feature and cotangent buffers.

  8. PySCF integration is split into focused modules

    The former large features.py module is now a thin public feature generator. Feature
    policy, feature mathematics, AO evaluation, screening, model chunking, and XC
    integration have separate owners; SkalaNumInt primarily adapts NumPy and CuPy data
    to Torch.

  9. Feature support is deliberately narrowed

    The unsupported nonlinear kinetic-tensor features ked_var and ked_det were
    removed. Screened raw features are restricted to density-matrix-linear meta-GGA
    quantities: density, grad, kin, and lapl. Direct second_order=True calls now
    raise an error in favor of gen_response(). The vjp for the pullback from model outputs to echange correlation potential now uses an analytical form instead of pytorch autograd.

  10. Correctness and performance coverage is substantially expanded

    New tests compare dense and screened CPU/GPU results for RKS, UKS, the XC potential,
    response/HVP, sparse and empty AO blocks, and nuclear gradients. Benchmark
    infrastructure measures runtime and peak memory, with pytest-benchmark,
    pytest-timeout, Memray support, and dedicated benchmark, GPU, and profiling markers.

Comparison of main branch and this branch vs fully dense evaluation
runtime:
image
max memory:
image

image image

Copilot AI left a comment

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.

Pull request overview

This PR introduces global spatial AO screening for Skala’s PySCF and GPU4PySCF integrations, evaluating AO-derived features in spatially grouped blocks (with atom-major restoration) and using chunk-local autograd plus blockwise VJP/JVP to bound memory while preserving a dense fallback.

Changes:

  • Add a global screened feature-evaluation path (spatial grid sorting + blockwise AO screening) for first- and second-order XC / response computations.
  • Update memory estimation to account for full-grid raw-feature/cotangent buffers and add targeted unit tests for the estimators.
  • Expand CPU/GPU test coverage for screening decisions, ordering/permutations, dense-vs-screened equivalence, and profiling/benchmark harnesses (with new pytest markers/deps).

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/test_memory_estimators.py Adds unit tests for the new global raw-feature buffer memory estimator and reserved-memory chunk sizing.
tests/test_gpu4pyscf_gradients.py Extends GPU gradient tests to compare CPU/GPU and dense/screened routes against stored references.
tests/test_gpu4pyscf_classes.py Marks GPU4PySCF class tests with a GPU marker and adjusts import ordering for CUDA gating.
tests/test_gpu4pyscf_ao_screening.py Adds GPU-focused AO screening tests covering sorting, response equivalence, masking/scatter, and CPU-vs-GPU validation.
tests/test_ao_screening.py Adds CPU AO screening tests for screening decisions, permutations, masking behavior, and screened response correctness.
tests/test_ao_screening_benchmark.py Adds benchmark/profiling tests to compare screened vs dense runtime and peak memory (CPU + CUDA).
src/skala/pyscf/numint.py Switches atom-chunked path to global screened evaluation, adds _should_screen_aos, and updates response/HVP logic accordingly.
src/skala/pyscf/memory_estimators.py Adds reserved-memory support and a new estimator for global raw-feature buffer storage.
src/skala/pyscf/gradients.py Minor dict-comprehension cleanup while building gradient feature dictionaries.
src/skala/pyscf/features.py Adds spatial grid ordering, caching, AO screening block loop helpers, and global screened feature evaluation machinery.
src/skala/pyscf/backend.py Reorders/cleans __all__ exports.
src/skala/gpu4pyscf/gradients.py Minor dict-comprehension cleanup mirroring the CPU gradients module.
src/skala/functional/utils/irreps.py Reorders __slots__ entries.
src/skala/functional/load.py Simplifies all(...) checks (removes unnecessary list creation).
src/skala/functional/init.py Reorders __all__ exports.
src/skala/ase/init.py Removes a now-redundant # noqa: F401 from the Skala export.
pyproject.toml Adds pytest timeout/addopts/markers and new dev dependencies (benchmark/timeout/memray).
environment-gpu.yml Adds pytest-benchmark and pytest-timeout to the GPU development/test environment.
environment-cpu.yml Adds pytest-benchmark and pytest-timeout to the CPU development/test environment.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_ao_screening.py Outdated
Comment thread tests/test_gpu4pyscf_ao_screening.py Outdated
Comment thread src/skala/pyscf/numint.py Outdated
@JensWehner Jens (JensWehner) changed the title Imropve Vxc Screening Improve Vxc Screening Aug 4, 2026

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/skala/pyscf/xc_integrator.py:292

  • sorted_raw_features (and the model-chunk raw features derived from it) are computed from dm0.double(), but screened_feature_jvp() is called with dm0/dm1 directly. If dm0/dm1 are not float64 this will produce a tangent with a different dtype than local_raw_features, and the subsequent torch.autograd.grad(..., grad_outputs=atom_major_tangent[...]) will error due to dtype mismatch.
            atom_major_tangent = screened_feature_jvp(
                dm0,
                dm1,
                mol,
                spatial_grid_layout,

Comment thread src/skala/pyscf/xc_integrator.py
Jens (JensWehner) and others added 2 commits August 6, 2026 17:16
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.

Suppressed comments (7)

tests/test_xc_integrator.py:4

  • tests/utils.py is not importable as a top-level utils module (there is no utils.py at the repository root), so this import will fail under pytest. Import the helper from the tests namespace instead.
    tests/test_ao_screening.py:7
  • tests/utils.py is not importable as a top-level utils module (there is no utils.py at the repository root), so this import will fail under pytest. Import the helper from the tests namespace instead.
from utils import QuadraticFunctional, patch_ao_screening

tests/test_gpu4pyscf_ao_screening.py:26

  • tests/utils.py is not importable as a top-level utils module (there is no utils.py at the repository root), so this import will fail under pytest. Import the helper from the tests namespace instead.
from utils import QuadraticFunctional, patch_ao_screening  # noqa: E402

tests/test_ao_screening_benchmark.py:17

  • tests/utils.py is not importable as a top-level utils module (there is no utils.py at the repository root), so this import will fail under pytest. Import the helper from the tests namespace instead.
from utils import patch_ao_screening

tests/test_gpu4pyscf_gradients.py:26

  • tests/utils.py is not importable as a top-level utils module (there is no utils.py at the repository root), so this import will fail under pytest. Import the helper from the tests namespace instead.
    src/skala/functional/density.py:20
  • IMMUTABLES is typed as frozenset[str] but it contains Feature enum values. This will confuse type-checking (and IDEs) and can hide real key-type mistakes when working with FeatureMap.
IMMUTABLES: frozenset[str] = frozenset([Feature.GRID_COORDS, Feature.GRID_WEIGHTS])

src/skala/ase/init.py:11

  • This import is only used to re-export Skala via __all__, so Ruff/Pyflakes will likely flag it as F401 (imported but unused) without an explicit ignore (consistent with src/skala/foundry/__init__.py).
from skala.ase.calculator import Skala

Comment thread benchmarks/.gitignore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I would ignore these files, in this folder, I am not sure if they should be committed.

Comment on lines -37 to -39
- name: Run mypy
run: mypy .
shell: micromamba-shell {0}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we keep mypy here to run it in our production environment rather than the incomplete pre-commit env?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, the precommit mypy at the moment uses the local mypy version so it should use the full environment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we want to keep this large notebooks as part of the repo?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

probably not, I am not sure how much of the benchmarking code we should keep.

Comment thread src/skala/pyscf/xc_integrator.py Outdated
Comment on lines +127 to +132
spatial_grid_layout = cast(
SpatialGridLayout | None,
grid_state.get("_skala_spatial_grid_layout"),
)
if spatial_grid_layout is not None:
return spatial_grid_layout

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this caching save in case we use a GradientScanner as in the ASE calculator, where we reuse the Kohn-Sham object and the Grids object but just reset them with new molecule geometries?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, I did not think of grid object reuse, so I would have to hook that into the grid_object reset. That is annoying. Why would you ever reset a grid object, sigh.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

then I guess I have to create two classes as well which inherit from the respective Gridobjects.

dm_eval = dm.double()
electron_count = torch.zeros(2, device=self.device, dtype=dm_eval.dtype)
energy = torch.tensor(0.0, device=self.device, dtype=dm_eval.dtype)
feature_function = feature_math.MGGAFeatureFunction(self.feature_spec)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should we add update the feature spec here as in _integrate_dense with set(self.feature_spec.names) | {Feature.DENSITY, Feature.GRID_WEIGHTS}?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, good point.

blksize = _resolve_ao_block_size(mol, feature_function, block_size, max_memory, gpu)

if blksize is not None and blksize >= grids.weights.shape[0]:
features = evaluate_full_grid(dm.double(), mol, grids.coords, feature_function)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should this pass through the gpu option?

Suggested change
features = evaluate_full_grid(dm.double(), mol, grids.coords, feature_function)
features = evaluate_full_grid(dm.double(), mol, grids.coords, feature_function, gpu)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, ohh yes.

@awvwgk Sebastian Ehlert (awvwgk) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Left some comments, overall looks good to me.

One note, the gradient calculation is currently not affected by this update is it? We would still use the dense path there, so the ASE calculator which goes via the GradientScanner would see the same peek memory / compute usage as before?

@DerkKooi Derk Kooi (DerkKooi) left a comment

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.

Further simplifications are possible on the custom autodiff side because of the assumption of features linear in the density matrix. I have described the modifications in some detail in this review.

return loop_type(dm, mol, grids, feature_function, blksize)


class ChunkEvalForward(Function):

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.

If we are assuming that our features are only linear in the DM then we can (and I think should) go one step further in the simplification of the two custom autograd functions (both ChunkEvalForward and ChunkEvalBackward). I've outlined the simplifications in separate comments.

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
context.blksize,
context.compile_feature_function,
context.gpu,
*vectors_jvp,

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.

vectors_jvp can be done away with everywhere, since any jvp of a linear function of dm (func(dm)) is just that same linear function evaluated on the v of the jvp (func(v)).

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
device=ctx.dm.device,
dtype=ctx.dm.dtype,
)
vector_tangent = grad_inputs[7] if ctx.vectors_jvp else grad_inputs[0]

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.

this just becomes vector_tangent=grad_inputs[0]

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
dtype=ctx.dm.dtype,
)
return ChunkEvalForward.apply(
ctx.dm,

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.

and here replace ctx.dm by vector_tangent.

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
if ctx.vectors_jvp:
dm_grad = ctx.dm * 0
else:
dm_grad = ChunkEvalBackward.apply(

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.

ChunkEvalBackward doesn't need dm, so feature_cotangent can be added as the first argument instead.

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
*vectors_jvp,
) = inputs
context.vectors_jvp = tuple(vectors_jvp)
ctx.save_for_backward(context.dm)

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.

Linear function means we don't have to save the dm for the backward.

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
context.gpu,
_feature_cotangent,
) = inputs
ctx.save_for_backward(context.dm)

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.

Don't need to save the dm or the _feature_cotangent for the backward.

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
def jvp(
ctx: _ChunkEvalBackwardContext, *grad_inputs: torch.Tensor | None
) -> torch.Tensor:
feature_cotangent_tangent = grad_inputs[7]

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.

feature_cotangent_tangent = grad_inputs[0]

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
if feature_cotangent_tangent is None:
return torch.zeros_like(ctx.dm)
return ChunkEvalBackward.apply(
ctx.dm,

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.

feature_cotangent_tangent as first argument.

Comment thread src/skala/pyscf/ao_evaluation.py Outdated
grads: list[Tensor | None] = [ctx.dm * 0]
grads += [None] * 6
grads.append(
ChunkEvalForward.apply(

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.

First argument is grad_outputs[0], check again ctx.requires_grad[0].

@JensWehner

Copy link
Copy Markdown
Contributor Author

Left some comments, overall looks good to me.

One note, the gradient calculation is currently not affected by this update is it? We would still use the dense path there, so the ASE calculator which goes via the GradientScanner would see the same peek memory / compute usage as before?

Yes, correct. I did not look at gradients at all so far.

@JensWehner

Jens (JensWehner) commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor Author

So talking to Thijs Vogels (@tvogels) to make the review much easier I split this up into multiple PRs.

#102 #103 #105 #106

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.

4 participants