4040# cuMemAddressReserve wants a power-of-two alignment and a size that is a
4141# multiple of it. 2 MiB is the granularity the driver uses for pools.
4242VA_ALIGNMENT = 2 * MIB
43- # Above any plausible per-process budget, so the descending probe below always
44- # starts from a size that fails.
45- MAX_PROBE_BYTES = 1 << 46
4643
4744# Each driver-managed pool reserves about this multiple of installed device
4845# memory. Used to express remaining headroom in units of "one more pool".
4946POOL_RESERVATION_MULTIPLE = 2
5047
48+ # The probe only has to answer "how much room is left relative to one pool", so
49+ # it never asks for more than this many pools' worth. An absolute ceiling is the
50+ # wrong shape: 64 TiB, the previous value, is meaningless on a machine with room
51+ # to spare and hangs under WSL, where the ask crosses into the host GPU driver.
52+ PROBE_CEILING_POOLS = 4
53+ # Used when the device memory size cannot be read.
54+ DEFAULT_PROBE_CEILING_BYTES = 64 * 1024 * MIB
55+
5156
5257def align_up (size : int ) -> int :
5358 """Round to a multiple of the reservation alignment.
@@ -76,8 +81,36 @@ def _reserve_and_release(size: int) -> bool:
7681 return True
7782
7883
79- def largest_reservable (reserve = None , max_bytes : int = MAX_PROBE_BYTES , refine_steps : int = 4 ) -> int :
80- """Largest contiguous reservation the driver still grants.
84+ def pool_bytes_for (device_memory : int | None ) -> int | None :
85+ """Address space one driver-managed pool reserves, measured at 2x device memory."""
86+ if device_memory is None :
87+ return None
88+ return align_up (POOL_RESERVATION_MULTIPLE * device_memory )
89+
90+
91+ def probe_ceiling (device_memory : int | None ) -> int :
92+ """Largest size the probe is willing to ask for.
93+
94+ Expressed in pools rather than absolute bytes: past a few pools' worth the
95+ exact figure tells nobody anything, and asking for an enormous range is
96+ where the cost and the WSL hang live.
97+
98+ Built by multiplying the *rounded* pool size rather than rounding a multiple
99+ of device memory, so that the ceiling is a whole number of pools. Rounding
100+ each independently leaves the ceiling a hair under, and the headroom count
101+ then reports one pool fewer than it should.
102+ """
103+ pool = pool_bytes_for (device_memory )
104+ if pool is None :
105+ return DEFAULT_PROBE_CEILING_BYTES
106+ return PROBE_CEILING_POOLS * pool
107+
108+
109+ def largest_reservable (max_bytes : int , * , reserve = None , refine_steps : int = 4 ) -> int :
110+ """Largest contiguous reservation the driver still grants, up to ``max_bytes``.
111+
112+ Returns ``max_bytes`` when even that is granted, so the result is a lower
113+ bound rather than the true maximum -- which is the point, see probe_ceiling.
81114
82115 Halves down from ``max_bytes`` rather than doubling up from the granularity,
83116 because a *refused* reservation allocates nothing and returns immediately
@@ -94,15 +127,20 @@ def largest_reservable(reserve=None, max_bytes: int = MAX_PROBE_BYTES, refine_st
94127 """
95128 reserve = _reserve_and_release if reserve is None else reserve
96129
97- size = max_bytes
130+ # Halving has to re-align: unlike the old power-of-two ceiling, a ceiling
131+ # derived from device memory goes odd after a couple of steps, and an
132+ # unaligned ask is refused outright -- which would read as no space left.
133+ ceiling = align_up (max_bytes )
134+ size = ceiling
135+ refused = None
98136 while size >= VA_ALIGNMENT and not reserve (size ):
99- size //= 2
137+ size , refused = ( size // 2 // VA_ALIGNMENT ) * VA_ALIGNMENT , size
100138 if size < VA_ALIGNMENT :
101139 return 0
102- if size == max_bytes :
103- return size # nothing was refused , so there is no bracket to narrow
140+ if refused is None :
141+ return ceiling # granted at the ceiling , so the answer is a lower bound
104142
105- low , high = size , size * 2 # high was refused on the way down
143+ low , high = size , refused
106144 for _ in range (refine_steps ):
107145 middle = ((low + high ) // 2 // VA_ALIGNMENT ) * VA_ALIGNMENT
108146 if middle <= low or middle >= high :
@@ -211,7 +249,16 @@ class ReservationReport:
211249 """What the early reservations cost, for the terminal."""
212250
213251 def __init__ (
214- self , device_name , device_memory , before , after , reservations , measured , seconds = 0.0 , unsupported = False
252+ self ,
253+ device_name ,
254+ device_memory ,
255+ before ,
256+ after ,
257+ reservations ,
258+ measured ,
259+ seconds = 0.0 ,
260+ unsupported = False ,
261+ ceiling = None ,
215262 ):
216263 self .device_name = device_name
217264 self .device_memory = device_memory
@@ -221,24 +268,29 @@ def __init__(
221268 self .measured = measured
222269 self .seconds = seconds
223270 self .unsupported = unsupported
271+ self .ceiling = ceiling
272+
273+ def _measured_range (self , value ) -> str :
274+ """Render a probe result, flagging it as a lower bound when capped."""
275+ if value is not None and self .ceiling is not None and value >= self .ceiling :
276+ return f">= { format_bytes (value )} "
277+ return format_bytes (value )
224278
225279 @property
226280 def failed (self ) -> list [Reservation ]:
227281 return [item for item in self .reservations if not item .succeeded ]
228282
229283 @property
230284 def pool_reservation_bytes (self ) -> int | None :
231- if self .device_memory is None :
232- return None
233- return align_up (POOL_RESERVATION_MULTIPLE * self .device_memory )
285+ return pool_bytes_for (self .device_memory )
234286
235287 def lines (self ) -> list [str ]:
236288 out = [f"device 0: { self .device_name } ({ format_bytes (self .device_memory )} device memory)" ]
237289 if self .unsupported :
238290 out .append ("device does not support memory pools; nothing to reserve" )
239291 return out
240292 if self .measured :
241- out .append (f"largest reservable range before: { format_bytes (self .before )} " )
293+ out .append (f"largest reservable range before: { self . _measured_range (self .before )} " )
242294 else :
243295 out .append ("largest reservable range: not measured (no virtual memory management support)" )
244296
@@ -247,15 +299,13 @@ def lines(self) -> list[str]:
247299 out .append (f" { item .name :<24} { item .detail :<24} { status } " )
248300
249301 if self .measured :
250- # A drop here is a *lower* bound on what was taken: the driver may
251- # carve its reservations out of a region other than the largest
252- # hole, in which case the largest hole does not move at all.
253- change = None if self .before is None or self .after is None else self .before - self .after
254- out .append (f"largest reservable range after: { format_bytes (self .after )} " )
302+ out .append (f"largest reservable range after: { self ._measured_range (self .after )} " )
255303 pool_bytes = self .pool_reservation_bytes
256304 if pool_bytes and self .after is not None :
305+ capped = self .ceiling is not None and self .after >= self .ceiling
306+ count = f"{ self .after // pool_bytes } { '+' if capped else '' } "
257307 out .append (
258- f"remaining headroom: { self . after // pool_bytes } more pool-sized "
308+ f"remaining headroom: { count } more pool-sized "
259309 f"({ format_bytes (pool_bytes )} ) reservations [{ self .seconds :.1f} s measuring]"
260310 )
261311 return out
@@ -273,7 +323,7 @@ def build_failure_message(report: ReservationReport) -> str:
273323 f" device 0 { report .device_name } " ,
274324 f" installed device memory { format_bytes (report .device_memory )} " ,
275325 f" needed per driver-managed pool { format_bytes (pool_bytes )} of *virtual address space*" ,
276- f" largest range still available { format_bytes (report .after if report .measured else None ) } " ,
326+ f" largest range still available { report . _measured_range (report .after ) if report .measured else 'unknown' } " ,
277327 "" ,
278328 ]
279329 for item in report .failed :
@@ -298,15 +348,18 @@ def reserve_driver_pools(device, measure: bool = True) -> ReservationReport:
298348 return ReservationReport (device .name , device_memory , None , None , [], measured = False , unsupported = True )
299349
300350 measured = measure and vmm_supported (device .device_id )
351+ ceiling = probe_ceiling (device_memory )
301352 started = time .perf_counter ()
302- before = largest_reservable () if measured else None
353+ before = largest_reservable (ceiling ) if measured else None
303354 elapsed = time .perf_counter () - started
304355
305356 reservations = reservations_for (device )
306357 for item in reservations :
307358 item .run ()
308359
309360 started = time .perf_counter ()
310- after = largest_reservable () if measured else None
361+ after = largest_reservable (ceiling ) if measured else None
311362 elapsed += time .perf_counter () - started
312- return ReservationReport (device .name , device_memory , before , after , reservations , measured , elapsed )
363+ return ReservationReport (
364+ device .name , device_memory , before , after , reservations , measured , elapsed , ceiling = ceiling
365+ )
0 commit comments