Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed_data / cost_model.py: 71%
129 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""Replaceable workload estimates, independent of packing hard limits."""
17from __future__ import annotations
19import math
20from collections.abc import Mapping
21from dataclasses import dataclass
22from typing import Any, Protocol
24from hyper_parallel.distributed_data.schema import SampleMetadata, WorkloadCost
27class CostModel(Protocol):
28 """Deterministic, stateless CPU estimate evaluated on the centralized planner.
30 Implementations consume lightweight ``metadata.features``, not image tensors.
31 They must not mutate metadata, perform distributed collectives, or change
32 ``pack_tokens`` / ``packing_costs``. Return components in comparable units.
33 Models may also expose ``padding_cost(padding_tokens) -> WorkloadCost`` for
34 constructor padding. The planner and built-in LPT policies include it in
35 each pack's layout-dependent cost; custom policies still receive source costs.
36 """
38 def __call__(self, metadata: SampleMetadata) -> WorkloadCost:
39 """Estimate one selected sample's additive workload."""
42class CausalAttentionCostModel:
43 """Normalized causal-attention square cost for one independent text document.
45 Costs use token-squared units, omitting common model coefficients and all
46 linear terms. The constructor's independent PAD sequence contributes its
47 own squared length through ``padding_cost`` after each candidate placement.
48 """
50 def __call__(self, metadata: SampleMetadata) -> WorkloadCost:
51 """Return L squared without counting ignored targets as padding."""
52 if metadata.features.get("D", 0):
53 raise ValueError("CausalAttentionCostModel requires causal text documents without diffusion tokens")
54 return WorkloadCost(llm=metadata.pack_tokens ** 2)
56 @staticmethod
57 def padding_cost(padding_tokens: int) -> WorkloadCost:
58 """Account for the independent PAD sequence that Attention still computes.
60 Args:
61 padding_tokens: Number of tokens appended to one packed row.
63 Returns:
64 Normalized causal Attention square cost of that PAD sequence.
65 """
66 return WorkloadCost(llm=padding_tokens ** 2)
69def _configuration_dict(config: Any) -> dict[str, Any]:
70 """Accept an ordinary dictionary or a configuration with ``to_dict``."""
71 if isinstance(config, Mapping):
72 return dict(config)
73 if callable(getattr(config, "to_dict", None)):
74 value = config.to_dict()
75 if isinstance(value, Mapping):
76 return dict(value)
77 raise ValueError("model_config must be a mapping or a configuration exposing to_dict().")
80def _integer(value: Any, name: str, minimum: int = 1) -> int:
81 if not isinstance(value, int) or isinstance(value, bool) or value < minimum:
82 raise ValueError(f"{name} must be an integer >= {minimum}, but got {value!r}.")
83 return value
86@dataclass(frozen=True)
87class BackboneFlopsConfig:
88 """Latent-attention decoder dimensions, independent of model libraries.
90 Only the shared backbone decoder layers are modeled. Diffusion input/output
91 stacks, the language-model head, ViT, VAE, and their projectors are excluded.
92 ``mlp_layer_types`` follows the actual layer order: ``dense`` or ``sparse``.
93 """
95 hidden_size: int
96 num_hidden_layers: int
97 num_attention_heads: int
98 intermediate_size: int
99 mlp_layer_types: tuple[str, ...]
100 kv_lora_rank: int
101 q_lora_rank: int | None
102 qk_nope_head_dim: int
103 qk_rope_head_dim: int
104 v_head_dim: int
105 moe_intermediate_size: int = 0
106 n_routed_experts: int = 0
107 num_experts_per_tok: int = 0
108 n_shared_experts: int = 0
109 attn_block_mode: str = "tail"
110 diff_time_embed_mode: str = "add"
112 def __post_init__(self) -> None:
113 """Reject dimensions or layer patterns not covered by this estimate."""
114 for name in (
115 "hidden_size", "num_hidden_layers", "num_attention_heads", "intermediate_size",
116 "kv_lora_rank", "v_head_dim",
117 ):
118 _integer(getattr(self, name), name)
119 for name in ("qk_nope_head_dim", "qk_rope_head_dim", "n_shared_experts"):
120 _integer(getattr(self, name), name, 0)
121 if self.q_lora_rank is not None:
122 _integer(self.q_lora_rank, "q_lora_rank")
123 if self.qk_nope_head_dim + self.qk_rope_head_dim == 0:
124 raise ValueError("The combined query/key head dimension must be positive.")
125 if (
126 not isinstance(self.mlp_layer_types, tuple)
127 or len(self.mlp_layer_types) != self.num_hidden_layers
128 or any(kind not in ("dense", "sparse") for kind in self.mlp_layer_types)
129 ):
130 raise ValueError("mlp_layer_types must contain one 'dense' or 'sparse' entry per backbone layer.")
131 for name in ("moe_intermediate_size", "n_routed_experts", "num_experts_per_tok"):
132 _integer(getattr(self, name), name, int("sparse" in self.mlp_layer_types))
133 if self.num_experts_per_tok > self.n_routed_experts:
134 raise ValueError("num_experts_per_tok cannot exceed n_routed_experts.")
135 if self.attn_block_mode not in ("tail", "blockmask"):
136 raise ValueError("attn_block_mode must be 'tail' or 'blockmask'.")
137 if self.diff_time_embed_mode not in ("add", "prepend", "adaln"):
138 raise ValueError("diff_time_embed_mode must be 'add', 'prepend', or 'adaln'.")
140 @classmethod
141 def from_model_config(cls, model_config: Any) -> "BackboneFlopsConfig":
142 """Read the final model configuration after architecture overrides.
144 Args:
145 model_config: Configuration with explicit decoder dimensions and layer types.
146 """
147 config = _configuration_dict(model_config)
148 text = _configuration_dict(config.get("text_config", config))
149 required = (
150 "hidden_size", "num_hidden_layers", "num_attention_heads", "intermediate_size",
151 "kv_lora_rank", "q_lora_rank", "qk_nope_head_dim", "qk_rope_head_dim", "v_head_dim",
152 )
153 missing = [name for name in required if name not in text]
154 if missing:
155 raise ValueError(f"Backbone cost model is missing configuration fields: {missing}.")
156 pattern = text.get("mlp_layer_types")
157 if not isinstance(pattern, (list, tuple)):
158 raise ValueError("model_config must provide mlp_layer_types in actual decoder layer order.")
159 qk_dim = (
160 _integer(text["qk_nope_head_dim"], "qk_nope_head_dim", 0)
161 + _integer(text["qk_rope_head_dim"], "qk_rope_head_dim", 0)
162 )
163 if text.get("qk_head_dim", qk_dim) != qk_dim:
164 raise ValueError("qk_head_dim must equal qk_nope_head_dim + qk_rope_head_dim.")
165 return cls(
166 **{name: text[name] for name in required},
167 mlp_layer_types=tuple(pattern),
168 **{name: text.get(name, 0) for name in (
169 "moe_intermediate_size", "n_routed_experts", "num_experts_per_tok", "n_shared_experts",
170 )},
171 attn_block_mode=config.get("attn_block_mode", "tail"),
172 diff_time_embed_mode=config.get("diff_time_embed_mode", "add"),
173 )
176class DefaultCostModel:
177 """Shape-based FLOPs for a shared latent-attention and expert backbone.
179 Args:
180 model_config: Final model configuration or :class:`BackboneFlopsConfig`.
181 Required to derive the backbone's arithmetic workload.
182 training_multiplier: Constant forward-to-training FLOPs proxy: 1 for
183 forward, 3 for forward/backward, or 4 for full-layer recomputation.
184 It does not change balancing decisions when shared by all samples.
186 Note:
187 MAC=2 FLOPs. The attention area per independent raw item is
188 ``P**2 / 2 + D * (P + D)``; blockwise attention adds half the square of each
189 conditional-image placeholder run. ``P`` includes control tokens and
190 conditional-image tokens, not just natural-language text. Costs add
191 across packed items; there is no attention between different items.
193 Linear terms include MLA Q/KV/O projections, dense SwiGLU, routed top-k
194 and shared experts, and router projection. Norms, activations, softmax,
195 attention tile padding, communication and expert-kernel efficiency are
196 excluded. This is theoretical arithmetic, not a latency predictor.
197 """
199 def __init__(self, model_config: Any, *, training_multiplier: float = 3.0) -> None:
200 """Compile workload coefficients from the effective decoder configuration.
202 Args:
203 model_config: Explicit decoder dimensions and attention layout.
204 training_multiplier: Forward-to-training arithmetic scale.
205 """
206 if model_config is None:
207 raise ValueError("DefaultCostModel requires model_config; provide it or supply a custom cost_model.")
208 if (
209 not isinstance(training_multiplier, (int, float))
210 or isinstance(training_multiplier, bool)
211 or not math.isfinite(training_multiplier)
212 or training_multiplier <= 0
213 ):
214 raise ValueError("training_multiplier must be finite and positive.")
215 self.training_multiplier = float(training_multiplier)
216 self.config = (
217 model_config if isinstance(model_config, BackboneFlopsConfig)
218 else BackboneFlopsConfig.from_model_config(model_config)
219 )
220 self.linear_flops_per_token = 0
221 self.attention_flops_per_pair = 0
222 self._compile_coefficients()
224 def _compile_coefficients(self) -> None:
225 """Precompute linear-token and attention-pair FLOPs from backbone dimensions."""
226 config = self.config
227 hidden, heads = config.hidden_size, config.num_attention_heads
228 qk_dim = config.qk_nope_head_dim + config.qk_rope_head_dim
229 query_mac = hidden * heads * qk_dim if config.q_lora_rank is None else (
230 hidden * config.q_lora_rank + config.q_lora_rank * heads * qk_dim
231 )
232 projection_mac = (
233 query_mac
234 + hidden * (config.kv_lora_rank + config.qk_rope_head_dim)
235 + config.kv_lora_rank * heads * (config.qk_nope_head_dim + config.v_head_dim)
236 + heads * config.v_head_dim * hidden
237 )
238 dense_layers = config.mlp_layer_types.count("dense")
239 sparse_layers = config.num_hidden_layers - dense_layers
240 dense_mac = 3 * hidden * config.intermediate_size
241 sparse_mac = (
242 3 * hidden * config.moe_intermediate_size * (config.num_experts_per_tok + config.n_shared_experts)
243 + hidden * config.n_routed_experts
244 )
245 self.linear_flops_per_token = 2 * (
246 config.num_hidden_layers * projection_mac + dense_layers * dense_mac + sparse_layers * sparse_mac
247 )
248 self.attention_flops_per_pair = 2 * config.num_hidden_layers * heads * (qk_dim + config.v_head_dim)
250 def forward_flops(self, metadata: SampleMetadata) -> float:
251 """Estimate decoder forward FLOPs from CPU-only per-raw-item metadata.
253 Args:
254 metadata: One independent item's token counts and attention segments.
255 """
256 prefix = _integer(metadata.features.get("P"), "metadata.features['P']", 0)
257 diffusion = _integer(metadata.features.get("D"), "metadata.features['D']", 0)
258 if prefix + diffusion != metadata.pack_tokens:
259 raise ValueError("metadata features P + D must equal pack_tokens before optional timestep-token insertion.")
260 if diffusion and self.config.diff_time_embed_mode == "prepend":
261 diffusion += 1
262 pairs = 0.5 * prefix * prefix + diffusion * (prefix + diffusion)
263 if self.config.attn_block_mode == "blockmask":
264 runs = metadata.features.get("cond_image_token_lengths")
265 if not isinstance(runs, (list, tuple)):
266 raise ValueError("Blockwise attention requires cond_image_token_lengths (empty for no images).")
267 lengths = [_integer(length, "conditional-image run length") for length in runs]
268 if sum(lengths) > prefix:
269 raise ValueError("Conditional-image run lengths cannot exceed the prefix token count P.")
270 pairs += 0.5 * sum(length * length for length in lengths)
271 return float(self.linear_flops_per_token * (prefix + diffusion) + self.attention_flops_per_pair * pairs)
273 def __call__(self, metadata: SampleMetadata) -> WorkloadCost:
274 """Return backbone-only workload without changing physical packing caps."""
275 return WorkloadCost(llm=self.training_multiplier * self.forward_flops(metadata))
278def resolve_cost_model(cost_model: CostModel | None, model_config: Any = None) -> CostModel:
279 """Use an explicit callback or construct the default from model dimensions.
281 Args:
282 cost_model: Optional user-supplied workload callback.
283 model_config: Required backbone configuration when the callback is absent.
284 """
285 if cost_model is None:
286 return DefaultCostModel(model_config)
287 if not callable(cost_model):
288 raise ValueError("cost_model must be callable: SampleMetadata -> WorkloadCost.")
289 return cost_model
292__all__ = ["BackboneFlopsConfig", "CostModel", "DefaultCostModel"]