Skip to content

Commit 75319c4

Browse files
committed
docs: VisualBuilderSpec.md — roadmap for spec layer над Bricks
Слой между BrickGraph и GUI/runtime: символьные shape-контракты на бриках, авто-резолвер с adapter-вставками, memory report (с учётом fusion regions для shared activations). Цель: GUI красит несовместимые рёбра ДО Compile и показывает "влезет / не влезет в HBM" до того как кто-то получит CUDA OOM в runtime. Слой лежит ВЫШЕ Auto-Fusion (потребляет FusionRegionPlan). 5 этапов: A — shape_contract.py: ShapeExpr + BrickShapeContract + contracts на все 18 BLOCK_BUILDERS kinds B — resolver.py: ResolvedBrickGraph + resolve_shapes (strict + lenient) C — adapters.py: 5+ auto-bridge brick rules (merge/split_heads, linear_bridge, residual_wrap, causal_mask) D — memory_report.py: per-brick + per-region rollup с учётом fusion regions, KV-cache, AdamW/Muon states E — verify_and_estimate + suggest_adapters + 12-preset perf gate bd epic: cppmega-mlx-dyr (с 5 stage children dyr.1–dyr.5).
1 parent 0b5523e commit 75319c4

1 file changed

Lines changed: 312 additions & 0 deletions

File tree

VisualBuilderSpec.md

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
План: Visual-Builder Spec Layer (Shape + Memory над Bricks)
2+
3+
Цель: дать визуальному конструктору слой, в котором (а) выходы блоков
4+
автоматически подгоняются ко входам следующих, (б) расчёт памяти
5+
рассчитывается ДО компиляции — чтобы GUI красил рёбра красным и
6+
показывал «не влезет в HBM» прежде, чем кто-нибудь нажмёт Compile, а
7+
не получал куб CUDA OOM в рантайме.
8+
9+
Лежит ВЫШЕ Auto-Fusion — потребляет `FusionRegionPlan`, чтобы
10+
правильно считать активации (fused-регионы делят shared registers и
11+
не суммируются).
12+
13+
---
14+
1. Что уже есть (карта существующего)
15+
16+
V4 Bricks (cppmega_v4/models/unified_superblock_v4.py)
17+
18 kinds в BLOCK_BUILDERS; каждый возвращает nn.Module. Топологию
18+
знаем — shapes НЕ знаем.
19+
20+
Auto-Fusion (cppmega_v4/fusion/, шипнуто на main)
21+
- BrickGraph: kind + params + module, но без shape-метаданных
22+
- plan_fusion_regions → FusionRegionPlan: знает категории, не знает
23+
байты
24+
- auto_compile_region: выбирает pattern + descriptors, но ничего не
25+
знает про память региона
26+
27+
Архитектурные пресеты (cppmega_v4/architectures/, 12 моделей)
28+
Дают JSON-spec для одной repeat-unit; не считают параметров и не
29+
делают валидацию shape-цепочки.
30+
31+
Тренировочный код (scripts/m04_train_step.py и др.)
32+
Память считается грубо — `params * 6 bytes (bf16 + fp32 grad + AdamW)`.
33+
Активации не учитываются вообще. KV-cache не учитывается.
34+
35+
---
36+
2. Что НЕ хватает (gap-analysis)
37+
38+
┌────────────────────────────────────────┬───────────────────────────────┐
39+
│ Что нужно GUI │ Сейчас │
40+
├────────────────────────────────────────┼───────────────────────────────┤
41+
│ Знать shape входа/выхода каждого брика │ ✗ (надо инстанциировать) │
42+
├────────────────────────────────────────┼───────────────────────────────┤
43+
│ Подсветить несовпадение shape на ребре │ ✗ (узнаём через CUDA traceback)│
44+
├────────────────────────────────────────┼───────────────────────────────┤
45+
│ Авто-вставить reshape / Linear bridge │ ✗ (юзер делает руками) │
46+
├────────────────────────────────────────┼───────────────────────────────┤
47+
│ Посчитать вес модели по графу │ частично (только params) │
48+
├────────────────────────────────────────┼───────────────────────────────┤
49+
│ Учесть optimizer states (AdamW = 2×) │ ✗ │
50+
├────────────────────────────────────────┼───────────────────────────────┤
51+
│ Учесть активации (с recompute / без) │ ✗ │
52+
├────────────────────────────────────────┼───────────────────────────────┤
53+
│ Учесть KV-cache при decode-режиме │ ✗ │
54+
├────────────────────────────────────────┼───────────────────────────────┤
55+
│ Учесть fusion: внутри региона меньше │ ✗ │
56+
├────────────────────────────────────────┼───────────────────────────────┤
57+
│ Сравнить total vs device HBM │ ✗ (узнаём через OOM) │
58+
└────────────────────────────────────────┴───────────────────────────────┘
59+
60+
---
61+
3. Архитектура (новый пакет cppmega_v4/spec/)
62+
63+
3.1 cppmega_v4/spec/shape_contract.py — символьный shape-контракт per brick
64+
65+
@dataclass(frozen=True)
66+
class ShapeExpr:
67+
"""Символьное выражение над named dims.
68+
Пример: ShapeExpr("B", "S", "H") = (B, S, H)
69+
ShapeExpr("B", "S", "nh*head_dim")
70+
"""
71+
dims: tuple[str, ...]
72+
73+
def resolve(self, env: dict[str, int]) -> tuple[int, ...]: ...
74+
75+
@dataclass(frozen=True)
76+
class BrickShapeContract:
77+
inputs: dict[str, ShapeExpr] # {"x": ShapeExpr("B","S","H")}
78+
outputs: dict[str, ShapeExpr]
79+
params_bytes: ShapeExpr # символьный footprint весов
80+
activations_bytes: ShapeExpr # peak forward (без recompute)
81+
kv_cache_bytes: ShapeExpr # 0 для не-attention бриков
82+
needs: frozenset[str] # {"doc_ids", "kv_cache"}
83+
opaque_shape: bool = False # data-dependent (sparse)
84+
85+
# API:
86+
def contract_for(kind: str) -> BrickShapeContract: ...
87+
def register_contract(kind: str, c: BrickShapeContract) -> None: ...
88+
89+
Все 18 BLOCK_BUILDERS kinds получают контракт — одна строка на брик
90+
(см. Этап A). Opaque-бриков мало (nsa, csa_hca) — для них
91+
``opaque_shape=True`` означает «B/S/H сохраняются, остальное trust».
92+
93+
3.2 cppmega_v4/spec/resolver.py — shape-resolver + ResolvedBrickGraph
94+
95+
@dataclass(frozen=True)
96+
class ResolvedEdge:
97+
producer: str
98+
consumer: str
99+
shape: tuple[int, ...] # после подстановки dim_env
100+
adapter: BrickNode | None # None если совпало, иначе вставленный
101+
102+
@dataclass(frozen=True)
103+
class ResolvedBrickGraph:
104+
original: BrickGraph
105+
dim_env: dict[str, int]
106+
nodes: tuple[BrickNode, ...] # = original.nodes + адаптеры
107+
edges: tuple[ResolvedEdge, ...]
108+
diagnostics: tuple[ShapeDiagnostic, ...] # warnings / errors
109+
110+
def resolve_shapes(
111+
graph: BrickGraph,
112+
dim_env: dict[str, int],
113+
*,
114+
strict: bool = True,
115+
) -> ResolvedBrickGraph: ...
116+
117+
Алгоритм:
118+
- Для каждого ребра: берём producer.outputs["y"], consumer.inputs["x"]
119+
- Резолвим обе через dim_env
120+
- Если shape совпали — adapter=None
121+
- Если layout-mismatch и есть адаптер — auto-вставляем
122+
- Если dim-mismatch — Diagnostic(severity=ERROR), GUI красит красным
123+
- strict=True → выкидываем ResolveError; False → возвращаем graph с
124+
diagnostics для GUI
125+
126+
3.3 cppmega_v4/spec/adapters.py — библиотека авто-bridge брик
127+
128+
ADAPTER_RULES: list[AdapterRule] = [
129+
AdapterRule(
130+
name="merge_heads",
131+
when=lambda p, c: p.layout == "(B,nh,S,d)" and c.layout == "(B,S,H)",
132+
build=lambda: ReshapeBrick(...),
133+
),
134+
AdapterRule(
135+
name="split_heads",
136+
when=...,
137+
build=lambda: ReshapeBrick(...),
138+
),
139+
AdapterRule(
140+
name="linear_bridge",
141+
when=lambda p, c: p.last_dim != c.first_dim,
142+
build=lambda H_a, H_b: LinearBrick(H_a, H_b),
143+
),
144+
AdapterRule(
145+
name="residual_wrap",
146+
when=...,
147+
build=...,
148+
),
149+
]
150+
151+
Каждое правило знает: (а) когда срабатывает, (б) какие байты
152+
параметров добавляет в memory report, (в) в какую fusion-категорию
153+
попадает (`norm_or_proj` — фьюзится с соседями).
154+
155+
3.4 cppmega_v4/spec/memory_report.py — катится поверх ResolvedBrickGraph
156+
157+
@dataclass(frozen=True)
158+
class BrickMemoryRow:
159+
kind: str
160+
name: str
161+
params_bytes: int
162+
activations_bytes: int
163+
kv_cache_bytes: int
164+
165+
@dataclass(frozen=True)
166+
class RegionMemoryRow:
167+
region_idx: int
168+
brick_names: tuple[str, ...]
169+
shared_activations_bytes: int # деление в fused-региона
170+
params_bytes: int # сумма по бриков
171+
172+
@dataclass(frozen=True)
173+
class MemoryReport:
174+
dim_env: dict[str, int]
175+
weights_bytes: int
176+
grads_bytes: int
177+
optimizer_bytes: int # AdamW = 2× weights (m + v)
178+
activations_bytes: int
179+
kv_cache_bytes: int
180+
edge_handoff_bytes: int # между не-fused
181+
total_bytes: int
182+
per_brick: dict[str, BrickMemoryRow]
183+
per_region: dict[int, RegionMemoryRow]
184+
185+
def fits_on(self, device_hbm_bytes: int, *, headroom: float = 0.9) -> bool:
186+
return self.total_bytes <= device_hbm_bytes * headroom
187+
188+
def estimate_memory(
189+
resolved: ResolvedBrickGraph,
190+
*,
191+
fusion_plan: tuple[FusionRegionPlan, ...] | None = None,
192+
dtype_bytes: int = 2, # bf16 default
193+
optimizer: str = "adamw",
194+
training: bool = True,
195+
kv_cache_dtype_bytes: int = 1, # int8 quant
196+
) -> MemoryReport: ...
197+
198+
Главная фишка: если передан fusion_plan, активации внутри одного
199+
региона учитываются ОДИН РАЗ (max по бриков), не сумируются. Это и
200+
делает Auto-Fusion видимой экономией для GUI.
201+
202+
3.5 cppmega_v4/spec/__init__.py — публичный API
203+
204+
# Verify-and-estimate в один вызов (то что зовёт GUI):
205+
def verify_and_estimate(
206+
graph: BrickGraph,
207+
dim_env: dict[str, int],
208+
*,
209+
device_hbm_bytes: int | None = None,
210+
training: bool = True,
211+
) -> tuple[ResolvedBrickGraph, MemoryReport]: ...
212+
213+
# Подсказчик адаптеров для GUI ("какие brick'и вставить чтоб подошло?")
214+
def suggest_adapters(
215+
producer: BrickNode,
216+
consumer: BrickNode,
217+
dim_env: dict[str, int],
218+
) -> list[AdapterSuggestion]: ...
219+
220+
---
221+
4. Как пресеты ложатся в memory report
222+
223+
Прогон verify_and_estimate на типичном dev-setup (B=1, S=4096, H=4096
224+
если не сказано иначе):
225+
226+
┌──────────────────────┬──────────────┬──────────────┬─────────────────┐
227+
│ Preset │ Weights (Gb) │ KV-cache (Mb)│ Влезет в 80Gb? │
228+
├──────────────────────┼──────────────┼──────────────┼─────────────────┤
229+
│ qwen3_next (1 unit) │ 0.8 │ 16 │ ✓ │
230+
│ ling26 (1 unit) │ 1.2 │ 24 │ ✓ │
231+
│ kimi_k2 (1 unit) │ 4.0 │ 8 │ ✓ (per unit)│
232+
│ deepseek_v3 26 units │ ~70 │ 320 │ ✓ tight │
233+
│ gemma4 (1 unit) │ 0.6 │ 128 │ ✓ │
234+
│ zaya1 (1 unit) │ 0.5 │ 192 │ ✓ │
235+
└──────────────────────┴──────────────┴──────────────┴─────────────────┘
236+
237+
GUI получает табличку «сколько unitов поместится на твоём железе»
238+
прежде чем юзер начнёт ставить repeat=N.
239+
240+
---
241+
5. Что GUI получает в итоге
242+
243+
- Каждый брик в палитре имеет shape-badge: `(B,S,H) → (B,S,H)`
244+
- Когда юзер тянет ребро между двумя бриками:
245+
• зелёное если shape совпали
246+
• жёлтое + suggestion если есть auto-adapter
247+
• красное если несовместимо вообще
248+
- Боковая панель: «Memory: 18.2 / 80 GB» — обновляется при каждом
249+
изменении графа
250+
- Тултип на красном брике: «4 GB peak activations — не влезает с
251+
batch=8, попробуй batch=2 или включи gradient_checkpointing»
252+
253+
---
254+
6. План реализации поэтапно
255+
256+
Этап A — ShapeContract foundation (1 заход)
257+
- cppmega_v4/spec/shape_contract.py — ShapeExpr + BrickShapeContract
258+
+ registry + контракты на все 18 BLOCK_BUILDERS kinds
259+
- Тесты: каждый kind имеет contract, ShapeExpr.resolve() корректен
260+
для линейных и нелинейных выражений (nh*head_dim, q_lora_rank+kv_lora_rank)
261+
262+
Этап B — Resolver + ResolvedBrickGraph (1 заход)
263+
- cppmega_v4/spec/resolver.py
264+
- strict-mode (бросает на несовпадении) и lenient (для GUI)
265+
- Тесты: цепочка Qwen3-Next резолвится без диагностик; искусственный
266+
H-mismatch → красный edge; layout-mismatch → adapter подложен
267+
268+
Этап C — Adapter library (1 заход)
269+
- cppmega_v4/spec/adapters.py — 5+ rules (merge/split_heads,
270+
linear_bridge, residual_wrap, causal_mask)
271+
- Каждый адаптер — это полноценный BLOCK_BUILDERS kind c shape-контрактом
272+
- Тесты: каждый adapter правильно вставляется, fusion-планнер
273+
группирует его с соседями (адаптер = norm_or_proj категория)
274+
275+
Этап D — MemoryReport (1 заход)
276+
- cppmega_v4/spec/memory_report.py
277+
- Учёт fusion-plan для shared activations
278+
- Учёт KV-cache (только в decode-режиме), optimizer states (AdamW/Muon)
279+
- Тесты: пустой граф = 0; известный Qwen3-Next ≈ известное число
280+
(±5%); fusion даёт меньше активаций чем без fusion (доказательство
281+
что fusion помогает не только скорости)
282+
283+
Этап E — Public API + GUI hooks (1 заход)
284+
- verify_and_estimate(graph, dim_env, *, device_hbm_bytes)
285+
- suggest_adapters(producer, consumer, dim_env) → list[AdapterSuggestion]
286+
- Sanity benchmarks: каждый из 12 пресетов даёт MemoryReport за
287+
< 50 ms на хост-CPU (это критично для real-time GUI)
288+
- Тесты: интеграционный тест «GUI workflow»: построить граф,
289+
добавить плохое ребро, получить diagnostic, accept suggested
290+
adapter, получить чистый MemoryReport
291+
292+
---
293+
7. Бюджет и риски
294+
295+
Бюджет: 5 этапов × ~2 часа = ~10 часов чистого кодинга. Этапы A и B
296+
строго последовательны (resolver зависит от contract); C/D/E
297+
параллелятся между собой после B.
298+
299+
Главный риск: символьные shape-выражения для не-тривиальных бриков
300+
(MLA: nh*v_head_dim + nh*qk_rope_head_dim, MoE: nh*top_k*expert_dim,
301+
sparse attn: data-dependent). Mitigation: `opaque_shape=True` escape
302+
для таких бриков — резолвер тогда требует от соседей "trust me, B/S/H
303+
сохраняются" и пропускает байтовый расчёт по фолбэк-числу.
304+
305+
Второй риск: memory model точна с ±10% (Apple Metal SDPA workspace,
306+
TileLang scratch, MLX allocator fragmentation). Для GUI "влезет / не
307+
влезет" этого хватит, для прод-sizing нужен второй слой с runtime-замерами.
308+
309+
Третий риск: каждый новый брик (включая будущий MTP-rewrite, FSDP-shards)
310+
должен обновить контракт. Mitigation: тест "for kind in BLOCK_BUILDERS:
311+
assert contract_for(kind) is not None" — забыли зарегистрировать → CI
312+
красный.

0 commit comments

Comments
 (0)