Skip to content

Reduce inference peak memory with chunking and fine-grained lifecycle control - #321

Open
heathcliff233 wants to merge 7 commits into
aqlaboratory:mainfrom
heathcliff233:lhong/2026-07/perf/inference-memory-lifecycle
Open

Reduce inference peak memory with chunking and fine-grained lifecycle control#321
heathcliff233 wants to merge 7 commits into
aqlaboratory:mainfrom
heathcliff233:lhong/2026-07/perf/inference-memory-lifecycle

Conversation

@heathcliff233

Copy link
Copy Markdown

Summary

Second PR in the #318 breakdown, stacked on #320. This one cuts inference peak memory with opt-in chunk caps and shorter N² lifetimes — no fused pair kernels. Offload stays off in the numbers below.

Until #320 merges, the GitHub diff against main also includes that PR. The three commits unique to this branch are:

  • perf: release trunk pair after confidence clone
  • perf: add opt-in triangle and transition chunk caps
  • perf: chunk diffusion conditioning and stream templates

Why N² features and intermediates dominate

Define 1U = N² × c_z × 4 — the storage of one fp32 trunk pair tensor. At long targets, almost every large allocation is pair-shaped: the working z, transition / triangle scratch, template distogram and unit-vector features, and diffusion relpos concatenations. Singles, atom tensors, and MSA rows are small next to that (fractions of a U). So peak memory is mostly how many N² tensors coexist, and how large the pair-op scratch is while they do.

This PR attacks that two ways: bound the pair-op working sets with opt-in chunk caps, and shorten lifetimes so fewer full N² tensors sit resident at once. Fused kernels that avoid materializing those intermediates are left for later.

Primary metric: peak CUDA alloc above live model params+buffers, reported in U.

Measured peak (homo_1200, S=1, cuEq, offload off)

Configuration Peak above params Limiting stage
Default (no caps, no lifecycle) 17.40U CONFIDENCE
Same caps, default lifecycle ~10U DIFFUSION
This PR (caps + lifecycle) 6.63U input_embedder

Warm forward with caps+lifecycle is ~108s vs ~87s uncapped default; eager trimul chunking is the main wall cost.

Caps alone bound PairBlock / transition / OPM scratch. They do not touch diffusion's full [N,N,267] relpos+concat (~10U), the 4-wide template embed (~9.5U), or input-embedder relpos staging (~6.6U). The drop from ~10U to 6.63U is the lifetime work on top of those caps:

Change Effect
Release trunk pair after confidence clone −1.0U early; secondary once caps apply
Row-chunk diffusion conditioning (128) DIFFUSION 10.1U → 6.4U
Stream templates one-at-a-time on GPU template 9.5U → 5.9U

After that, the global peak is input_embedder at 6.63U. Template distogram (~1.2U) stays in batch for the whole forward — no CPU park in this PR.

What this PR changes

  • Opt-in env caps: OPENFOLD3_TRI_ATTN_CHUNK_CAP, OPENFOLD3_TRIMUL_CHUNK_CAP, OPENFOLD3_TRANSITION_CHUNK_CAP (unset = upstream default)
  • Under a trimul cap and inplace-safe inference, skip cuEq trimul and use eager _inference_forward with that chunk
  • Confidence: ownership handoff so the trunk pair can be freed after the confidence clone
  • Diffusion conditioning: row-chunked trunk-pair + relpos embed (row_slice on relpos_complex)
  • Templates: GPU streaming when inplace_safe and n_templ > 1
  • scripts/dev/profile_inference_stages.py for warm / overall / hooked stage peaks

Out of scope

Fused SwiGLU / trimul / tri-attn / input-relpos, template CPU-park or O(N) coordinate templates, and dual z_init+z residency. Those are what the original tip used to reach ~4.2U; this PR stops at ~6.6U on homo_1200.

Test plan

  • Focused pytest: test_heads, test_diffusion_conditioning, test_template_module, test_utils (cap envs)
  • Profile homo_1200 S=1 with --triangle-chunk-cap 128 --transition-chunk-cap 128; expect ~6.6U, input_embedder peak
  • Same profile with caps unset: lifecycle-only path still correct; peak higher
  • Ubiquitin / MCL1 smoke under the same protocol

Gather token features through precomputed atom-to-token indices in the
decoder, and use segmented reductions for packed inference aggregation
to avoid CUDA atomic scatter nondeterminism. Keep the scatter path under
autograd.

Tests: pytest openfold3/tests/test_atomize_utils.py -q
Tests: pytest openfold3/tests/test_sequence_local_atom_attention.py -q
Keep separate chunk-size entries keyed by argument structure and
max_chunk_size so alternating inference targets can reuse prior OOM
probes instead of re-tuning on every revisit.

Tests: pytest openfold3/tests/utils/test_utils.py -k chunk_size_tuner -q
Scope on-the-fly feature RNG (e.g. ligand conformers) to each sample's
seed with save/restore so featurization no longer depends on ambient
Python/NumPy/Torch RNG state.

Tests: pytest openfold3/tests/core/data/framework/single_datasets/test_inference_seeding.py -q
Factorial check of feature seeding and segmented atom aggregation under
TF32 production math, with ambient RNG pollution between feature and
forward repeats.
Hand ownership of the inference trunk pair into rollout so confidence can
drop it after cloning, removing ~1U of co-resident pair tensor from the
confidence peak.
Bound shared PairBlock working sets after the chunk-size tuner via
OPENFOLD3_TRI_ATTN_CHUNK_CAP, OPENFOLD3_TRIMUL_CHUNK_CAP, and
OPENFOLD3_TRANSITION_CHUNK_CAP. Unset keeps upstream defaults. Add a
stage profiler to attribute peaks under these caps.
Row-chunk inference diffusion pair+relpos embedding to avoid the full
N² concat transient, and process templates one-at-a-time on GPU so the
four-wide stack activation is never materialized.
@jandom jandom added the training Relating to the training pipeline label Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

training Relating to the training pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants