Conversation
The index -> HF-name mapping existed in three near-identical copies (sLLamaWeights::iterate_tensors, OptStateWrapper::iterate_tensors, and MultiGPUPyTrainer::get_gradients). Replace them with name lookup functions in LLamaWeightID and index loops that skip empty tensors. This also makes get_gradients independent of the concrete weight layout, resolving its TODO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It only reads the data pointer and dtype of the source; narrow the parameter to Tensor so that the remaining TensorShard-taking interfaces are exactly the ones that consume shard metadata (safetensors IO via iterate_tensors). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The array is byte storage reinterpreted as x128; declaring it as char* gave it pointer element type and only 8-byte guaranteed alignment. (Spotted via the surogate fork, which hit real misalignment here.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The trainer's batch_size is the per-GPU micro-batch while step() takes the global batch; the harnesses passed the global batch size straight through, so any multi-GPU run failed the shape check. Divide by the GPU count instead, keeping the total batch (and thus gradients) independent of the GPU count. Gather the per-rank gradient shards in torch_reference so the comparison reconstructs full tensors, and add a pytest suite running the 2-GPU parity matrix (data-parallel, shard-weights, shard-gradients, both). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors model/weight naming utilities, expands architecture support to include Mistral (LLaMA-like), hardens config loading against unsupported HF settings, and fixes GEMM address calculations for very large matrix shapes. It also adds integration tests to validate gradient parity across architectures and across 2-GPU training modes.
Changes:
- Add gradient-parity integration tests for supported/refused architectures and for 2-GPU runs with different sharding modes.
- Add Mistral architecture handling in transformer config load/save, plus explicit rejection of unsupported config features (rope_scaling, attention_bias, etc.).
- Refactor weight/gradient naming to use centralized name helpers; update GEMM kernel indexing to use wider integer arithmetic and add a large-output regression test.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/test_multi_gpu.py | Adds end-to-end 2-GPU gradient parity test runner wrapper. |
| test/test_architectures.py | Adds per-architecture gradient parity + “must refuse” negative fixtures. |
| src/utilities/comm.h | Adjusts NCCL all-gather API to accept Tensor instead of TensorShard. |
| src/utilities/comm.cpp | Updates NCCL all-gather implementation signature accordingly. |
| src/training/transformer_config.h | Adds MISTRAL architecture enum. |
| src/training/transformer_config.cpp | Adds Mistral load/save support and explicit rejection of unsupported HF config fields. |
| src/testing/test-gemm.cpp | Adds large-output regression test for GEMM INT overflow bug. |
| src/models/llama_weights.h | Adds centralized HF weight-name helpers and a typed accessor for block tensors. |
| src/models/llama_weights.cpp | Refactors tensor iteration/naming to use centralized name helpers. |
| src/models/llama_optimizer.cpp | Refactors optimizer-state tensor naming/iteration to use centralized name helpers. |
| src/kernels/rmsnorm.cu | Fixes shared-memory declaration/alignment for RMSNorm kernels. |
| src/kernels/gemm_mma.cu | Uses wider index arithmetic to avoid overflow for large m*k / m*n. |
| src/binding/python/tests/torch_reference.py | Fixes multi-GPU gradient retrieval semantics (per-GPU shard concat) and per-GPU microbatch sizing. |
| src/binding/python/tests/run.py | Makes training test runner respect config.gpus and divides global batch into per-GPU microbatches. |
| src/binding/py_train.cpp | Generalizes multi-GPU gradient enumeration via centralized weight-name helpers. |
| scripts/create_tiny_test_model.py | Adds script to generate tiny HF-cache fixtures for integration tests (incl. mistral). |
Comments suppressed due to low confidence (2)
src/testing/test-gemm.cpp:289
- cudaMemset sizes for
aandbare also missingsizeof(__nv_fp8_e4m3), so only a fraction of each buffer is initialized (and if the allocations are fixed, these would become short writes).
CUDA_CHECK(cudaMemset(a, 0x38, (std::size_t)m * k));
CUDA_CHECK(cudaMemset(b, 0x38, (std::size_t)n * k));
src/kernels/gemm_mma.cu:217
out_offsetis intended to be 64-bit (per the comment), but it's declared aslong, which may be 32-bit depending on platform/toolchain. Use an explicitly 64-bit type forout_offsetso the INT overflow fix is robust.
// 64 bit: (row index) * n overflows int once m * n exceeds 2^31
long out_offset = ((long)(i + ii) * TI + r) * n + (j + jj) * TJ + 8 * c;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Mistral is the only architecture string among the llama-compatible models that we did not accept yet; everything else in that group already declares LlamaForCausalLM or Qwen2ForCausalLM and worked unchanged. More importantly, several config.json fields were inferred from the architecture enum or ignored outright, so a checkpoint could be loaded as a model that differs from what it describes -- rope_scaling (Llama-3.1, deepseek-coder) was silently dropped, which only shows up as degraded quality. Read those fields now and refuse what we cannot express exactly: rope_scaling, mlp_bias, attention_bias, a non-silu hidden_act, non-zero attention_dropout, and an active sliding window. Note that Qwen ships an inactive sliding_window that must keep loading. HF's attention_bias also biases o_proj, for which we have no tensor, so it is rejected rather than mapped onto UseQKVBias, which means q/k/v only. The same asymmetry applies when writing: a q/k/v-only bias has no faithful representation in the llama schema, since true would promise an o_proj bias we lack and false would disclaim biases we have. Saving such a config is therefore refused too, so we cannot emit a config.json we would then reject. scripts/create_tiny_test_model.py grows the architectures it can emit, and test/test_architectures.py compares every parameter gradient against transformers for each of them, plus checks that the unrepresentable configs are refused with a decent message. test-transformer-config.cpp covers the save/load round trip per architecture, which is what pins down the save-versus-load asymmetry above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The epilogue computed the output address as `((i + ii) * TI + r) * n` in `int`, so once m * n exceeds 2^31 the product wraps and the kernel writes to a bogus address -- an illegal memory access rather than a wrong result. An LM head of 16384 x 151936 is already past the limit, which is why this showed up on an L40 and not on the 16 GB cards here. The A/B row offsets get the same widening, where the product is m * k / 16. Confirmed both ways: m*n = 2.145e9 runs clean, 2.416e9 faults, and compute-sanitizer pins it to the 16-byte store in the epilogue. All three epilogue paths share the index. The added regression test allocates a >INT_MAX output (~5 GB, skipped if it does not fit) and was verified to fail without the fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.
Suppressed comments (7)
src/utilities/comm.cpp:189
schedule_all_gathertakes a genericTensoras the source, butgather_weight()assumes the source pointer addresses the local shard (it sendsBytes/world_size()bytes fromSrc). Passing a full tensor here would produce an incorrect all-gather (each rank sends the same prefix). Restoring theTensorShardparameter type (or at least validatingsrc.bytes() * world_size() == tgt.bytes()) would prevent accidental misuse.
void NCCLCommunicator::schedule_all_gather(const Tensor& src, Tensor& tgt) {
if (src.Data == nullptr) {
throw std::runtime_error("gather: Source tensor is null");
}
src/testing/test-gemm.cpp:287
- In the new large-output GEMM test, the
cudaMalloc/cudaMemsetsizes foraandbare missingsizeof(__nv_fp8_e4m3). This is inconsistent with the rest of the file (which uses* sizeof(T)everywhere) and would under-allocate / under-initialize if the fp8 storage type is not exactly 1 byte.
CUDA_CHECK(cudaMalloc(&a, (std::size_t)m * k));
CUDA_CHECK(cudaMalloc(&b, (std::size_t)n * k));
CUDA_CHECK(cudaMalloc(&c, out_bytes));
CUDA_CHECK(cudaMalloc(&scale_a, sizeof(float)));
CUDA_CHECK(cudaMalloc(&scale_b, sizeof(float)));
src/kernels/gemm_mma.cu:86
- This kernel relies on 64-bit indexing, but it uses
longcasts.longis not guaranteed to be 64-bit on all platforms (e.g., Windows), which can reintroduce the overflow the comment warns about. Preferlong long(or a fixed-width integer) for the intermediate products.
// 64 bit on purpose: `m * k` (and `m * n` in the epilogue) exceeds INT_MAX at ordinary
// large-model shapes, and the wrapped product becomes an illegal address.
if(wid < 2) {
g_ptr = reinterpret_cast<const uint4*>(a) + (long)(bi + wid) * TI * stride;
s_ptr = input_tiles + wid * ROW_OFFSET;
} else {
g_ptr = reinterpret_cast<const uint4*>(b) + (long)(bj + wid - 2) * TJ * stride;
s_ptr = input_tiles + (wid - 2) * ROW_OFFSET + DEPTH * PIPE_OFFSET;
}
src/kernels/gemm_mma.cu:218
- Same portability concern here:
out_offsetis meant to be 64-bit, butlongis not guaranteed to be 64-bit. Usinglong long(orint64_t) keeps the overflow fix effective across platforms.
// 64 bit: (row index) * n overflows int once m * n exceeds 2^31
long out_offset = ((long)(i + ii) * TI + r) * n + (j + jj) * TJ + 8 * c;
src/training/transformer_config.cpp:105
- The comment says Qwen’s
sliding_windowis inactive unlessuse_sliding_windowis set, but the code currently treats a missinguse_sliding_windowastrue(active) viaget_or("use_sliding_window", true). This makes the behavior contradict the comment and could incorrectly reject some Qwen configs. Consider making the default depend on architecture: defaultfalsefor Qwen2/Qwen3, defaulttruefor others (no flag).
// Qwen's sliding_window stays inactive unless use_sliding_window is set; Mistral has
// no such flag, so a non-null window there is always active.
if(auto it = config_json.find("sliding_window"); it != config_json.end() && !it->is_null()
&& get_or("use_sliding_window", true)) {
reject("sliding_window", it->dump());
}
src/utilities/comm.h:24
schedule_all_gatheris intended to gather a per-rank shard into a full tensor. If you switch the parameter type back toTensorShardfor type-safety,comm.halso needs a forward declaration forTensorShard(it was removed in this PR).
struct Tensor;
src/utilities/comm.h:49
schedule_all_gatherassumessrcpoints at the local shard that will be all-gathered into the fulltgt(seegather_weight()which sendsBytes/world_size()bytes fromSrc). Takingconst TensorShard&here keeps that contract enforceable at compile time and prevents accidental calls with a full tensor.
void schedule_all_gather(const Tensor& src, Tensor& tgt);
some more refactoring, adding support for mistral (llama-like) architectures, fix matmul for larger shapes.