[perf](kt-kernel): SFT end2end#2094
Conversation
Coarsen base-weight gradient work from individual output tiles to fixed-intermediate strips so each task reuses packed panels across the hidden dimension. Keep aligned thread-local BF16 panels across tasks and retain FP32 AMX accumulator tiles for the full K reduction. Gate and up run separate K passes while sharing the packed input panel. On the matched Qwen3-30B-A3B 1-GPU test, stable Full-FT backward drops from 9.281s to 6.792s (-26.82%), step time drops from 19.252s to 16.140s, and TPS rises from 212.76 to 253.78. The LoRA-only backward control changes by -2.74%. Validated with clang-format, the Release AMX/CUDA extension build, TP1/TP2 reference gradients across boundary token counts, and the 15-step Full-then-LoRA performance run.
Use one expert-aggregated tile driver for AVX512-BF16 and AMX base-weight gradients, and pack updated full-precision weights directly into TP BufferB layouts without temporary partitions. Add worker-local profiling and focused dWeight/strided-repack coverage.
Retain the first CPU MoE forward state across non-reentrant checkpoint recomputation, write BF16 activations directly into the backward cache, and reduce dWeight packing and gradient-clear traffic. Extend staged profiling and cover checkpoint reuse plus AMX/AVX dWeight paths.
Bind Full-FT and LoRA Parameter.grad directly to the KT-managed BF16 gradient buffers, avoiding PyTorch duplicate accumulation. Accumulate microbatch gradients in C++, lazily clear expert buffers between optimizer windows, preserve rank-0 distributed ownership, and add lifecycle and AMX dWeight coverage.
There was a problem hiding this comment.
Code Review
This pull request introduces full fine-tuning (full weight gradient) support and lightweight staged profiling for the KT SFT MoE operators. Key changes include the implementation of BF16DWeightKernel for outer-product gradient accumulation, direct BF16 weight reloading, and authoritative optimizer-gradient lifecycle management to reduce memory overhead and avoid full object recreation. It also adds a staged profiler (SFTProfiler) and comprehensive unit tests. The review feedback highlights a bug in multi-threaded profiling where thread_local timings are incorrectly read on the main thread, a conceptual indexing issue in from_bb_transposed regarding tile strides, and an optimization opportunity in sync_kt_lora_gradients to avoid an unnecessary temporary CPU tensor allocation during cross-device copies.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| [this](int _) { | ||
| auto& timings = amx::bf16_dweight_timings(); | ||
| profiler_.record_ns(SFTProfileStage::BwdBaseWeightGradPanelInput, timings.panel_input_ns, | ||
| timings.panel_input_calls); | ||
| profiler_.record_ns(SFTProfileStage::BwdBaseWeightGradPanelGradOutput, timings.panel_grad_output_ns, | ||
| timings.panel_grad_output_calls); | ||
| }); |
There was a problem hiding this comment.
The amx::bf16_dweight_timings() function returns a thread_local reference. Since the finalizer lambda [this](int _) is executed on the main thread after all tasks are completed, timings will only contain the metrics for the tasks executed by the main thread itself, completely ignoring the metrics from other worker threads in the pool. This leads to severely underreported profiling statistics in multi-threaded environments. Since SFTProfiler::record_ns is already thread-safe (using std::atomic::fetch_add), a much simpler and correct approach is to record the timings directly from the worker tasks (e.g., at the end of each task or periodically), or accumulate them into a shared/atomic structure.
| for (int i = 0; i < N_STEP; i++) { | ||
| for (int j = 0; j < K_STEP; j++) { | ||
| dst_tile[j * K_STEP + i] = src_tile[i * K_STEP + j]; | ||
| } | ||
| } |
There was a problem hiding this comment.
In from_bb_transposed, the destination tile dst_tile has dimensions K_STEP (rows) by N_STEP (columns). Therefore, its row stride is N_STEP. The indexing for dst_tile should use j * N_STEP + i instead of j * K_STEP + i. Although K_STEP and N_STEP are currently both 32, using N_STEP is conceptually correct and prevents potential out-of-bounds or layout corruption bugs if the step sizes ever differ in future configurations.
for (int i = 0; i < N_STEP; i++) {
for (int j = 0; j < K_STEP; j++) {
dst_tile[j * N_STEP + i] = src_tile[i * K_STEP + j];
}
}| if grad_buf is not None: | ||
| grad_gpu = grad_buf.cuda() | ||
| dist.all_reduce(grad_gpu, op=dist.ReduceOp.SUM) | ||
| grad_gpu.div_(world_size) | ||
| grad_buf.copy_(grad_gpu.cpu()) |
There was a problem hiding this comment.
In sync_kt_lora_gradients, when copying the synchronized base weight gradients back to the CPU buffer, grad_buf.copy_(grad_gpu.cpu()) is used. This creates an unnecessary temporary CPU tensor via .cpu(). Since PyTorch's copy_ operator natively supports cross-device copies, you can directly use grad_buf.copy_(grad_gpu) to avoid the overhead of allocating and copying to a temporary CPU tensor, matching the optimized pattern used for LoRA gradients on line 783.
| if grad_buf is not None: | |
| grad_gpu = grad_buf.cuda() | |
| dist.all_reduce(grad_gpu, op=dist.ReduceOp.SUM) | |
| grad_gpu.div_(world_size) | |
| grad_buf.copy_(grad_gpu.cpu()) | |
| if grad_buf is not None: | |
| grad_gpu = grad_buf.cuda() | |
| dist.all_reduce(grad_gpu, op=dist.ReduceOp.SUM) | |
| grad_gpu.div_(world_size) | |
| grad_buf.copy_(grad_gpu) |
Allocate authoritative Full-FT gradient buffers with torch.empty. The C++ state machine performs the mandatory full clear before first use, avoiding redundant Python-side first touch.
Depends on #2086.
This is a stacked PR based exactly on the #2086 head commit
66f5f15. The implementation specific review commit is6eeffa7.What does this PR do?
Makes the BF16 buffers managed by KT C++ the single authoritative optimizer-gradient storage for CPU-only AMXBF16_SFT Full-FT, LoRA, and Hybrid training.
Parameter.graddirectly to the preallocated KT buffers.KTMoEFunction.backward, removing the duplicate PyTorchAccumulateGradand largeaten::add_path..gradaliases afteroptimizer.step; Full-FT gradient buffers usetorch.empty, and C++ clears them before first use.1 / world_sizeonly to parameter gradients.The scope remains CPU-only AMXBF16_SFT. Quantized backends, GPU experts, and SkipLoRA behavior are unchanged.
Validation
test_bf16_dweighttargets build successfully.AccumulateGrad, largeaten::add_, or KT zero-gradient scan.Before submitting