[ROCm] Cpu offload for ROCm 7.13+ to align the hipMemcpyBatchAsync params and perf in 7.14x (#43018)
Signed-off-by: Hongxia Yang <hongxia.yang@amd.com> Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>
Hongxia Yang committed
Aug 21, 2026 at 12:07 UTC
cd7b7c265a30b8c3e5c908d59f4cb0d7dd8f8ff3
4 files changed
+338
-59
csrc/libtorch_stable/cache_kernels.cu
+76
-19
@@ -16,6 +16,7 @@
16
#include <algorithm>
17
#include <cassert>
18
#include <cfloat>
19
+#include <cstdlib>
20
21
#ifdef USE_ROCM
22
#include <hip/hip_bf16.h>
@@ -76,6 +77,31 @@ void swap_blocks(torch::stable::Tensor& src, torch::stable::Tensor& dst,
77
}
78
}
79
80
+namespace {
81
+// ROCm hipMemcpyBatchAsync faults for count > 8192 (MI350X/gfx950), so chunk
82
+// at that ceiling.
83
+constexpr int64_t kRocmDefaultMaxBatchDescriptors = 8192;
84
+
85
+constexpr const char* kMaxBatchDescriptorsEnv =
86
+ "VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS";
87
+
88
+// Max copy descriptors per batch-memcpy call (0 = unlimited). ROCm caps and
89
+// chunks; CUDA is uncapped. The env var (>0) overrides on any platform.
90
+int64_t resolve_max_batch_descriptors() {
91
+ static const int64_t cached = []() -> int64_t {
92
+ const char* val = std::getenv(kMaxBatchDescriptorsEnv);
93
+ const int64_t override_val = val ? std::atoll(val) : 0;
94
+ if (override_val > 0) return override_val;
95
+#if defined(USE_ROCM)
96
+ return kRocmDefaultMaxBatchDescriptors;
97
+#else
98
+ return 0;
99
+#endif
100
+ }();
101
+ return cached;
102
+}
103
+} // namespace
104
+
105
void swap_blocks_batch(const torch::stable::Tensor& src_ptrs,
106
const torch::stable::Tensor& dst_ptrs,
107
const torch::stable::Tensor& sizes,
@@ -142,32 +168,63 @@ void swap_blocks_batch(const torch::stable::Tensor& src_ptrs,
168
: CU_MEMCPY_SRC_ACCESS_ORDER_STREAM;
169
size_t attrs_idx = 0;
170
size_t fail_idx = 0;
145
- CUresult result = batch_fn(reinterpret_cast<CUdeviceptr*>(dst_data),
146
- reinterpret_cast<CUdeviceptr*>(src_data),
147
- reinterpret_cast<size_t*>(size_data),
148
- static_cast<size_t>(n), &attr, &attrs_idx, 1,
149
- &fail_idx, static_cast<CUstream>(stream));
150
- STD_TORCH_CHECK(result == CUDA_SUCCESS,
151
- "cuMemcpyBatchAsync failed at index ", fail_idx,
152
- " with error ", result);
171
+ // Uncapped on CUDA (max_desc == 0 -> single call) unless overridden by
172
+ // VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS; chunk to honor the override.
173
+ const int64_t max_desc = resolve_max_batch_descriptors();
174
+ const int64_t step = max_desc <= 0 ? n : max_desc;
175
+ for (int64_t off = 0; off < n; off += step) {
176
+ const int64_t cnt = std::min(step, n - off);
177
+ CUresult result = batch_fn(reinterpret_cast<CUdeviceptr*>(dst_data + off),
178
+ reinterpret_cast<CUdeviceptr*>(src_data + off),
179
+ reinterpret_cast<size_t*>(size_data + off),
180
+ static_cast<size_t>(cnt), &attr, &attrs_idx, 1,
181
+ &fail_idx, static_cast<CUstream>(stream));
182
+ STD_TORCH_CHECK(result == CUDA_SUCCESS,
183
+ "cuMemcpyBatchAsync failed at index ", fail_idx,
184
+ " with error ", result);
185
+ }
186
return;
187
}
188
#elif defined(USE_ROCM) && defined(HIP_VERSION) && HIP_VERSION >= 70100000
156
- // ROCm 7.1+ exposes hipMemcpyBatchAsync. The 7.2.1 implementation early-
157
- // returns hipErrorNotSupported whenever numAttrs > 0 (see ROCm/clr @
158
- // rocm-7.2.1 hipamd/src/hip_memory.cpp:2819-2822), so call with
159
- // numAttrs=0.
189
+ // ROCm 7.1+ exposes hipMemcpyBatchAsync. ROCm 7.2.1-7.2.3 early-return
190
+ // hipErrorNotSupported whenever numAttrs > 0 (see ROCm/clr @ rocm-7.2.1
191
+ // hipamd/src/hip_memory.cpp:2819-2822), so those releases must call with
192
+ // numAttrs=0. ROCm 7.13+ accepts numAttrs > 0.
193
+ // rocm-7.14+ has better performance.
194
{
195
hipMemcpyAttributes attr = {};
196
size_t attrs_idx = 0;
197
size_t fail_idx = 0;
164
- hipError_t result = hipMemcpyBatchAsync(
165
- reinterpret_cast<void**>(dst_data), reinterpret_cast<void**>(src_data),
166
- reinterpret_cast<size_t*>(size_data), static_cast<size_t>(n), &attr,
167
- &attrs_idx, 0, &fail_idx, static_cast<hipStream_t>(stream));
168
- STD_TORCH_CHECK(result == hipSuccess,
169
- "hipMemcpyBatchAsync failed at index ", fail_idx,
170
- " with error ", result);
198
+ size_t num_attrs = 0;
199
+ #if HIP_VERSION >= 71300000
200
+ static const bool runtime_accepts_attrs = []() {
201
+ int runtime_version = 0;
202
+ return hipRuntimeGetVersion(&runtime_version) == hipSuccess &&
203
+ runtime_version >= 71300000;
204
+ }();
205
+ if (runtime_accepts_attrs) {
206
+ attr.srcAccessOrder = is_src_access_order_any
207
+ ? hipMemcpySrcAccessOrderAny
208
+ : hipMemcpySrcAccessOrderStream;
209
+ num_attrs = 1;
210
+ }
211
+ #endif
212
+ // hipMemcpyBatchAsync faults (GPU memory access fault) above 8192
213
+ // descriptors/call on ROCm 7.15, so chunk the batch at the resolved cap.
214
+ const int64_t max_desc = resolve_max_batch_descriptors();
215
+ const int64_t step = max_desc <= 0 ? n : max_desc;
216
+ for (int64_t off = 0; off < n; off += step) {
217
+ const int64_t cnt = std::min(step, n - off);
218
+ hipError_t result = hipMemcpyBatchAsync(
219
+ reinterpret_cast<void**>(dst_data + off),
220
+ reinterpret_cast<void**>(src_data + off),
221
+ reinterpret_cast<size_t*>(size_data + off), static_cast<size_t>(cnt),
222
+ &attr, &attrs_idx, num_attrs, &fail_idx,
223
+ static_cast<hipStream_t>(stream));
224
+ STD_TORCH_CHECK(result == hipSuccess,
225
+ "hipMemcpyBatchAsync failed at index ", fail_idx,
226
+ " with error ", result);
227
+ }
228
return;
229
}
230
#endif
tests/v1/simple_kv_offload/test_hip_mem_ops.py
new
+145
@@ -0,0 +1,145 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Unit tests for the HIP/ROCm paths in vllm.v1.simple_kv_offload.cuda_mem_ops.
4
+
5
+Deliberately not skipped off ROCm: the version gate takes a plain version int
6
+and the library loader is monkeypatched, so nothing here needs a HIP runtime
7
+and the logic stays covered by the default (NVIDIA) CI that runs this
8
+directory.
9
+"""
10
+
11
+import ctypes
12
+
13
+import numpy as np
14
+import pytest
15
+
16
+from vllm.v1.simple_kv_offload import cuda_mem_ops
17
+from vllm.v1.simple_kv_offload.cuda_mem_ops import (
18
+ BatchMemcpyParams,
19
+ _CUmemcpyAttributes,
20
+ _load_hip_runtime,
21
+ _num_attrs_for_hip_version,
22
+ copy_blocks,
23
+)
24
+
25
+
26
+# hipMemcpyBatchAsync rejects numAttrs > 0 on ROCm 7.2.1-7.2.3 and accepts it
27
+# on 7.13+. HIP encodes the version as major*10_000_000 + minor*100_000 + patch.
28
+@pytest.mark.parametrize(
29
+ ("version", "expected"),
30
+ [
31
+ (0, 0), # unknown (hipRuntimeGetVersion failed) -> conservative 0
32
+ (70_100_000, 0), # ROCm 7.1
33
+ (70_200_000, 0), # ROCm 7.2.0
34
+ (70_253_211, 0), # ROCm 7.2.2 (patch 53211)
35
+ (70_300_000, 0), # ROCm 7.3
36
+ (71_200_000, 0), # ROCm 7.12 -- just below the 7.13 cutoff
37
+ (71_300_000, 1), # ROCm 7.13 -- first release accepting numAttrs > 0
38
+ (71_326_193, 1), # ROCm 7.13 nightly (patch 26193)
39
+ (71_400_000, 1), # ROCm 7.14
40
+ (71_500_000, 1), # ROCm 7.15
41
+ (80_000_000, 1), # ROCm 8.0
42
+ ],
43
+)
44
+def test_num_attrs_for_hip_version(version: int, expected: int):
45
+ assert _num_attrs_for_hip_version(version) == expected
46
+
47
+
48
+@pytest.mark.parametrize(
49
+ ("available", "expected"),
50
+ [
51
+ # Devel install: the unversioned symlink is present and preferred.
52
+ ({"libamdhip64.so", "libamdhip64.so.7"}, "libamdhip64.so"),
53
+ # Runtime-only / wheel-packaged ROCm: versioned soname only.
54
+ ({"libamdhip64.so.7"}, "libamdhip64.so.7"),
55
+ ({"libamdhip64.so.6"}, "libamdhip64.so.6"),
56
+ ],
57
+)
58
+def test_load_hip_runtime_falls_back_to_versioned_soname(
59
+ monkeypatch, available: set[str], expected: str
60
+):
61
+ loaded = []
62
+
63
+ def fake_cdll(name, mode=0):
64
+ if name not in available:
65
+ raise OSError(f"{name}: cannot open shared object file")
66
+ loaded.append(name)
67
+ return name
68
+
69
+ monkeypatch.setattr(cuda_mem_ops.ctypes, "CDLL", fake_cdll)
70
+ assert _load_hip_runtime() == expected
71
+ assert loaded == [expected]
72
+
73
+
74
+def test_load_hip_runtime_reports_every_candidate_when_all_fail(monkeypatch):
75
+ def fake_cdll(name, mode=0):
76
+ raise OSError("cannot open shared object file")
77
+
78
+ monkeypatch.setattr(cuda_mem_ops.ctypes, "CDLL", fake_cdll)
79
+ with pytest.raises(OSError, match="could not load the HIP runtime") as exc:
80
+ _load_hip_runtime()
81
+ for name in ("libamdhip64.so", "libamdhip64.so.7", "libamdhip64.so.6"):
82
+ assert name in str(exc.value)
83
+
84
+
85
+def _fake_params(num_layers: int, bytes_per_block: int = 512) -> BatchMemcpyParams:
86
+ """Minimal params for driving copy_blocks() without a real device.
87
+
88
+ Bases are 0 and the batch-memcpy function is monkeypatched, so no memory is
89
+ ever dereferenced; only the descriptor bookkeeping (count/chunking) matters.
90
+ """
91
+ return BatchMemcpyParams(
92
+ src_bases=np.zeros(num_layers, dtype=np.uint64),
93
+ dst_bases=np.zeros(num_layers, dtype=np.uint64),
94
+ bpb=np.full(num_layers, bytes_per_block, dtype=np.uint64),
95
+ num_layers=num_layers,
96
+ attrs=_CUmemcpyAttributes(),
97
+ attrs_idx=ctypes.c_size_t(0),
98
+ num_attrs=0,
99
+ fail_idx=ctypes.c_size_t(0),
100
+ stream_handle=0,
101
+ )
102
+
103
+
104
+def _record_counts(monkeypatch) -> list[int]:
105
+ """Patch the batch-memcpy call to record the descriptor count per call."""
106
+ counts: list[int] = []
107
+
108
+ def fake_fn(dst, src, sizes, count, *rest):
109
+ counts.append(count)
110
+ return 0
111
+
112
+ monkeypatch.setattr(cuda_mem_ops, "_batch_memcpy", (fake_fn, 0))
113
+ return counts
114
+
115
+
116
+# ROCm chunks the descriptor batch at the cap (hipMemcpyBatchAsync faults above
117
+# 8192/call); CUDA leaves the cap at 0 and issues a single call.
118
+@pytest.mark.parametrize(
119
+ ("max_desc", "num_layers", "num_blocks", "expected_counts"),
120
+ [
121
+ (8192, 3, 5000, [8192, 6808]), # 15000 desc -> 8192 + 6808
122
+ (8192, 1, 8192, [8192]), # exactly at the cap -> single call
123
+ (8192, 1, 8193, [8192, 1]), # one over -> spills into a second call
124
+ (4096, 61, 135, [4096, 4096, 43]), # bs=1 Kimi shape (8235 desc)
125
+ (0, 61, 135, [8235]), # CUDA uncapped -> single call
126
+ ],
127
+)
128
+def test_copy_blocks_chunks_at_descriptor_cap(
129
+ monkeypatch, max_desc, num_layers, num_blocks, expected_counts
130
+):
131
+ counts = _record_counts(monkeypatch)
132
+ monkeypatch.setattr(cuda_mem_ops, "_max_batch_descriptors", max_desc)
133
+
134
+ ids = list(range(num_blocks))
135
+ copy_blocks(ids, ids, _fake_params(num_layers))
136
+
137
+ assert counts == expected_counts
138
+ assert sum(counts) == num_layers * num_blocks
139
+
140
+
141
+def test_copy_blocks_noop_on_empty(monkeypatch):
142
+ counts = _record_counts(monkeypatch)
143
+ monkeypatch.setattr(cuda_mem_ops, "_max_batch_descriptors", 8192)
144
+ copy_blocks([], [], _fake_params(num_layers=4))
145
+ assert counts == []
vllm/envs.py
+7
@@ -299,6 +299,7 @@ if TYPE_CHECKING:
299
VLLM_DEBUG_MFU_METRICS: bool = False
300
VLLM_WEIGHT_OFFLOADING_DISABLE_PIN_MEMORY: bool = False
301
VLLM_WEIGHT_OFFLOADING_DISABLE_UVA: bool = False
302
+ VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS: int = 0
303
VLLM_WSL2_ENABLE_PIN_MEMORY: bool = False
304
VLLM_DISABLE_LOG_LOGO: bool = False
305
VLLM_LORA_DISABLE_PDL: bool = False
@@ -2059,6 +2060,12 @@ environment_variables: dict[str, Callable[[], Any]] = {
2060
"VLLM_WEIGHT_OFFLOADING_DISABLE_UVA": lambda: bool(
2061
int(os.getenv("VLLM_WEIGHT_OFFLOADING_DISABLE_UVA", "0"))
2062
),
2063
+ # Max descriptors per CPU-KV-offload batch-memcpy call. 0 = platform default
2064
+ # (ROCm chunks at 8192, since hipMemcpyBatchAsync faults above that on
2065
+ # rocm 7.14/7.15 when batch copy is optimized; CUDA uncapped). Set >0 to override.
2066
+ "VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS": lambda: int(
2067
+ os.getenv("VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS", "0")
2068
+ ),
2069
# On WSL2 with a compatible kernel (>= 4.19.121), pinned memory is
2070
# supported but disabled by default due to a small performance regression.
2071
# Set to 1 when pinned memory or UVA is required (e.g. CPU offloading
vllm/v1/simple_kv_offload/cuda_mem_ops.py
+110
-40
@@ -8,6 +8,7 @@ from typing import Any, NamedTuple
8
import numpy as np
9
import torch
10
11
+import vllm.envs as envs
12
from vllm.logger import init_logger
13
from vllm.platforms import current_platform
14
@@ -58,27 +59,47 @@ _BATCH_MEMCPY_FUNC_TYPE = ctypes.CFUNCTYPE(
59
ctypes.c_void_p,
60
)
61
61
-# Resolved lazily on first use.
62
-_batch_memcpy_fn: Any = None
62
+# Resolved lazily on first use: (entry point, numAttrs to pass).
63
+_batch_memcpy: tuple[Any, int] | None = None
64
65
+# Max copy descriptors per batch call, resolved lazily on first use.
66
+_max_batch_descriptors: int | None = None
67
65
-def _resolve_batch_memcpy():
66
- """Resolve the platform batch-memcpy entry point (one-time).
68
+# ROCm hipMemcpyBatchAsync faults for count > 8192 (MI350X/gfx950), so we chunk
69
+# at that ceiling. Override via VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS.
70
+_ROCM_DEFAULT_MAX_BATCH_DESCRIPTORS = 8192
71
68
- * CUDA: ``cuMemcpyBatchAsync`` via ``cuGetProcAddress`` (uses
69
- srcAccessOrder=STREAM via one attributes entry).
70
- * ROCm: ``hipMemcpyBatchAsync`` from libamdhip64 (ROCm 7.1+). ROCm
71
- 7.2.1 or 7.2.2 rejects any call with ``numAttrs > 0``
72
- (see ROCm/clr @ rocm-7.2.1 hipamd/src/hip_memory.cpp:2819-2822), so
73
- we call with ``numAttrs=0``.
72
75
- Raises ``RuntimeError`` if the symbol is unavailable (older CUDA
76
- driver, ROCm < 7.1, unusual install). The connector requires the
77
- batch API.
73
+def _resolve_max_batch_descriptors() -> int:
74
+ """Max copy descriptors to pass to one batch-memcpy call (0 = unlimited).
75
+
76
+ ROCm's ``hipMemcpyBatchAsync`` faults above 8192 descriptors per call, so
77
+ on ROCm we cap and chunk larger transfers. CUDA's ``cuMemcpyBatchAsync``
78
+ handles arbitrary counts and is left uncapped. Set
79
+ ``VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS`` (>0) to override on any platform.
80
+ """
81
+ global _max_batch_descriptors
82
+ if _max_batch_descriptors is None:
83
+ override = envs.VLLM_KV_OFFLOAD_MAX_BATCH_DESCRIPTORS
84
+ if override > 0:
85
+ _max_batch_descriptors = override
86
+ else:
87
+ _max_batch_descriptors = (
88
+ _ROCM_DEFAULT_MAX_BATCH_DESCRIPTORS if current_platform.is_rocm() else 0
89
+ )
90
+ return _max_batch_descriptors
91
+
92
+
93
+def _resolve_batch_memcpy() -> tuple[Any, int]:
94
+ """Resolve the batch-memcpy entry point and its ``numAttrs`` (one-time).
95
+
96
+ CUDA uses ``cuMemcpyBatchAsync``; ROCm uses ``hipMemcpyBatchAsync``.
97
+ Raises ``RuntimeError`` if the symbol is unavailable (old CUDA driver,
98
+ ROCm < 7.1, unusual install).
99
"""
100
if current_platform.is_rocm():
101
try:
81
- lib = ctypes.CDLL("libamdhip64.so", mode=ctypes.RTLD_GLOBAL)
102
+ lib = _load_hip_runtime()
103
fn = lib.hipMemcpyBatchAsync
104
except (OSError, AttributeError) as e:
105
raise RuntimeError(
@@ -97,14 +118,55 @@ def _resolve_batch_memcpy():
118
ctypes.c_void_p, # failIdx
119
ctypes.c_void_p, # stream
120
]
100
- return fn
121
+ return fn, _rocm_num_attrs(lib)
122
123
from cuda.bindings import driver as drv
124
125
err, ptr, _ = drv.cuGetProcAddress(b"cuMemcpyBatchAsync", 12080, 0)
126
if err != drv.CUresult.CUDA_SUCCESS:
127
raise RuntimeError(f"cuGetProcAddress(cuMemcpyBatchAsync) failed: {err}")
107
- return _BATCH_MEMCPY_FUNC_TYPE(ptr)
128
+ return _BATCH_MEMCPY_FUNC_TYPE(ptr), 1
129
+
130
+
131
+def _load_hip_runtime() -> ctypes.CDLL:
132
+ """Load ``libamdhip64``, tolerating installs without the devel symlink.
133
+
134
+ The unversioned ``libamdhip64.so`` only ships with the ROCm devel package;
135
+ runtime-only and wheel-packaged ROCm installs provide just the versioned
136
+ soname. ``dlopen`` returns the already-mapped library when asked for a
137
+ soname the process has loaded — torch loads HIP at import — so the
138
+ versioned names resolve even when they are not on the loader search path.
139
+ """
140
+ errors = []
141
+ for name in ("libamdhip64.so", "libamdhip64.so.7", "libamdhip64.so.6"):
142
+ try:
143
+ return ctypes.CDLL(name, mode=ctypes.RTLD_GLOBAL)
144
+ except OSError as e:
145
+ errors.append(f"{name}: {e}")
146
+ raise OSError("could not load the HIP runtime: " + "; ".join(errors))
147
+
148
+
149
+def _rocm_num_attrs(lib: ctypes.CDLL) -> int:
150
+ """``numAttrs`` for ``hipMemcpyBatchAsync`` on the running HIP runtime."""
151
+ ver = ctypes.c_int(0)
152
+ try:
153
+ if lib.hipRuntimeGetVersion(ctypes.byref(ver)) != 0:
154
+ ver.value = 0
155
+ except (OSError, AttributeError):
156
+ ver.value = 0
157
+ return _num_attrs_for_hip_version(ver.value)
158
+
159
+
160
+def _num_attrs_for_hip_version(version: int) -> int:
161
+ """``numAttrs`` for ``hipMemcpyBatchAsync`` given a HIP runtime version int.
162
+
163
+ ROCm 7.2.1-7.2.3 reject ``numAttrs > 0`` (ROCm/clr @ rocm-7.2.1
164
+ hipamd/src/hip_memory.cpp:2819-2822); 7.13+ accept it. ``version`` 0
165
+ (unknown) yields the conservative 0.
166
+ """
167
+ # HIP encodes version as major*10_000_000 + minor*100_000 + patch.
168
+ major, minor = version // 10_000_000, (version // 100_000) % 100
169
+ return 1 if (major, minor) >= (7, 13) else 0
170
171
172
class BatchMemcpyParams(NamedTuple):
@@ -112,10 +174,12 @@ class BatchMemcpyParams(NamedTuple):
174
dst_bases: np.ndarray # [num_layers] uint64
175
bpb: np.ndarray # [num_layers] uint64 — bytes per block
176
num_layers: int
115
- # CUDA only: one attributes entry carrying srcAccessOrder. Unused on ROCm
116
- # (7.2.1 or 7.2.2) because the current runtime rejects numAttrs > 0.
177
+ # One attributes entry carrying srcAccessOrder. Ignored when num_attrs is
178
+ # 0, which is what ROCm runtimes older than 7.13 require (see
179
+ # _num_attrs_for_hip_version).
180
attrs: _CUmemcpyAttributes
181
attrs_idx: ctypes.c_size_t
182
+ num_attrs: int
183
# NOTE: cuMemcpyBatchAsync_v2() removed fail_idx field, but we use
184
# cuMemcpyBatchAsync() with fail_idx for backward compatibility
185
fail_idx: ctypes.c_size_t
@@ -128,9 +192,10 @@ def build_params(
192
stream: torch.cuda.Stream,
193
src_access_order: int = CU_MEMCPY_SRC_ACCESS_ORDER_ANY,
194
) -> BatchMemcpyParams:
131
- global _batch_memcpy_fn
132
- if _batch_memcpy_fn is None:
133
- _batch_memcpy_fn = _resolve_batch_memcpy()
195
+ global _batch_memcpy
196
+ if _batch_memcpy is None:
197
+ _batch_memcpy = _resolve_batch_memcpy()
198
+ _, num_attrs = _batch_memcpy
199
200
assert list(src_caches.keys()) == list(dst_caches.keys())
201
src_tensors = list(src_caches.values())
@@ -153,6 +218,7 @@ def build_params(
218
num_layers=len(src_tensors),
219
attrs=attrs,
220
attrs_idx=ctypes.c_size_t(0),
221
+ num_attrs=num_attrs,
222
fail_idx=ctypes.c_size_t(0),
223
stream_handle=stream.cuda_stream,
224
)
@@ -168,6 +234,9 @@ def copy_blocks(
234
if n == 0:
235
return
236
237
+ assert _batch_memcpy is not None, "build_params() must run before copy_blocks()"
238
+ fn, _ = _batch_memcpy
239
+
240
src_ids = np.array(src_block_ids, dtype=np.uint64)
241
dst_ids = np.array(dst_block_ids, dtype=np.uint64)
242
@@ -180,23 +249,24 @@ def copy_blocks(
249
sz_all = np.repeat(params.bpb, n)
250
total = n * params.num_layers
251
183
- # ROCm 7.2.1/7.2.2 rejects any call with numAttrs>0 (hipMemcpyBatchAsync
184
- # hipamd/src/hip_memory.cpp:2819-2822); CUDA uses one attrs entry so
185
- # srcAccessOrder is honored. attrs / attrsIdxs are ignored when
186
- # numAttrs==0, so we pass the same values from both paths.
187
- num_attrs = 0 if current_platform.is_rocm() else 1
188
- err = _batch_memcpy_fn(
189
- dst_all.ctypes.data,
190
- src_all.ctypes.data,
191
- sz_all.ctypes.data,
192
- total,
193
- ctypes.addressof(params.attrs),
194
- ctypes.byref(params.attrs_idx),
195
- num_attrs,
196
- ctypes.byref(params.fail_idx),
197
- params.stream_handle,
198
- )
199
- if err != 0:
200
- raise RuntimeError(
201
- f"batch memcpy failed: err={err} failIdx={params.fail_idx.value}"
252
+ # Chunk on ROCm: hipMemcpyBatchAsync faults above 8192 descriptors/call.
253
+ # CUDA is uncapped (max_desc == 0) and issues a single call.
254
+ max_desc = _resolve_max_batch_descriptors()
255
+ step = total if max_desc <= 0 else max_desc
256
+ for off in range(0, total, step):
257
+ cnt = min(step, total - off)
258
+ err = fn(
259
+ dst_all[off : off + cnt].ctypes.data,
260
+ src_all[off : off + cnt].ctypes.data,
261
+ sz_all[off : off + cnt].ctypes.data,
262
+ cnt,
263
+ ctypes.addressof(params.attrs),
264
+ ctypes.byref(params.attrs_idx),
265
+ params.num_attrs,
266
+ ctypes.byref(params.fail_idx),
267
+ params.stream_handle,
268
)
269
+ if err != 0:
270
+ raise RuntimeError(
271
+ f"batch memcpy failed: err={err} failIdx={params.fail_idx.value}"
272
+ )