Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / activation_memory / pinned_memory_pool.py: 88%
175 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Framework-independent pool for reusable pinned host memory."""
17import threading
18from collections import defaultdict
19from dataclasses import dataclass
20from typing import Any, Dict, List, Optional, Tuple
22import torch
24from . import _backend
26_MIN_BUCKET_BYTES = 1024
27_IN_USE = "in_use"
28_AVAILABLE = "available"
29_PENDING = "pending"
32@dataclass
33class _Block:
34 """A complete allocation owned by :class:`PinnedMemoryPool`."""
36 buffer: Any
37 capacity: int
38 state: str
39 event: Optional[Any] = None
40 storage_key: Optional[int] = None
43def _valid_positive_int(value: int, name: str) -> None:
44 if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
45 raise ValueError(f"{name} must be a positive integer, but got {value!r}.")
48def _storage_key(tensor: Any) -> int:
49 """Return a stable identity for a tensor's underlying storage."""
50 try:
51 return int(tensor.untyped_storage().data_ptr())
52 except (AttributeError, TypeError, ValueError) as exc:
53 raise ValueError("release() expects a tensor with an identifiable storage.") from exc
56class PinnedMemoryPool:
57 """Thread-safe, capacity-bounded pool of pinned CPU byte buffers.
59 The pool keeps complete one-dimensional ``uint8`` allocations and returns
60 length-limited views from :meth:`acquire`. A returned view may be cast to
61 another dtype by the caller, but no view may be used after :meth:`release`.
62 When no reusable block is available and a new pinned allocation would
63 exceed ``max_host_bytes``, :meth:`acquire` raises ``RuntimeError``.
65 Args:
66 max_host_bytes: Maximum total capacity retained by the pool.
67 align_limit: Largest size used by the power-of-two alignment policy.
69 Raises:
70 ValueError: If a constructor or ``acquire`` argument is invalid.
71 RuntimeError: If an allocation would exceed ``max_host_bytes`` and no
72 sufficient reusable block exists.
73 """
75 def __init__(self, max_host_bytes: int, align_limit: int = 2 * 1024**3) -> None:
76 _valid_positive_int(max_host_bytes, "max_host_bytes")
77 _valid_positive_int(align_limit, "align_limit")
78 self._max_host_bytes = max_host_bytes
79 self._align_limit = align_limit
80 self._available: Dict[int, List[_Block]] = defaultdict(list)
81 self._pending: Dict[int, List[_Block]] = defaultdict(list)
82 self._blocks: Dict[int, _Block] = {}
83 self._total_allocated = 0
84 self._lock = threading.Lock()
86 @property
87 def total_allocated(self) -> int:
88 """Return total aligned pinned capacity held by this pool."""
89 with self._lock:
90 return self._total_allocated
92 @property
93 def max_host_bytes(self) -> int:
94 """Return the pool's hard capacity limit in bytes."""
95 return self._max_host_bytes
97 @property
98 def align_limit(self) -> int:
99 """Return the largest alignment bucket size in bytes."""
100 return self._align_limit
102 def _aligned_size(self, size: int) -> int:
103 if size > self._align_limit:
104 return size
105 minimum = max(size, _MIN_BUCKET_BYTES)
106 power = 1 << (minimum - 1).bit_length()
107 aligned = min(power, self._align_limit)
108 return max(aligned, size)
110 def _reclaim_completed_locked(self, minimum_capacity: int) -> None:
111 """Move completed pending blocks into the available buckets."""
112 # A transfer batch may release many blocks with one event. Retain the
113 # event objects so each runtime event is queried only once per scan.
114 event_results: Dict[int, Tuple[Any, bool]] = {}
115 for capacity in list(self._pending):
116 if capacity < minimum_capacity:
117 continue
118 still_pending = []
119 for block in self._pending[capacity]:
120 event = block.event
121 event_key = id(event)
122 event_result = event_results.get(event_key)
123 if event_result is None:
124 completed = event.query()
125 event_results[event_key] = (event, completed)
126 else:
127 completed = event_result[1]
128 if completed:
129 block.event = None
130 block.state = _AVAILABLE
131 self._available[capacity].append(block)
132 else:
133 still_pending.append(block)
134 if still_pending:
135 self._pending[capacity] = still_pending
136 else:
137 del self._pending[capacity]
139 def _register_view_locked(self, block: _Block, view: Any) -> Any:
140 """Associate a checked-out view with its owning allocation."""
141 key = _storage_key(view)
142 owner = self._blocks.get(key)
143 if owner is not None and owner is not block:
144 raise RuntimeError("Two host buffers exposed the same storage identity.")
145 old_key = block.storage_key
146 if old_key is not None and old_key != key and self._blocks.get(old_key) is block:
147 del self._blocks[old_key]
148 self._blocks[key] = block
149 block.storage_key = key
150 return view
152 def _checkout_locked(self, block: _Block, size: int) -> Any:
153 """Create and register the exact view returned by :meth:`acquire`."""
154 view = block.buffer if size == block.capacity else block.buffer[:size]
155 return self._register_view_locked(block, view)
157 def _find_available_locked(self, minimum_capacity: int) -> Optional[_Block]:
158 for capacity in sorted(self._available):
159 if capacity >= minimum_capacity and self._available[capacity]:
160 block = self._available[capacity].pop()
161 if not self._available[capacity]:
162 del self._available[capacity]
163 block.state = _IN_USE
164 return block
165 return None
167 def _find_pending_locked(self, minimum_capacity: int) -> Optional[_Block]:
168 for capacity in sorted(self._pending):
169 if capacity >= minimum_capacity and self._pending[capacity]:
170 block = self._pending[capacity].pop(0)
171 if not self._pending[capacity]:
172 del self._pending[capacity]
173 block.state = _IN_USE
174 return block
175 return None
177 def acquire(self, size: int) -> Any:
178 """Acquire a pooled pinned CPU ``uint8`` view."""
179 _valid_positive_int(size, "size")
180 aligned_size = self._aligned_size(size)
182 with self._lock:
183 self._reclaim_completed_locked(aligned_size)
184 block = self._find_available_locked(aligned_size)
185 if block is not None:
186 try:
187 return self._checkout_locked(block, size)
188 except Exception:
189 block.state = _AVAILABLE
190 self._available[block.capacity].append(block)
191 raise
193 if self._total_allocated + aligned_size <= self._max_host_bytes:
194 self._total_allocated += aligned_size
195 reserved = True
196 else:
197 reserved = False
198 block = self._find_pending_locked(aligned_size)
199 if block is None:
200 raise RuntimeError(
201 "PinnedMemoryPool capacity exceeded: "
202 f"requested_bytes={size}, aligned_bytes={aligned_size}, "
203 f"pooled_bytes={self._total_allocated}, "
204 f"max_host_bytes={self._max_host_bytes}."
205 )
207 if reserved:
208 try:
209 buffer = _backend.alloc_tensor_buffer(
210 aligned_size,
211 torch.uint8,
212 device="cpu",
213 pin_memory=True,
214 )
215 except Exception:
216 with self._lock:
217 self._total_allocated -= aligned_size
218 raise
219 block = _Block(buffer, aligned_size, _IN_USE)
220 try:
221 with self._lock:
222 view = self._checkout_locked(block, size)
223 except Exception:
224 with self._lock:
225 self._total_allocated -= aligned_size
226 raise
227 return view
229 event = block.event
230 try:
231 event.synchronize()
232 except Exception:
233 with self._lock:
234 block.state = _PENDING
235 block.event = event
236 self._pending[block.capacity].insert(0, block)
237 raise
238 block.event = None
239 try:
240 with self._lock:
241 return self._checkout_locked(block, size)
242 except Exception:
243 with self._lock:
244 block.state = _AVAILABLE
245 self._available[block.capacity].append(block)
246 raise
248 def release(self, tensor: Any, event: Optional[Any] = None) -> None:
249 """Return an acquired view to the pool, optionally after an async event."""
250 key = _storage_key(tensor)
251 with self._lock:
252 block = self._blocks.get(key)
253 if block is None:
254 raise ValueError("The tensor does not belong to this PinnedMemoryPool.")
255 if block.state != _IN_USE:
256 raise ValueError("The tensor has already been released to this PinnedMemoryPool.")
257 if event is None:
258 block.state = _AVAILABLE
259 self._available[block.capacity].append(block)
260 else:
261 block.state = _PENDING
262 block.event = event
263 self._pending[block.capacity].append(block)