Skip to content

[#15793][perf] Optimize KV cache for unified memory systems (DGX Spark) - #12301

Open
mihai-chiorean wants to merge 12 commits into
NVIDIA:mainfrom
mihai-chiorean:feat/unified-memory-kv-cache-offload
Open

[#15793][perf] Optimize KV cache for unified memory systems (DGX Spark)#12301
mihai-chiorean wants to merge 12 commits into
NVIDIA:mainfrom
mihai-chiorean:feat/unified-memory-kv-cache-offload

Conversation

@mihai-chiorean

@mihai-chiorean mihai-chiorean commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #15793

On unified-memory systems like DGX Spark / GB10, CPU and GPU share the same physical LPDDR memory pool. The current KV cache sizing path can still treat GPU memory and host memory as separate tiers when host_cache_size is nonzero, but there is no separate host DRAM tier to offload to on these systems. cudaMemGetInfo already reports the unified pool, so the secondary host-cache budget should be folded away instead of tracked as a separate pool.

This patch adds integrated-device detection and folds the host KV cache tier on unified-memory systems:

  1. Detect true unified-memory devices: Add isUnifiedMemorySystem() in C++ using cudaDevAttrIntegrated. This intentionally does not use cudaDevAttrPageableMemoryAccess, which can also be true on HMM-enabled discrete GPUs.

  2. Fold Python host_cache_size defaults: Add unified_memory_detected to KvCacheConfig, auto-detect via is_device_integrated(), and force host_cache_size to 0 when unified memory is detected.

  3. Defensive C++ budget guard: In calculateFreeMemBytes(), force freeSecondaryMemBytes = 0 on unified-memory systems if C++ executor users bypass the Python validator and still provide a nonzero host_cache_size.

All changes are gated on integrated-device detection and preserve existing behavior on discrete GPU systems.

Test Coverage

  • Verified is_device_integrated() returns true on DGX Spark GB10 / SM121 with about 121.7 GB visible memory.
  • Verified KvCacheConfig auto-detects the unified-memory device path.
  • Pre-commit checks pass.
  • Existing KV cache manager unit tests require LLM_MODELS_ROOT and full model data, so they are deferred to NVIDIA CI.

Test plan

  • Verify integrated-device detection on DGX Spark / GB10 / SM121.
  • Verify KvCacheConfig auto-detects the unified-memory device path.
  • Pre-commit checks pass (clang-format, codespell, DCO).
  • Build and run existing KV cache manager unit tests on discrete GPU — deferred to NVIDIA CI because model data is not available locally.
  • Run inference workload on DGX Spark — deferred to NVIDIA CI because full model weights are not available locally.

PR Checklist

Please review the following before submitting your PR:

  • PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.

  • PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.

  • Test cases are provided for new code paths (see test instructions)

  • Any new dependencies have been scanned for license and vulnerabilities

  • CODEOWNERS updated if ownership changes

  • Documentation updated as needed

  • Update tava architecture diagram if there is a significant design change in PR.

  • The reviewers assigned automatically/manually are appropriate for the PR.

  • Please check this after reviewing the above items as appropriate for this PR.

Dev Engineer Review

  • Added common::isUnifiedMemorySystem() using cudaDevAttrIntegrated, correctly avoiding cudaDevAttrPageableMemoryAccess and preserving discrete-GPU behavior.
  • Unified-memory KV-cache handling consolidates host cache accounting into the primary pool, avoids unnecessary secondary-pool transfers, and retains existing paths for discrete GPUs.
  • Python configuration adds automatic unified-memory detection and optimization controls; C++ safeguards protect callers that bypass Python validation.
  • No test files or configuration/test-list changes were modified.

QA Engineer Review

No test changes.

@mihai-chiorean
mihai-chiorean requested review from a team as code owners March 18, 2026 03:15
@mihai-chiorean
mihai-chiorean requested a review from hchings March 18, 2026 03:15
@coderabbitai

coderabbitai Bot commented Mar 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: de7dbab8-bed9-4bb2-b874-a6ae7154bafb

📥 Commits

Reviewing files that changed from the base of the PR and between 90c4c1d and cb863d9.

📒 Files selected for processing (1)
  • cpp/include/tensorrt_llm/common/cudaUtils.h

Walkthrough

The changes detect integrated CUDA devices and adapt KV cache configuration and memory management for unified CPU-GPU memory, including secondary-pool allocation, transfer handling, and memory-budget accounting.

Changes

Unified memory KV cache handling

Layer / File(s) Summary
Unified-memory detection and configuration
cpp/include/tensorrt_llm/common/cudaUtils.h, tensorrt_llm/llmapi/llm_args.py
Adds isUnifiedMemorySystem() using cudaDevAttrIntegrated and adds optional unified-memory optimization configuration with automatic device-based detection.
KV cache allocation and accounting
cpp/tensorrt_llm/batch_manager/kvCacheManager.cpp
Uses GPU-accessible memory for the secondary pool on unified systems, skips redundant onboard/offload transfers, and folds the secondary host-cache budget into the primary pool.
Header metadata
cpp/include/tensorrt_llm/common/cudaUtils.h
Updates the SPDX copyright year range through 2026.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant API as KvCacheConfig
    participant CUDA as CUDA runtime
    participant KVMgr as KV cache manager

    App->>API: Create cache configuration
    API->>CUDA: Detect integrated device
    CUDA-->>API: Unified-memory status
    API-->>App: Apply unified-memory optimization defaults
    App->>KVMgr: Allocate and size KV cache pools
    alt Unified-memory system
        KVMgr->>KVMgr: Use GPU-accessible secondary memory
        KVMgr->>KVMgr: Skip onboard/offload transfers
        KVMgr->>KVMgr: Fold secondary budget into primary pool
    else Discrete GPU system
        KVMgr->>KVMgr: Preserve host-pool and transfer behavior
    end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the unified-memory KV cache optimization and matches the linked issue.
Description check ✅ Passed The PR description covers the summary, test coverage, test plan, and checklist, though it is not exactly the template format.
Linked Issues check ✅ Passed The changes implement the issue's required integrated-device detection, host-cache folding, and discrete-GPU preservation.
Out of Scope Changes check ✅ Passed No unrelated code changes are evident beyond the requested unified-memory KV cache work and a header year update.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cpp/include/tensorrt_llm/common/cudaUtils.h`:
- Around line 320-331: The function isUnifiedMemorySystem caches a single global
result in sIsUnified but queries the current device (cudaGetDevice), so if the
active CUDA device changes later the cached value can be stale; update
isUnifiedMemorySystem to either query the device attributes on every call
(remove the static sIsUnified lambda) or maintain a per-device cache keyed by
the device id (call cudaGetDevice, then consult/populate a map< int, bool >
using cudaDeviceGetAttribute and check_cuda_error) so the returned
pageable-memory flag correctly reflects the current device.

In `@tensorrt_llm/llmapi/llm_args.py`:
- Around line 2205-2208: The try/except around the call to
is_device_integrated() should stop catching broad Exception; replace the broad
catch with a specific catch for RuntimeError so CUDA-related failures are
handled while other exceptions propagate. Locate the block where unified =
is_device_integrated() is called and change the except Exception to except
RuntimeError, leaving unified=False on that branch and not swallowing other
errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bdc552fe-e226-42ae-866b-31769cdf4f1c

📥 Commits

Reviewing files that changed from the base of the PR and between 2f94dda and 90c4c1d.

📒 Files selected for processing (3)
  • cpp/include/tensorrt_llm/common/cudaUtils.h
  • cpp/tensorrt_llm/batch_manager/kvCacheManager.cpp
  • tensorrt_llm/llmapi/llm_args.py

Comment on lines +320 to +331
inline bool isUnifiedMemorySystem()
{
static bool const sIsUnified = []()
{
int device{-1};
check_cuda_error(cudaGetDevice(&device));
int pageableMemAccess{0};
check_cuda_error(cudaDeviceGetAttribute(&pageableMemAccess, cudaDevAttrPageableMemoryAccess, device));
return pageableMemAccess != 0;
}();
return sIsUnified;
}

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.

⚠️ Potential issue | 🟠 Major

Device-specific detection is cached globally

Line 322 caches the result once for the process, but the query is for the current CUDA device. If the active device changes later, this can return stale data and apply the wrong KV-cache path.

💡 Proposed fix (evaluate per call to avoid stale cross-device cache)
 inline bool isUnifiedMemorySystem()
 {
-    static bool const sIsUnified = []()
-    {
-        int device{-1};
-        check_cuda_error(cudaGetDevice(&device));
-        int pageableMemAccess{0};
-        check_cuda_error(cudaDeviceGetAttribute(&pageableMemAccess, cudaDevAttrPageableMemoryAccess, device));
-        return pageableMemAccess != 0;
-    }();
-    return sIsUnified;
+    int device{-1};
+    check_cuda_error(cudaGetDevice(&device));
+    int pageableMemAccess{0};
+    check_cuda_error(cudaDeviceGetAttribute(&pageableMemAccess, cudaDevAttrPageableMemoryAccess, device));
+    return pageableMemAccess != 0;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/include/tensorrt_llm/common/cudaUtils.h` around lines 320 - 331, The
function isUnifiedMemorySystem caches a single global result in sIsUnified but
queries the current device (cudaGetDevice), so if the active CUDA device changes
later the cached value can be stale; update isUnifiedMemorySystem to either
query the device attributes on every call (remove the static sIsUnified lambda)
or maintain a per-device cache keyed by the device id (call cudaGetDevice, then
consult/populate a map< int, bool > using cudaDeviceGetAttribute and
check_cuda_error) so the returned pageable-memory flag correctly reflects the
current device.

Comment thread tensorrt_llm/llmapi/llm_args.py Outdated
Comment on lines +2205 to +2208
try:
unified = is_device_integrated()
except Exception:
unified = False

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify broad exception handling remains removed from this validator.
rg -nP --type=py -C2 'except\s+Exception\s*:' tensorrt_llm/llmapi/llm_args.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 290


🏁 Script executed:

# Find and examine the is_device_integrated() function definition
rg -n 'def is_device_integrated' --type=py -A5

Repository: NVIDIA/TensorRT-LLM

Length of output: 525


🏁 Script executed:

# Check for all broad exception handlers in llm_args.py
rg -nP --type=py 'except\s+(Exception|BaseException|:)' tensorrt_llm/llmapi/llm_args.py | head -20

Repository: NVIDIA/TensorRT-LLM

Length of output: 249


🏁 Script executed:

# Get broader context around lines 2205-2208
sed -n '2200,2215p' tensorrt_llm/llmapi/llm_args.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 840


🏁 Script executed:

# Get the full implementation of is_device_integrated()
sed -n '1362,1400p' tensorrt_llm/_utils.py

Repository: NVIDIA/TensorRT-LLM

Length of output: 1473


🏁 Script executed:

# Check if is_device_integrated() can raise specific exceptions
rg -n 'is_device_integrated' tensorrt_llm/_utils.py -A20

Repository: NVIDIA/TensorRT-LLM

Length of output: 944


Catch specific exceptions instead of broad Exception

Line 2207 catches Exception, which masks unrelated errors and complicates debugging. The is_device_integrated() function raises RuntimeError on CUDA-related failures, so catch that specifically. Avoid AssertionError in the proposed fix—this function does not raise it.

Suggested fix
         if self.enable_unified_memory_optimization is None:
             try:
                 unified = is_device_integrated()
-            except Exception:
+            except RuntimeError:
+                logger.debug(
+                    "Unified-memory auto-detection failed; defaulting to disabled")
                 unified = False
             object.__setattr__(self, 'enable_unified_memory_optimization',
                                unified)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
unified = is_device_integrated()
except Exception:
unified = False
if self.enable_unified_memory_optimization is None:
try:
unified = is_device_integrated()
except RuntimeError:
logger.debug(
"Unified-memory auto-detection failed; defaulting to disabled")
unified = False
object.__setattr__(self, 'enable_unified_memory_optimization',
unified)
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 2207-2207: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tensorrt_llm/llmapi/llm_args.py` around lines 2205 - 2208, The try/except
around the call to is_device_integrated() should stop catching broad Exception;
replace the broad catch with a specific catch for RuntimeError so CUDA-related
failures are handled while other exceptions propagate. Locate the block where
unified = is_device_integrated() is called and change the except Exception to
except RuntimeError, leaving unified=False on that branch and not swallowing
other errors.

@svc-trtllm-gh-bot svc-trtllm-gh-bot added the Community want to contribute PRs initiated from Community label Mar 18, 2026
@nvpohanh

Copy link
Copy Markdown
Collaborator

@eopXD could you review this? thanks

@pengbowang-nv

Copy link
Copy Markdown
Collaborator

I wonder why we need a secondary pool at all on DGX Spark. Also do you have any test result or evidence on memory usage reduction? Thanks!

@mihai-chiorean

Copy link
Copy Markdown
Contributor Author

@pengbowang-nv We don't need a secondary pool on DGX Spark — hence freeSecondaryMemBytes = 0. On unified memory, the offload/onboard memcpy copies data to the same physical address since CPU and GPU share the same LPDDR5x pool.

Tested on DGX Spark GB10 (SM121, 128GB unified memory):

[TRT-LLM] [I] Unified memory detected: host_cache_size (1073741824 bytes) will be folded
           into primary GPU pool by the C++ runtime.

is_device_integrated() returns True, the validator detects unified memory, and host_cache_size is folded into the primary pool. The C++ runtime then sets freeSecondaryMemBytes = 0, so blocksInSecondaryPool = 0 — no secondary pool is allocated. The memcpy skips in the offload/onboard paths are defensive for the edge case where a user explicitly forces host_cache_size.

Rebased on latest main, no conflicts.

@pengbowang-nv

Copy link
Copy Markdown
Collaborator

I think we shouldn't allow a secondary pool with non-zero size on DGX Spark, it meaningless since we want secondary pool to hold what first pool cannot hold.
I think we should set self.host_cache_size to zero forcefully on DGX Spark like devices. @eopXD and @nvpohanh
for comment. Though for now we already have self.host_cache_size default to 0 so this might have less impact. Thanks!

@mihai-chiorean
mihai-chiorean force-pushed the feat/unified-memory-kv-cache-offload branch from c86f04c to 91dd9a5 Compare March 25, 2026 23:28
@mihai-chiorean

Copy link
Copy Markdown
Contributor Author

@pengbowang-nv Good point — updated. host_cache_size is now forced to 0 on unified memory systems instead of just logging. The Python validator calls object.__setattr__(self, 'host_cache_size', 0) when is_device_integrated() returns True, which is consistent with the C++ side already setting freeSecondaryMemBytes = 0.

@mihai-chiorean

Copy link
Copy Markdown
Contributor Author

@pengbowang-nv what do you think? I've addressed your feedback.

@pengbowang-nv

Copy link
Copy Markdown
Collaborator

Thanks! I'm thinking of complete remove these code guarding operation under unified memory situation, which means we assume that we won't be using secondary pool on unified memory at all. So just guard on python side would be enough. I'm also waiting for @eopXD to give some advice as I'm not sure of impact of such change.

@mihai-chiorean

mihai-chiorean commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! I'm thinking of complete remove these code guarding operation under unified memory situation, which means we assume that we won't be using secondary pool on unified memory at all. So just guard on python side would be enough. I'm also waiting for @eopXD to give some advice as I'm not sure of impact of such change.

yeaa that's a fair call.happy to give that a try if that's the preference.

@mihai-chiorean

mihai-chiorean commented Apr 1, 2026

Copy link
Copy Markdown
Contributor Author

@pengbowang-nv I simplified it. C++ footprint goes from 5 guard sites to 1. Python-side host_cache_size=0 enforcement stays as the primary path.

@mihai-chiorean

Copy link
Copy Markdown
Contributor Author

I was out for a few days. @eopXD any chance you can take a look at this? would be very helpful. Or is @nvpohanh the better person for this?

@mihai-chiorean
mihai-chiorean force-pushed the feat/unified-memory-kv-cache-offload branch from 0e3f2b0 to 89babd4 Compare May 6, 2026 04:07
@nvpohanh

nvpohanh commented May 8, 2026

Copy link
Copy Markdown
Collaborator

@eopXD Could you review this? Thanks

@mihai-chiorean

Copy link
Copy Markdown
Contributor Author

@eopXD any chance you could take a look? or @hchings ?

@mihai-chiorean

Copy link
Copy Markdown
Contributor Author

@hchings — friendly bump. This is a small, scoped change for unified-memory systems (DGX Spark / SM121):

  • cpp/include/tensorrt_llm/common/cudaUtils.h (+18): isUnifiedMemorySystem() using cudaDevAttrIntegrated
  • cpp/tensorrt_llm/batch_manager/kvCacheManager.cpp (+18): single guard at calculateFreeMemBytes setting freeSecondaryMemBytes = 0 on UMA
  • tensorrt_llm/llmapi/llm_args.py (+38): unified_memory_detected field + validator forcing host_cache_size = 0

@nvpohanh originally suggested @eopXD might be a good reviewer here. If neither of you is the right person, please feel free to redirect to the kv-cache-manager-devs group — I can't add team reviewers as an external contributor.

… Spark)

On Grace Blackwell systems (e.g. DGX Spark / GB10), CPU and GPU share
the same 128 GB LPDDR5x physical memory pool. The KV cache block
manager treats GPU and CPU as separate tiers, which forces a non-zero
host_cache_size to allocate a meaningless secondary pool and triggers
unnecessary memcpy for offload/onload that are physically no-ops.

Changes:

- Add isUnifiedMemorySystem() to cudaUtils.h, using cudaDevAttrIntegrated
  (not cudaDevAttrPageableMemoryAccess, which returns true on any
  HMM-enabled discrete GPU). Queries each call -- the attribute lookup
  is cheap and supports mixed-device processes.

- Add unified_memory_detected field to KvCacheConfig in Python, with
  auto-detection via is_device_integrated(). When True, host_cache_size
  is forced to 0 in the validator. This is the primary guard.

- Add a single defensive guard in calculateFreeMemBytes() that forces
  freeSecondaryMemBytes=0 on unified memory if host_cache_size is still
  non-zero (e.g. C++ executor API users bypassing the Python validator).

All changes are no-ops on discrete GPU systems.

Refs: NVIDIA#3130
Signed-off-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>
@mihai-chiorean
mihai-chiorean force-pushed the feat/unified-memory-kv-cache-offload branch from b370d78 to 893d3f4 Compare June 25, 2026 22:29
@mihai-chiorean

Copy link
Copy Markdown
Contributor Author

@hchings @pengbowang-nv @nvpohanh can I get a CI run..? this seems good to go

@moraxu

moraxu commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

/bot run

@mihai-chiorean mihai-chiorean changed the title [None][perf] Optimize KV cache for unified memory systems (DGX Spark) [#15793][perf] Optimize KV cache for unified memory systems (DGX Spark) Jun 30, 2026
Comment thread tensorrt_llm/llmapi/llm_args.py Outdated
Comment thread cpp/tensorrt_llm/batch_manager/kvCacheManager.cpp Outdated
Signed-off-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>
Signed-off-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>

# Conflicts:
#	tensorrt_llm/llmapi/llm_args.py
Signed-off-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>
@mihai-chiorean
mihai-chiorean requested a review from a team as a code owner July 20, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Community want to contribute PRs initiated from Community

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Redundant KV host cache tier on DGX Spark unified-memory systems

7 participants