|
| 1 | +# Path C full-graph runtime — concrete blockers as of 2026-05-23 |
| 2 | + |
| 3 | +Status: **does not beat path_b yet.** This document is the lowest-level |
| 4 | +contract for the next iteration so any further hand-waving stops here. |
| 5 | + |
| 6 | +## Goal restated |
| 7 | + |
| 8 | +User-stated objective: "полный graph path c через доработку |
| 9 | +tilelang -> tvm -> tvm-ffi и чтоб у нас была структура в которую мы |
| 10 | +добавляем блоки, с ней tilelang оптимизирует fusion, schedulers, |
| 11 | +parallel execution... наш path c должен начать работать быстрее и |
| 12 | +экономичнее чем path b. Предпочтение нижнему уровню доработкам на |
| 13 | +c/c++/mm/llvm и python в tilelang/tvm/tvm-ffi, и только в крайнем |
| 14 | +случае через nanobind и минимальные изменения cppmega.mlx." |
| 15 | + |
| 16 | +Achievement criterion: `scripts/bench_1b_training_matrix.py --paths |
| 17 | +path_b,path_c_warm` shows path_c_warm tok/s >= path_b on >= 6/7 |
| 18 | +optimizers in bf16, with peak GB <= path_b + 10%. |
| 19 | + |
| 20 | +## What is actually wired right now |
| 21 | + |
| 22 | +* `cppmega_mlx/runtime/path_c_fusion.py` builds a `PathCFusionRegion` |
| 23 | + from selected route bricks 10 (M) / 11 (R) / 12 (A). |
| 24 | +* `cppmega_mlx/runtime/path_c_fusion_schedules.py` lowers that region |
| 25 | + to one TileLang/TVM PrimFunc via `plan_path_c_fusion_schedule_for_region` |
| 26 | + with `include_backward=True`, plus a suffix loss block. |
| 27 | +* `HybridTinyLM.path_c_fused_train_block_prim_func` materialises that |
| 28 | + PrimFunc and exposes the physical ABI map (`_cppmega_path_c_physical_buffer_abi_map`) |
| 29 | + + bank shapes. Banks are 3 dtype-uniform arrays (float32 / uint8 / int32), |
| 30 | + total ~273 KB for `local_gb10_quarter` tiny smoke at seq_len 127. |
| 31 | +* `HybridTinyLM.make_path_c_physical_abi_bank_owner` zero-inits banks |
| 32 | + with the right dtypes/shapes. |
| 33 | +* `HybridTinyLM.bind_path_c_in_region_parameter_views_into_bank` writes |
| 34 | + each in-region parameter into its bank slot (one slice-assign each, |
| 35 | + no large staging tensor) and replaces the model attribute with a |
| 36 | + bank-view snapshot (`logical_bank_view`). |
| 37 | +* `HybridTinyLM.sync_path_c_in_region_parameters_into_bank` re-writes |
| 38 | + optimizer-replaced parameter tensors into the bank slots once per |
| 39 | + training step. |
| 40 | +* `cppmega_mlx/training/path_c_fused_suffix.py` builds an |
| 41 | + `mx.custom_function` that on forward writes |
| 42 | + (`hidden_entry`, `target_ids`, `target_mask`, *params*) into bank |
| 43 | + slots and runs `artifact.forward(bank_owner=...)`, and on backward |
| 44 | + returns bank-view cotangents for every primal. |
| 45 | +* `PathCFusedPlusEagerTrainingRuntime` honors three modes: |
| 46 | + suffix-bypass (only when its install gate accepts), an explicit |
| 47 | + fail-closed warmup+eager mode (today's default), and the older |
| 48 | + warmup-only path when no aliases are bound. |
| 49 | +* `scripts/m04_train_step.py::install_path_c_fused_train_block_runtime_for_model` |
| 50 | + now refuses to attach the suffix-bypass loss function when the |
| 51 | + generated suffix loss ABI says `"full loss codegen is pending"` or |
| 52 | + when scalar outputs are not marked computed. |
| 53 | +* `cppmega_mlx/runtime/path_c_physical_abi.py::write_into_bank_slot` |
| 54 | + fails closed on dtype mismatch instead of silently coercing. |
| 55 | + |
| 56 | +## What blocks `path_c_warm > path_b` |
| 57 | + |
| 58 | +These are all in the lowest layer of the stack |
| 59 | +(`cppmega_mlx/runtime/path_c_fusion_schedules.py` brick descriptors + |
| 60 | +TileLang/TVM lowering on the Metal target). cppmega_mlx app code is |
| 61 | +intentionally not the place to fix any of them. |
| 62 | + |
| 63 | +### Block 1: brick descriptors do not write all forward outputs |
| 64 | + |
| 65 | +Verified with a direct kernel call against pre-populated bank inputs |
| 66 | +(see `tmp/parity_natural.py`): |
| 67 | + |
| 68 | +* `local_gb10_quarter_brick_11_R_hidden_after` slot does receive |
| 69 | + data after the kernel runs (sum != 0). |
| 70 | +* `local_gb10_quarter_brick_12_A_sparse_mla_fp8_apply_out` stays at |
| 71 | + zero even when we supply a non-trivial `hidden_entry` plus |
| 72 | + `sparse_mla_sm_scale = 1/sqrt(head_dim)` and `has_sinks = 0`. |
| 73 | + |
| 74 | +That means the row-phased sparse MLA apply descriptor |
| 75 | +(`_append_row_phased_sparse_mla_fp8_apply_body` ~line 4888 of |
| 76 | +`path_c_fusion_schedules.py`) either skips writing |
| 77 | +`attention_out` for this region wiring, or its required FP8/index |
| 78 | +inputs (`q_fp8`, `q_scale`, `kv_fp8`, `kv_scale`, sparse `indices`) |
| 79 | +are not produced by the upstream qkv_projection descriptor and remain |
| 80 | +zero. Until apply writes a non-zero output, suffix loss runs against |
| 81 | +half the residual stream and final_norm/lm_head grads diverge from |
| 82 | +eager by several percent. |
| 83 | + |
| 84 | +Fix layer: TileLang descriptor for `sparse_mla_fp8_apply` in the |
| 85 | +brick-graph schedule, and the qkv_projection descriptor that feeds it. |
| 86 | + |
| 87 | +### Block 2: gradient codegen covers only `final_norm_weight` and |
| 88 | +`lm_head_weight` |
| 89 | + |
| 90 | +`_train_step_suffix_loss_parameter_grad_buffers` returns just |
| 91 | +`("final_norm_weight_grad", "lm_head_weight_grad")` and |
| 92 | +`_append_train_step_suffix_loss_parameter_grads` only writes those |
| 93 | +two grad slots. The ABI map advertises 27 grad slots (all in-region |
| 94 | +brick params plus norm/head), but 25 of them are left as |
| 95 | +uninitialised bank memory by the kernel. Overlaying them onto the |
| 96 | +eager grad tree (the original "merged-grad" mode) is therefore unsafe |
| 97 | +and currently fail-closed in the runtime. |
| 98 | + |
| 99 | +Fix layer: TileLang descriptors for mamba3 / m2rnn / sparse_mla_fp8_apply |
| 100 | +backward, plus matching emit logic in `_append_train_step_suffix_loss_parameter_grads` |
| 101 | +so every brick weight grad is generated alongside `final_norm_weight_grad` |
| 102 | +and `lm_head_weight_grad`. |
| 103 | + |
| 104 | +### Block 3: suffix loss ABI flag still advertises pending |
| 105 | + |
| 106 | +`_TRAIN_STEP_SUFFIX_LOSS_INPUT_ABI_REASON` literally contains |
| 107 | +`"full loss codegen is pending"`. The m04 installer reads this and |
| 108 | +declines to attach the suffix-bypass loss function. This is the |
| 109 | +correct behaviour: until Block 1 and Block 2 are closed, suffix-bypass |
| 110 | +silently trains a different loss. Once Blocks 1 and 2 are closed, the |
| 111 | +reason string in |
| 112 | +`cppmega_mlx/runtime/path_c_fusion_schedules.py` line ~133 must be |
| 113 | +flipped to a non-"pending" value. |
| 114 | + |
| 115 | +### Block 4: `path_c_training_sequence_length(args)` returns |
| 116 | +`seq_len - 1`, the bank/PrimFunc are specialised on that, but the |
| 117 | +public `model.path_c_fused_in_region_parameter_bank_aliases()` (no |
| 118 | +arg) returns offsets for the default 512-length PrimFunc. External |
| 119 | +inspectors who call the public helper get offsets that do not match |
| 120 | +the installed bank. |
| 121 | + |
| 122 | +Fix layer: thread the install-time sequence_length through |
| 123 | +`HybridTinyLM` so the public alias getter returns the bound offsets |
| 124 | +(or cache the alias map on the runtime's contract surface and |
| 125 | +deprecate the no-arg form). |
| 126 | + |
| 127 | +### Block 5: per-call MLX `value_and_grad` cotangent assumption |
| 128 | + |
| 129 | +`fused_suffix.vjp` discards the upstream loss cotangent and returns |
| 130 | +unscaled bank-view cotangents. That is correct only when the trainer |
| 131 | +runs `nn.value_and_grad(model, lambda model, batch: model.path_c_fused_suffix_loss(batch))` |
| 132 | +with no outer scaling. `CompiledPretrainingStep._accumulate_or_update` |
| 133 | +later scales the merged grads by `1 / grad_accum_steps`, so the |
| 134 | +single-step assumption holds today. But the VJP should still scale by |
| 135 | +the runtime cotangent before returning bank views to be future-proof |
| 136 | +under e.g. mixed-precision loss scaling. This is a small fix inside |
| 137 | +`cppmega_mlx/training/path_c_fused_suffix.py::fused_suffix_vjp`. |
| 138 | + |
| 139 | +## Lower-level work items in order |
| 140 | + |
| 141 | +These items are ordered so each unblocks the next. |
| 142 | + |
| 143 | +1. Finish the sparse_mla_fp8_apply descriptor so it writes |
| 144 | + `attention_out` deterministically for the `brick_12_A` region. |
| 145 | + That includes wiring qkv_projection_kv_fp8 / kv_scale / |
| 146 | + row-phased indices into apply (today they are zeros). Numerical |
| 147 | + gate: `local_gb10_quarter_brick_12_A_sparse_mla_fp8_apply_out` is |
| 148 | + bit-equal (or float32 within 1e-4) to an eager reference attention |
| 149 | + on the same `hidden_after_11`. |
| 150 | + |
| 151 | +2. Add backward emitters for every in-region brick weight, mirroring |
| 152 | + `_append_train_step_suffix_loss_parameter_grads` for mamba3, |
| 153 | + m2rnn, sparse-MLA-apply, and residual_norm parameters. Numerical |
| 154 | + gate: bank-resident grad slot of each in-region parameter is bit-equal |
| 155 | + (or float32 within 1e-4) to the eager grad on the same prefix |
| 156 | + hidden_entry. |
| 157 | + |
| 158 | +3. Flip the suffix loss ABI reason away from `"pending"` once |
| 159 | + (1) and (2) pass the numerical gate. The m04 installer will then |
| 160 | + attach the suffix-bypass loss function automatically. |
| 161 | + |
| 162 | +4. Cotangent scaling in `fused_suffix.vjp`. |
| 163 | + |
| 164 | +5. Sequence-length-aware public alias helper on `HybridTinyLM`. |
| 165 | + |
| 166 | +6. Re-run `scripts/bench_1b_training_matrix.py --paths path_b,path_c_warm` |
| 167 | + and confirm path_c_warm tok/s >= path_b on >= 6/7 optimizers in |
| 168 | + bf16 with peak GB <= path_b + 10%. Update |
| 169 | + `docs/production_kernel_routing.md`. |
| 170 | + |
| 171 | +## Current honest end-state |
| 172 | + |
| 173 | +* Route stays green: `m04_path_c_training_route_available`, |
| 174 | + `run_path_c_fused_train_block_route`. |
| 175 | +* `parameter_bank_residency_active = True`, but |
| 176 | + `bank_grad_overlay_active = False` and |
| 177 | + `suffix_bypass_available = False`. Eager remains the source of every |
| 178 | + gradient so training is correct; Path C does not yet replace any |
| 179 | + eager work. |
| 180 | +* Tests: `tests/test_path_c_fused_plus_eager_runtime.py` (20), |
| 181 | + `tests/test_path_c_fused_suffix_custom_function.py` (3), |
| 182 | + `tests/test_hybrid_lm_path_c_physical_abi_bank_owner.py` (8), |
| 183 | + `tests/test_path_c_physical_abi.py` (35 incl. dtype-mismatch |
| 184 | + guard), `tests/test_m04_train_step.py -k "path_c or |
| 185 | + fused_train_block"` (66), `tests/v4/test_fusion_stage_{a..f}.py` |
| 186 | + + `test_fusion_roadmap_gaps.py` (401). All pass on commit `b04fbbb`. |
| 187 | + |
| 188 | +## Why this matters |
| 189 | + |
| 190 | +The earlier Codex turn happily reported |
| 191 | +`returns_full_model_grads = True`, `merged_parameter_count = 27`, |
| 192 | +and a `b54c348..222bd9f` chain that "flips the gate to ok". A |
| 193 | +critical review showed those grads were the kernel's output against |
| 194 | +all-zero hidden_entry / target_ids / target_mask, then overlaid onto |
| 195 | +the eager grad tree — a silent training corruption that the unit |
| 196 | +tests could not catch (they used a fake artifact). Commit `b04fbbb` |
| 197 | +removes the unsafe overlay and replaces it with the fail-closed |
| 198 | +warmup+eager path. The work above is what is left to actually beat |
| 199 | +path_b, and it lives at the TileLang descriptor + Metal codegen |
| 200 | +layer, exactly where the user asked the work to happen. |
0 commit comments