Coverage for cuda/core/checkpoint.py: 48.28%

116 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-16 01:23 +0000

1# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5"""CUDA process checkpointing on Linux. 

6 

7This module requires the CUDA 13 build of cuda.core, because the driver 

8structures it uses are CUDA 13 types. It also requires a CUDA driver of version 

912.8 or newer with checkpoint API support. 

10""" 

11 

12import ctypes as _ctypes 

13from collections.abc import Mapping 

14from typing import Any 

15 

16from cuda.bindings import driver as _driver 

17from cuda.core._utils.cuda_utils import handle_return as _handle_cuda_return 

18from cuda.core._utils.version import BUILD_CUDA_MAJOR as _BUILD_CUDA_MAJOR 

19from cuda.core._utils.version import driver_version as _driver_version 

20from cuda.core.typing import ProcessStateType as _ProcessStateType 

21 

22_PROCESS_STATE_NAME_ATTRS: tuple[tuple[str, _ProcessStateType], ...] = ( 

23 ("CU_PROCESS_STATE_RUNNING", "running"), 

24 ("CU_PROCESS_STATE_LOCKED", "locked"), 

25 ("CU_PROCESS_STATE_CHECKPOINTED", "checkpointed"), 

26 ("CU_PROCESS_STATE_FAILED", "failed"), 

27) 

28 

29_REQUIRED_DRIVER_VERSION = (12, 8, 0) 

30_driver_capability_checked = False 

31 

32 

33class Process: 

34 """ 

35 CUDA process that can be locked, checkpointed, restored, and unlocked. 

36 

37 Parameters 

38 ---------- 

39 pid : int 

40 Process ID of the CUDA process. 

41 """ 

42 

43 __slots__ = ("_pid",) 

44 

45 def __init__(self, pid: int): 

46 self._pid = _check_pid(pid) 1jopmn

47 

48 @property 

49 def pid(self) -> int: 

50 """ 

51 Process ID of the CUDA process. 

52 """ 

53 return self._pid 1j

54 

55 @property 

56 def state(self) -> _ProcessStateType: 

57 """ 

58 CUDA checkpoint state for this process. 

59 """ 

60 driver = _get_driver() 

61 state = _call_driver(driver, driver.cuCheckpointProcessGetState, self._pid) 

62 state_names = _get_process_state_names(driver) 

63 try: 

64 return state_names[state] 

65 except KeyError as e: 

66 state_value = int(state) 

67 raise RuntimeError(f"Unknown CUDA checkpoint process state: {state_value}") from e 

68 

69 @property 

70 def restore_thread_id(self) -> int: 

71 """ 

72 CUDA restore thread ID for this process. 

73 """ 

74 driver = _get_driver() 

75 return int(_call_driver(driver, driver.cuCheckpointProcessGetRestoreThreadId, self._pid)) 

76 

77 def lock(self, timeout_ms: int = 0) -> None: 

78 """ 

79 Lock this process, blocking further CUDA API calls. 

80 

81 Parameters 

82 ---------- 

83 timeout_ms : int, optional 

84 Timeout in milliseconds. A value of 0 indicates no timeout. 

85 """ 

86 driver = _get_driver() 

87 args = driver.CUcheckpointLockArgs() 

88 args.timeoutMs = _check_timeout_ms(timeout_ms) 

89 _call_driver(driver, driver.cuCheckpointProcessLock, self._pid, args) 

90 

91 def checkpoint(self) -> None: 

92 """ 

93 Checkpoint the GPU memory contents of this locked process. 

94 """ 

95 driver = _get_driver() 

96 _call_driver(driver, driver.cuCheckpointProcessCheckpoint, self._pid, None) 

97 

98 def restore(self, gpu_mapping: Mapping[Any, Any] | None = None) -> None: 

99 """ 

100 Restore this checkpointed process. 

101 

102 Parameters 

103 ---------- 

104 gpu_mapping : mapping, optional 

105 GPU UUID remapping from each checkpointed GPU UUID to the GPU UUID 

106 to restore onto. For migration workflows, provide mappings for 

107 every GPU visible to the kernel-mode driver. User-space masking 

108 such as ``CUDA_VISIBLE_DEVICES`` does not reduce this mapping 

109 requirement. 

110 """ 

111 driver = _get_driver() 

112 args = _make_restore_args(driver, gpu_mapping) 

113 _call_driver(driver, driver.cuCheckpointProcessRestore, self._pid, args) 

114 

115 def unlock(self) -> None: 

116 """ 

117 Unlock this locked process so it can resume CUDA API calls. 

118 """ 

119 driver = _get_driver() 

120 _call_driver(driver, driver.cuCheckpointProcessUnlock, self._pid, None) 

121 

122 

123def _get_driver() -> Any: 

124 global _driver_capability_checked 

125 if _driver_capability_checked: 

126 return _driver 

127 

128 # A restore onto other GPUs uses CUcheckpointGpuPair, a CUDA 13 type that 

129 # cuda-bindings 12.x does not have. 

130 if _BUILD_CUDA_MAJOR < 13: 

131 raise RuntimeError("CUDA checkpointing requires the CUDA 13 build of cuda.core. Install cuda-core[cu13].") 

132 

133 driver_ver = _driver_version() 

134 if driver_ver < _REQUIRED_DRIVER_VERSION: 

135 raise RuntimeError( 

136 "CUDA checkpointing is not supported by the installed NVIDIA driver. " 

137 "Upgrade to a driver version with CUDA checkpoint API support." 

138 ) 

139 

140 _driver_capability_checked = True 

141 return _driver 

142 

143 

144def _get_process_state_names(driver: Any) -> dict[Any, _ProcessStateType]: 

145 return {getattr(driver.CUprocessState, attr): state_name for attr, state_name in _PROCESS_STATE_NAME_ATTRS} 

146 

147 

148def _call_driver(driver: Any, func: Any, *args: Any) -> Any: 

149 try: 1klef

150 result = func(*args) 1klef

151 except RuntimeError as e: 1kl

152 if "cuCheckpointProcess" in str(e) and "not found" in str(e): 1kl

153 raise RuntimeError( 1l

154 "CUDA checkpointing is not supported by the installed NVIDIA driver. " 

155 "Upgrade to a driver version with CUDA checkpoint API support." 

156 ) from e 

157 raise 1k

158 

159 err = result[0] 1ef

160 not_supported_errors = ( 1ef

161 getattr(driver.CUresult, "CUDA_ERROR_NOT_FOUND", None), 

162 getattr(driver.CUresult, "CUDA_ERROR_NOT_SUPPORTED", None), 

163 ) 

164 if err in not_supported_errors: 1ef

165 raise RuntimeError( 1ef

166 "CUDA checkpointing is not supported by the installed NVIDIA driver. " 

167 "Upgrade to a driver version with CUDA checkpoint API support." 

168 ) 

169 

170 return _handle_cuda_return(result) 

171 

172 

173def _check_pid(pid: int) -> int: 

174 if isinstance(pid, bool) or not isinstance(pid, int): 1jopmn

175 raise TypeError("pid must be an int") 1op

176 if pid <= 0: 1jmn

177 raise ValueError("pid must be a positive int") 1mn

178 return pid 1j

179 

180 

181def _check_timeout_ms(timeout_ms: int) -> int: 

182 if isinstance(timeout_ms, bool) or not isinstance(timeout_ms, int): 1tuqv

183 raise TypeError("timeout_ms must be an int") 1tuv

184 if timeout_ms < 0: 1q

185 raise ValueError("timeout_ms must be >= 0") 1q

186 return timeout_ms 

187 

188 

189def _make_restore_args(driver: Any, gpu_mapping: Mapping[Any, Any] | None) -> Any: 

190 if gpu_mapping is None: 1agr

191 return None 

192 if not isinstance(gpu_mapping, Mapping): 1agr

193 raise TypeError("gpu_mapping must be a mapping from checkpointed GPU UUID to restore GPU UUID") 1r

194 

195 pairs = [] 1ag

196 for old_uuid, new_uuid in gpu_mapping.items(): 1ag

197 pair = driver.CUcheckpointGpuPair() 1a

198 buffers: list[Any] = [] # holds ctypes string-buffer keepalives for the call below 1a

199 pair.oldUuid = _as_cuuuid(driver, old_uuid, buffers) 1a

200 pair.newUuid = _as_cuuuid(driver, new_uuid, buffers) 1a

201 pairs.append(pair) 1a

202 

203 if not pairs: 1ag

204 return None 1g

205 

206 args = driver.CUcheckpointRestoreArgs() 1a

207 args.gpuPairs = pairs 1a

208 args.gpuPairsCount = len(pairs) 1a

209 return args 1a

210 

211 

212def _as_cuuuid(driver: Any, value: Any, buffers: list[Any]) -> Any: 

213 """Convert *value* to a ``CUuuid``. 

214 

215 Accepts a ``CUuuid`` instance (returned as-is) or a UUID string in 

216 the ``"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`` format returned by 

217 :attr:`Device.uuid`. 

218 """ 

219 if isinstance(value, driver.CUuuid): 1bcwhisa

220 return value 1w

221 if isinstance(value, str): 1bchisa

222 try: 1bchia

223 raw = bytes.fromhex(value.replace("-", "")) 1bchia

224 except ValueError: 1i

225 raise ValueError( 1i

226 f"GPU UUID string must be 32 hex characters (with optional hyphens), got {value!r}" 

227 ) from None 

228 if len(raw) != 16: 1bcha

229 raise ValueError(f"GPU UUID string must be 32 hex characters (with optional hyphens), got {value!r}") 1h

230 buf = _ctypes.create_string_buffer(raw, 16) 1bca

231 buffers.append(buf) 1bca

232 return driver.CUuuid(_ctypes.addressof(buf)) 1bca

233 raise TypeError("GPU UUID values must be CUDA UUID objects or UUID strings") 1s

234 

235 

236__all__ = [ 

237 "Process", 

238]