Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / recompute / profile.py: 100%

44 statements  

« 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"""What each recompute switch of a layer kind saves and costs, measured alone. 

16 

17A selective layer's memory and backward time add up over its switches. Each 

18switch gates its own activation term in the memory model and its own op in 

19the recompute estimate, and only ``gather`` acts on the communication 

20buffers, both on the memory kept and on the volume sent again. So a layer that 

21recomputes a set of ops costs the plain layer plus what each of those ops 

22costs alone, and a :class:`SwitchProfile`, nine measurements, prices all 128 

23settings of the seven switches; an MLA layer's, a tenth, all 256 with the 

24switch of its up-projections. 

25 

26A census of a layer kind (IR phase 5) prices its plain layer, and a layer 

27running HyperParallel's selective policy, by what it measured, and every 

28other setting by what its records per op state for the ops the setting 

29keeps, or by the formulas where it states none. The settings then add up 

30from the layer selective with every op kept, and the policy's is measured 

31whole. Where the census prices them per op, a setting's working set as 

32warm-up ends is clamped where the layer keeps more than its backward holds, 

33so it adds up before the clamp, which :meth:`SwitchProfile.selective` 

34applies to each setting. 

35""" 

36from dataclasses import dataclass, field, replace 

37from typing import FrozenSet, Iterable, Mapping, Optional, Tuple 

38 

39# The recompute switches; 1 keeps an op's activation, 0 recomputes the op. 

40SWITCHES = ("attBMM", "headCast", "dropout", "softmax", "normOp", "gather", "ffAct", "attUp") 

41# The switches a layer kind may keep nothing under, an MLA layer's 

42# up-projections: a front weighs them only where they change what the kind 

43# costs, so that every other kind's settings stay the seven switches'. 

44OPTIONAL = ("attUp",) 

45 

46 

47@dataclass(frozen=True) 

48class Cost: 

49 """What running a layer one way costs. 

50 

51 Attributes: 

52 memory_per_micro_batch: Bytes kept for each micro-batch in flight: the 

53 activations, and the communication buffers that grow with the 

54 micro-batches in flight. 

55 memory_once: Bytes of communication buffers kept once, however many 

56 micro-batches are in flight. 

57 backward_time: The backward time, recompute included. 

58 excess: At each of its profile's :attr:`~SwitchProfile.counts`, the 

59 bytes the two memories charge beyond what the layer keeps with 

60 that many micro-batches in flight. The split is exact at one 

61 micro-batch and at the most any stage keeps, and a buffer that 

62 does not grow can hide one that does in between. 

63 working: What the working set of the layer's backward holds beyond 

64 what the layer keeps at one micro-batch, as the memory model 

65 charges it to the layer that ends warm-up: under FSDP that 

66 reshards, two layers' gathered parameters, its own and the 

67 next's, in the buffers the gathers take, and where a census 

68 prices the layer, what its backward holds beyond what the stage 

69 keeps for it already. For full recompute, beyond what the plain 

70 layer keeps: its backward runs the plain layer, on activations 

71 the stage does not keep. 

72 first_working: The same for the backward a stage runs last, its 

73 first layer's, as a micro-batch's backward ends: one layer's 

74 gathered parameters, with none left to prefetch, and all a 

75 census says the backward holds, as the stage keeps no 

76 activations of that micro-batch for it. 

77 census_kept: Where a census's records per op price the layer, what 

78 it keeps of them at one micro-batch; *working* is then stated 

79 before the clamp (:attr:`SwitchProfile.census_held`). 

80 """ 

81 

82 memory_per_micro_batch: float 

83 memory_once: float 

84 backward_time: float 

85 excess: Tuple[float, ...] = () 

86 working: float = 0.0 

87 first_working: float = 0.0 

88 census_kept: float = 0.0 

89 

90 def __add__(self, other: "Cost") -> "Cost": 

91 """The sum, cost by cost.""" 

92 return Cost( 

93 *(mine + theirs for mine, theirs in zip(self.values(), other.values())), 

94 excess=tuple(mine + theirs for mine, theirs in zip(self.excess, other.excess)), 

95 working=self.working + other.working, 

96 first_working=self.first_working + other.first_working, 

97 census_kept=self.census_kept + other.census_kept, 

98 ) 

99 

100 def __sub__(self, other: "Cost") -> "Cost": 

101 """The difference, cost by cost.""" 

102 return Cost( 

103 *(mine - theirs for mine, theirs in zip(self.values(), other.values())), 

104 excess=tuple(mine - theirs for mine, theirs in zip(self.excess, other.excess)), 

105 working=self.working - other.working, 

106 first_working=self.first_working - other.first_working, 

107 census_kept=self.census_kept - other.census_kept, 

108 ) 

109 

110 def values(self) -> Tuple[float, float, float]: 

111 """``(memory per micro-batch, memory once, backward time)``.""" 

112 return self.memory_per_micro_batch, self.memory_once, self.backward_time 

113 

114 

115@dataclass(frozen=True) 

116class SwitchProfile: 

117 """A layer kind's costs: plain, with each op alone recomputed, and fully recomputed. 

118 

119 Attributes: 

120 forward_time: The layer's forward time, whatever it recomputes. 

121 plain: The layer keeping every op. 

122 alone: Per switch, the layer recomputing that op alone. 

123 full: The layer fully recomputed. 

124 counts: The counts of micro-batches in flight, between one and the 

125 most any stage keeps, at which each cost states its excess. 

126 selective_base: The layer selective with every op kept, which the 

127 settings add up from where a census prices the plain layer and 

128 not a selective one; the plain layer when omitted. 

129 whole: The settings measured whole, which do not add up over their 

130 switches: HyperParallel's selective policy, where a census 

131 prices it. 

132 census_held: Where a census's records per op price the selective 

133 settings, what the layer's backward holds at one micro-batch: 

134 as warm-up ends, it holds that beyond what the layer keeps, and 

135 no less than nothing. 

136 """ 

137 

138 forward_time: float 

139 plain: Cost 

140 alone: Mapping[str, Cost] 

141 full: Cost 

142 counts: Tuple[int, ...] = () 

143 selective_base: Optional[Cost] = None 

144 whole: Mapping[FrozenSet[str], Cost] = field(default_factory=dict) 

145 census_held: Optional[float] = None 

146 

147 def acting(self) -> Tuple[str, ...]: 

148 """The switches a setting is made of: every one but an optional one that changes nothing.""" 

149 base = self.plain if self.selective_base is None else self.selective_base 

150 return tuple(name for name in SWITCHES 

151 if name not in OPTIONAL or (name in self.alone and self.alone[name] != base)) 

152 

153 def selective(self, recompute: Iterable[str]) -> Cost: 

154 """The cost of recomputing the ops *recompute* names. 

155 

156 The plain layer's for none, a setting's own where it is measured 

157 whole, and otherwise the base's, plus what each op costs alone 

158 beyond it; where a census's records per op price it, its working 

159 set as warm-up ends clamped as the memory model clamps it. 

160 """ 

161 chosen = frozenset(recompute) 

162 if chosen in self.whole: 

163 return self.whole[chosen] 

164 base = self.plain if self.selective_base is None or not chosen else self.selective_base 

165 cost = base 

166 for name in SWITCHES: 

167 if name in chosen: 

168 cost = cost + (self.alone[name] - base) 

169 if chosen and self.census_held is not None: 

170 cost = replace(cost, working=cost.working + max(0.0, cost.census_kept - self.census_held)) 

171 return cost