Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / hyper_offload / execution / warmup / executor.py: 100%

102 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +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"""Warmup-phase executor. 

16 

17During warmup, the executor records the execution trace while applying 

18online greedy eviction to keep device memory within budget. Every 

19input tensor that was evicted in an earlier op is faulted back 

20synchronously (demand-paging) before dispatch. 

21 

22Physical state transitions are delegated to 

23:class:`~offload.runtime.residency.ResidencyManager`. 

24""" 

25 

26from __future__ import annotations 

27 

28import logging 

29from typing import Any 

30 

31import torch 

32from torch.utils._pytree import tree_flatten 

33 

34from hyper_parallel.auto_parallel.hyper_offload.execution.base import BaseExecutor 

35from hyper_parallel.auto_parallel.hyper_offload.runtime.timer import DeviceTimer 

36from hyper_parallel.auto_parallel.hyper_offload.execution.warmup.tracker import ActivationTracker 

37from hyper_parallel.auto_parallel.hyper_offload.ir.replay import OpGuide 

38from hyper_parallel.auto_parallel.hyper_offload.ir.trace import AccessKind, ActivationTrace, StorageAccess, TraceOp 

39from hyper_parallel.auto_parallel.hyper_offload.runtime.bandwidth import profile_transfer_bandwidth 

40from hyper_parallel.auto_parallel.hyper_offload.runtime.residency import ResidencyManager 

41 

42logger = logging.getLogger(__name__) 

43 

44 

45def iter_tensors(value: Any) -> list[torch.Tensor]: 

46 """Return tensor leaves from an arbitrary pytree.""" 

47 leaves, _ = tree_flatten(value) 

48 return [leaf for leaf in leaves if isinstance(leaf, torch.Tensor)] 

49 

50 

51class WarmupExecutor(BaseExecutor): 

52 """Executor for the warmup phase. 

53 

54 Records the execution trace while applying online greedy eviction 

55 to keep device memory within the configured budget. Evicts the 

56 **oldest** activations first when the memory budget is exceeded 

57 (oldest-first within the same op, largest-sized entries are 

58 preferred as tie-breaker to minimise eviction count). 

59 """ 

60 

61 def __init__( 

62 self, 

63 residency_manager: ResidencyManager, 

64 memory_limit_bytes: int, 

65 ) -> None: 

66 super().__init__(residency_manager) 

67 self._memory_limit_bytes = memory_limit_bytes 

68 self._tracker = ActivationTracker() 

69 self._timer = DeviceTimer() 

70 self._guide: list[OpGuide] = [] 

71 self._ops: list[TraceOp] = [] 

72 #: sid -> the op index that first produced this activation. 

73 self._sid_produced_at_op: dict[int, int] = {} 

74 

75 # ------------------------------------------------------------------ 

76 # Eviction policy 

77 # ------------------------------------------------------------------ 

78 

79 def _enforce_budget(self, protected_sids: set[int]) -> None: 

80 """Evict warmup activations until resident bytes fit the configured budget.""" 

81 while self.residency_manager.resident_bytes > self._memory_limit_bytes: 

82 # Greedy: evict oldest first; within the same op, evict largest first. 

83 victim_sid: int | None = None 

84 victim_key: tuple[int, int] | None = None 

85 

86 for sid, produced_at_op in self._sid_produced_at_op.items(): 

87 if sid in protected_sids: 

88 continue 

89 size = self.residency_manager.device_resident_size(sid) 

90 if size is None: 

91 continue 

92 key = (produced_at_op, -size) 

93 if victim_key is None or key < victim_key: 

94 victim_key = key 

95 victim_sid = sid 

96 

97 if victim_sid is None: 

98 raise RuntimeError( 

99 "Warmup memory budget exceeded but no evictable activation found. " 

100 f"resident_bytes={self.residency_manager.resident_bytes}, " 

101 f"limit={self._memory_limit_bytes}, " 

102 f"protected_sids={protected_sids}" 

103 ) 

104 

105 self.residency_manager.copy_d2h(victim_sid) 

106 self.residency_manager.release_device(victim_sid) 

107 

108 # ------------------------------------------------------------------ 

109 # Lifecycle hooks 

110 # ------------------------------------------------------------------ 

111 

112 def on_op_begin(self, func, args, kwargs) -> None: 

113 """Before op: enforce memory budget and fault inputs back to device.""" 

114 super().on_op_begin(func, args, kwargs) 

115 

116 protected_sids: set[int] = set() 

117 for t in iter_tensors((args, kwargs)): 

118 if (sid := self._tracker.get_activation_sid(t)) is not None: 

119 protected_sids.add(sid) 

120 self._enforce_budget(protected_sids) 

121 

122 self._timer.start() 

123 

124 def on_op_end(self, result) -> Any: 

125 """After op: record trace, residency metadata, and return shadowed result.""" 

126 op_duration_ms = self._timer.stop() 

127 

128 func, args, kwargs = self._last_func, self._last_args, self._last_kwargs 

129 

130 self._tracker.register_op_activations(iter_tensors((args, kwargs)), iter_tensors(result)) 

131 

132 op = TraceOp(name=func.__name__, duration_ms=op_duration_ms) 

133 

134 # --- Detect mutated (write) input tensors via func._schema --- 

135 mutated_tensor_ids: set[int] = set() 

136 if hasattr(func, "_schema") and func._schema.is_mutable: # pylint: disable=protected-access 

137 flat_args = iter_tensors((args, {})) 

138 for idx, arg_info in enumerate(func._schema.arguments): # pylint: disable=protected-access 

139 if arg_info.alias_info is not None and arg_info.alias_info.is_write and idx < len(flat_args): # pylint: disable=protected-access 

140 mutated_tensor_ids.add(id(flat_args[idx])) 

141 

142 seen: set[tuple[int, AccessKind]] = set() 

143 

144 # --- Input accesses --- 

145 for t in iter_tensors((args, kwargs)): 

146 sid = self._tracker.get_activation_sid(t) 

147 if sid is None: 

148 continue 

149 is_mutated = id(t) in mutated_tensor_ids 

150 kind = AccessKind.WRITE if is_mutated else AccessKind.READ 

151 if (sid, kind) not in seen: 

152 seen.add((sid, kind)) 

153 op.accesses.append(StorageAccess(self.op_idx, sid, kind)) 

154 

155 # --- Output accesses + bindings --- 

156 leaves, _ = tree_flatten(result) 

157 output_bindings: dict[int, int] = {} 

158 

159 for leaf_index, t in enumerate(leaves): 

160 if not isinstance(t, torch.Tensor): 

161 continue 

162 sid = self._tracker.get_activation_sid(t) 

163 if sid is None: 

164 continue 

165 

166 if (sid, AccessKind.WRITE) not in seen: 

167 seen.add((sid, AccessKind.WRITE)) 

168 op.accesses.append(StorageAccess(self.op_idx, sid, AccessKind.WRITE)) 

169 

170 # Track the op that first produced this activation (used by 

171 # eviction policy: oldest-first). 

172 if sid not in self._sid_produced_at_op: 

173 self._sid_produced_at_op[sid] = self.op_idx 

174 

175 output_bindings[leaf_index] = sid 

176 

177 guide = OpGuide( 

178 name=func.__name__, 

179 output_leaf_count=len(leaves), 

180 output_bindings=output_bindings, 

181 ) 

182 

183 self._ops.append(op) 

184 self._guide.append(guide) 

185 

186 return self.apply_shadows(result, output_bindings) 

187 

188 def finish(self) -> tuple[ActivationTrace, list[OpGuide]]: 

189 """Finish warmup and return recorded trace and replay guide.""" 

190 d2h, h2d = profile_transfer_bandwidth() 

191 

192 trace = ActivationTrace( 

193 ops=list(self._ops), 

194 storage_sizes=self._tracker.storage_sizes, 

195 retained_sids=set(self.retained_sids), 

196 memory_limit_bytes=self._memory_limit_bytes, 

197 d2h_bandwidth_gbps=d2h, 

198 h2d_bandwidth_gbps=h2d, 

199 ) 

200 guide = self._guide 

201 

202 self.reset() 

203 return trace, guide 

204 

205 def reset(self) -> None: 

206 """Reset.""" 

207 self._sid_produced_at_op.clear() 

208 self._ops = [] 

209 self._guide = [] 

210 self._tracker.clear_activations() 

211 super().reset()