Skip to content

Reduce memory for no-KV KumoTabular and add a memory-efficient RankGaussian - #996

Open
JingangQu wants to merge 9 commits into
estimator-batching-fixesfrom
nokv-mem-opt
Open

JingangQu wants to merge 9 commits into
estimator-batching-fixesfrom
nokv-mem-opt

Conversation

@JingangQu

@JingangQu JingangQu commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #994.

Without the KV cache, KumoTabular embeds all context and query rows in one forward and post-processes all estimator outputs at once. On BeyondArena, Kumo-Tabular-L (16 estimators, contexts capped at 200k rows and 500 columns) runs out of memory on an 80 GB H100 for the largest tables, e.g. delivery_eta_1m in the forward and maps_router_eta_1m in the recipe fit. This PR runs the query side of the no-KV path in passes sized from the chunk memory limit that TransformerBlock already uses for batch_size_limit="auto", and cuts full-size temporaries in the recipe fit. All 142 BeyondArena core datasets now run without the KV cache, at unchanged speed, with predictions equal to #994 up to rare floating-point rounding.

Changes

  • One chunk memory limit (sdm/_memory.py): chunk_memory_limit(device) is the budget that attention "auto" batching already used (device memory × per-process fraction × SDM_CHUNK_MEMORY_FRACTION); attention and the TabFM cell embedding now call it, and split_size sizes balanced chunks within it. All new chunk and pass sizes derive from this limit or from the context size; none is hard-coded.
  • Row embedding in passes (row_embedding.py): without gradients on CUDA, when the query rows' cell buffer exceeds the limit, RowEmbedding embeds the context rows once and the query rows in balanced passes that replay the recorded context through sdm.cache.Cache, the same record and replay as fit/predict. Passes align with the row-attention chunks, so every row runs in a chunk of the same size as in a single pass; otherwise FlashAttention would round long rows differently in small chunks. The ICL block still runs once and now frees the label embedding and each layer's key/value early.
  • Recipe transforms and post-processing in passes (execution.py, base.py): RecipeExecution.transform and transform_output(outputs, dtype) process rows in passes. transform_output now also casts member outputs to dtype and inverts numerical targets pass by pass, so outputs stay in the model dtype until then; ICLModel passes the dtype of the first transformed query, before callbacks. Recipe documents the row independence this relies on.
  • Recipe fit memory (ensemble.py, choice.py, numerical/): evenly spaced ensemble members, such as Choice's round-robin options, are selected as views instead of copies; nanmean, isfinite and bool counts no longer make full-size copies; PowerTransform finds its bounds before allocating workspaces; RobustScale quantiles and ClipSigma transforms run in chunks within the limit. Processor outputs are bitwise unchanged.

Results

  • All processors and the default recipes are bitwise unchanged against Make estimator batching exact, robust and automatic #994.
  • BeyondArena (L, 16 estimators, H100 80 GB): all 507 tasks run without the KV cache, where earlier runs needed it on 8 datasets. The peak is 74.8 GiB on maps_router_eta_1m, and Elo is unchanged (1409 vs 1410 in one pool).
  • Speed: unchanged within noise (TabArena total −0.4% for L, −0.9% for S). A no-KV forward with a 200k-row context and 100k queries peaks at 12.9 GiB instead of 17.8 GiB.

Known limitation

The recipe fit is not chunked. On maps_router_eta_1m it still sets the 74.8 GiB peak, because Choice re-stacks its members while its input and option outputs are alive. This fits on an H100 80 GB and an RTX Pro 6000 96 GB.

RankGaussian from #993, memory-bounded

This PR also includes #993 (rank-Gaussian views and balanced class shifts in the Kumo recipe) and bounds the memory of its RankGaussian, which kept every fitted value and ran out of memory on maps_router_eta_1m with Kumo-Tabular-L:

  • Each column keeps at most max_knots (default 8192) knots. Columns with at most max_knots rows or distinct values keep all of them, so outputs are unchanged; larger columns keep the fitted values whose mid-ranks come closest to normal quantiles evenly spaced between the extremes, which stays within about two knot spacings (max |Δz| ≤ 2e-3 on large BeyondArena tables).
  • Fit processes columns and transform processes rows in chunks within the chunk memory limit.
  • Loaded state stays in double precision, and contexts and queries without rows are supported.
Metric (L / M / S) #993 + memory-efficient RankGaussian #993 with original RankGaussian Without #993
TabArena Elo 1959 / 1907 / 1790 1959 / 1907 / 1790 1951 / 1905 / 1785
BeyondArena Elo (142 datasets) 1424 / 1381 / 1322 n/a (L OOMs on maps_router) 1420 / 1371 / 1320
BeyondArena peak on maps_router, L (GiB) 78.3 OOM 74.8

- Move the `SDM_CHUNK_MEMORY_FRACTION` budget of attention and TabFM cell
  embedding chunks into one helper, `sdm._memory.chunk_memory_limit`.
- Expose the automatic attention batch size limit as
  `TransformerBlock.auto_batch_size_limit`, so callers can plan passes
  that align with its chunks.

Signed-off-by: Jingang Qu <jqu@nvidia.com>
- Find finite values without an `abs()` copy and count them without an
  int64 copy of the mask, and compute Standardize, ClipSigma and
  PowerTransform statistics with fewer full-size temporaries.
- Find the Yeo-Johnson bounds before allocating the PowerTransform
  workspaces.
- Compute RobustScale quantiles over column chunks and ClipSigma
  transforms over row chunks within the chunk memory limit.
- Transform in Standardize, ClipSoft and RobustScale with fewer
  temporaries.
- Select evenly spaced ensemble members as views, select shared
  DropConstantColumns groups before re-stacking members, and select Choice
  members one option at a time.

Signed-off-by: Jingang Qu <jqu@nvidia.com>
- Without gradients on CUDA, embed the context rows once and the query
  rows in balanced passes that replay the recorded context state, when
  the query cells would exceed the chunk memory limit.
- Align passes with the chunks of the row attention, so every row runs in
  a chunk of the same size as in a single pass. Passes then match a
  single pass up to rare rounding differences in small passes.
- Free the label embedding and each layer's full key/value early in the
  ICL block.

Signed-off-by: Jingang Qu <jqu@nvidia.com>
- Transform queries and post-process member outputs in passes over rows
  within the chunk memory limit in `RecipeExecution`.
- Keep member outputs in the model output dtype until post-processing,
  which casts them to the query dtype and inverts numerical targets pass
  by pass.
- Post-process the benchmark adapter's outputs through `transform_output`
  after freeing the transformed queries.

Signed-off-by: Jingang Qu <jqu@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 27, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@JingangQu
JingangQu changed the base branch from main to estimator-batching-fixes September 27, 2026 09:51
@JingangQu
JingangQu added this pull request to stack #997 September 27, 2026 13:35
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: NVIDIA/structured-data-models/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: c2505d6c-1917-4940-86e0-3be29db31419

📥 Commits

Reviewing files that changed from the base of the PR and between 2e6e613 and ec713ef.

📒 Files selected for processing (1)
  • test/models/test_base.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.


📝 Summary

Summary by CodeRabbit

  • New Features

    • Added a rank-based Gaussian transformation for numerical features, supporting ties, missing values, and large datasets.
    • Added balanced category shuffling to distribute shifts more evenly across ensemble members.
  • Performance

    • Large query sets and numerical processing now run in memory-sized passes on supported devices, helping reduce peak memory use.
    • Automatic attention batching now applies across device types.
  • Bug Fixes

    • Numerical transformations handle non-finite values more consistently, including during scaling and clipping.
    • Regression outputs now receive target inverse transforms during output processing and retain the requested dtype.

Walkthrough

The pull request adds memory-aware row chunking for model and recipe execution, updates numerical processors to use finite-value statistics and chunked operations, and adds RankGaussian and balanced categorical shifts. Tests cover chunked execution, non-finite values, and the new processor methods.

Changes

Memory-aware execution

Layer / File(s) Summary
Shared memory sizing and model batching
sdm/_memory.py, sdm/models/kumo/tabular/..., sdm/models/tabfm/..., sdm/nn/attention.py, sdm/models/base.py, benchmark/tabular/model.py, sdm/ensemble.py, test/models/kumo/tabular/..., test/models/test_base.py
Model paths use shared memory limits, cached row passes, and dtype-aware output transformation. Member selection can use slices for evenly spaced selections. CUDA tests compare chunked execution with single-pass execution.
Row-wise recipe transforms
sdm/processing/execution.py, sdm/processing/common/choice.py, sdm/processing/recipe.py, test/processing/test_execution.py
Recipe execution transforms queries and outputs in row-sized passes. Output values are cast to the requested dtype, with inverse target transforms applied for numerical targets. Choice tables are yielded lazily.

Numerical processor updates

Layer / File(s) Summary
Finite-value statistics and transforms
sdm/processing/numerical/..., test/processing/numerical/...
Numerical processors use shared finite-value statistics. RobustScale and ClipSigma process data in chunks. Tests cover non-finite values and constrained-memory execution.

Additional processor options

Layer / File(s) Summary
RankGaussian processor
sdm/processing/numerical/rank_gaussian.py, sdm/processing/numerical/__init__.py, sdm/processing/__init__.py, sdm/models/kumo/tabular/recipe.py, test/processing/numerical/test_rank_gaussian.py
Adds RankGaussian, which fits empirical rank knots and maps query values to normal quantiles. The Kumo numerical processor choices include RankGaussian. Tests cover ties, non-finite values, knot limits, dtype, and chunking.
Balanced categorical shifts
sdm/processing/categorical/shuffle.py, sdm/models/kumo/tabular/recipe.py, test/processing/categorical/test_shuffle.py, test/processing/test_contract.py
Adds the balanced_shift method and selects it for categorical Kumo targets. Tests check shift balance, reproducibility, and processor contract coverage.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RowEmbedding
  participant TransformerLayer
  participant Cache
  RowEmbedding->>TransformerLayer: Process context rows
  TransformerLayer->>Cache: Record context state
  RowEmbedding->>Cache: Freeze context state
  RowEmbedding->>TransformerLayer: Process query row chunks with cached state
Loading

Merge Risk: ⚪ Minimal · up to ec713

The reviewed test adjustment does not change CUDA execution, and no concrete user-impacting regression is established in the supplied context. No specific merge blocker remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.39% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 113 functions across 33 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main changes: reducing memory use for no-KV KumoTabular and adding a memory-efficient RankGaussian.
Description check ✅ Passed The description explains the memory changes, RankGaussian work, results, and known limitation. It is directly related to the changeset.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

ValterH and others added 4 commits September 27, 2026 09:51
- Keep every fitted value as a knot while a column has at most
  max_knots rows, and every distinct value while it has at most
  max_knots of them, so outputs are unchanged in both cases.
- Otherwise keep the fitted values whose mid-ranks come closest to normal
  quantiles evenly spaced between the extremes, which keeps outputs
  within about two knot spacings in normal scores.
- Fit columns and transform rows in chunks within the chunk memory limit.
- Keep loaded state in double precision, and support contexts and
  queries without rows.

Signed-off-by: Jingang Qu <jqu@nvidia.com>
@JingangQu JingangQu changed the title Run no-KV KumoTabular queries in passes and reduce recipe memory Run no-KV KumoTabular queries in passes, reduce recipe memory, and add a memory-efficient RankGaussian Sep 27, 2026
`TableTensor.cuda()` is typed to return `Tensor`, so the type check
rejected passing it to `MemberQuery._replace`. Cast it back to
`TableTensor`.

Signed-off-by: Jingang Qu <jqu@nvidia.com>
@JingangQu JingangQu changed the title Run no-KV KumoTabular queries in passes, reduce recipe memory, and add a memory-efficient RankGaussian Reduce memory for no-KV KumoTabular and add a memory-efficient RankGaussian Sep 27, 2026

This branch has not been deployed

No deployments
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