Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / trainer / runtime / data_iterator.py: 80%

107 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2025-2026 Bytedance Ltd. and/or its affiliates 

2# Copyright 2026 Huawei Technologies Co., Ltd 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); 

5# you may not use this file except in compliance with the License. 

6# You may obtain a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, 

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15# ============================================================================ 

16 

17"""Trainer-side dataloader iteration helpers. 

18 

19``BackgroundPrefetcher`` and ``HyperIter`` are split out of the former 

20``auto_models/trainer/base.py`` in stage 7 (05 §15.11 step 3); class names and 

21checkpointing semantics are retained. 

22""" 

23 

24__all__ = [ 

25 "BackgroundPrefetcher", 

26 "HyperIter", 

27] 

28 

29import logging 

30import queue 

31import threading 

32from typing import Any, Dict, Optional 

33 

34 

35logger = logging.getLogger(__name__) 

36 

37 

38class BackgroundPrefetcher: 

39 """ 

40 Prefetches batches from a dataloader in a background thread to overlap data loading 

41 with GPU computation. Synchronizes dataloader state for correct checkpointing. 

42 """ 

43 

44 def __init__(self, dataloader: Any, maxsize: int = 1) -> None: 

45 """Start the background worker prefetching from ``dataloader``.""" 

46 self.dataloader = dataloader 

47 self.iterator = iter(dataloader) 

48 self.queue = queue.Queue(maxsize=maxsize) 

49 self.stop_event = threading.Event() 

50 self.original_state_dict = getattr(dataloader, "state_dict", None) 

51 self.current_state = None 

52 self.thread = threading.Thread(target=self._worker) 

53 self.thread.daemon = True 

54 self.thread.start() 

55 

56 def _worker(self) -> None: 

57 """Prefetch data and capture dataloader state in a background thread.""" 

58 try: 

59 while not self.stop_event.is_set(): 

60 try: 

61 item = next(self.iterator) 

62 except StopIteration: 

63 self._put_result((StopIteration, None)) 

64 break 

65 

66 # A stop request cannot interrupt next(), so discard the item once 

67 # that call returns instead of retaining another batch and state. 

68 if self.stop_event.is_set(): 

69 break 

70 

71 # Ensure we capture the state so that subsequent dataloader advances 

72 # don't mutate the captured state in-place. The underlying dataloader's 

73 # state_dict() should handle deepcopying if necessary. 

74 state = self.original_state_dict() if self.original_state_dict else None 

75 if not self._put_result((item, state)): 

76 break 

77 # The worker must transfer any producer failure back to the training thread. 

78 except Exception as exc: # pylint: disable=broad-exception-caught 

79 self._put_result((exc, None)) 

80 finally: 

81 # A timed-out stop cannot cancel next(), so the worker must release 

82 # its own references when that call eventually returns. 

83 if self.stop_event.is_set(): 

84 self._cleanup_after_stop() 

85 

86 def _put_result(self, result: tuple[Any, Any]) -> bool: 

87 """Put a worker result while allowing a stop request to cancel the write.""" 

88 while not self.stop_event.is_set(): 

89 try: 

90 self.queue.put(result, timeout=0.1) 

91 return True 

92 except queue.Full: 

93 continue 

94 return False 

95 

96 def _drain_queue(self) -> None: 

97 """Release every result currently retained by the prefetch queue.""" 

98 while True: 

99 try: 

100 self.queue.get_nowait() 

101 except queue.Empty: 

102 return 

103 

104 def _cleanup_after_stop(self) -> None: 

105 """Release queued results and live dataloader references after stopping.""" 

106 self._drain_queue() 

107 self.iterator = None 

108 self.dataloader = None 

109 self.original_state_dict = None 

110 # The final checkpoint may be collected after stop(), so keep the 

111 # consumed-batch snapshot while releasing live dataloader references. 

112 

113 def __iter__(self) -> "BackgroundPrefetcher": 

114 """Return this prefetcher as its own iterator.""" 

115 return self 

116 

117 def __next__(self) -> Any: 

118 """Return the next prefetched item, re-raising worker failures.""" 

119 res = self.queue.get() 

120 if isinstance(res, tuple) and len(res) == 2: 

121 item, state = res 

122 if item is StopIteration: 

123 raise StopIteration 

124 if isinstance(item, Exception): 

125 raise item 

126 self.current_state = state 

127 return item 

128 if res is StopIteration: 

129 raise StopIteration 

130 if isinstance(res, Exception): 

131 raise res 

132 return res 

133 

134 def state_dict(self) -> Dict[str, Any]: 

135 """Return the dataloader state captured alongside the current item.""" 

136 if self.current_state is not None: 

137 return self.current_state 

138 if self.original_state_dict: 

139 return self.original_state_dict() 

140 return {} 

141 

142 def stop(self, timeout: Optional[float] = 5.0) -> bool: 

143 """Stop the background worker and release its dataloader references. 

144 

145 Args: 

146 timeout: Maximum seconds to wait. ``None`` waits until the worker exits. 

147 

148 Returns: 

149 Whether the worker terminated within the requested timeout. 

150 """ 

151 self.stop_event.set() 

152 self._drain_queue() 

153 if self.thread.is_alive(): 

154 self.thread.join(timeout=timeout) 

155 if self.thread.is_alive(): 

156 logger.warning("BackgroundPrefetcher worker thread did not terminate within timeout.") 

157 return False 

158 

159 self._cleanup_after_stop() 

160 return True 

161 

162 

163class HyperIter: 

164 """ 

165 A unified iterator wrapper that handles both standard iteration and background prefetching. 

166 """ 

167 

168 def __init__(self, dataloader: Any, use_background_prefetcher: bool = False, maxsize: int = 1) -> None: 

169 """Wrap ``dataloader`` in either direct iteration or background prefetching.""" 

170 self.dataloader = dataloader 

171 self.use_background_prefetcher = use_background_prefetcher 

172 if use_background_prefetcher: 

173 self.iterator = BackgroundPrefetcher(dataloader, maxsize=maxsize) 

174 else: 

175 self.iterator = iter(dataloader) 

176 

177 def __iter__(self) -> "HyperIter": 

178 """Return this wrapper as its own iterator.""" 

179 return self 

180 

181 def __next__(self) -> Any: 

182 """Return the next batch from the underlying iterator.""" 

183 return next(self.iterator) 

184 

185 def stop(self, timeout: Optional[float] = 5.0) -> bool: 

186 """Stop the background prefetch worker when one is active. 

187 

188 Args: 

189 timeout: Maximum seconds to wait. ``None`` waits until the worker exits. 

190 

191 Returns: 

192 Whether the worker is stopped, or ``True`` when prefetching is disabled. 

193 """ 

194 if self.use_background_prefetcher and hasattr(self.iterator, "stop"): 

195 return self.iterator.stop(timeout=timeout) 

196 return True 

197 

198 def state_dict(self) -> Dict[str, Any]: 

199 """Return the underlying dataloader or prefetcher state for checkpointing.""" 

200 if self.use_background_prefetcher and hasattr(self.iterator, "state_dict"): 

201 return self.iterator.state_dict() 

202 if hasattr(self.dataloader, "state_dict"): 

203 return self.dataloader.state_dict() 

204 return {}