Conversation
| @@ -16,14 +22,18 @@ def set_num_sms(num_sms: int) -> None: | |||
| _num_sms = num_sms | |||
There was a problem hiding this comment.
🔵 suggestion: set_num_sms() still validates num_sms against get_device_num_sms() with no argument, i.e. against the current device only. On hosts with heterogeneous GPUs the process-global override may be accepted based on one device and then applied to another with fewer SMs. This is pre-existing behaviour and the PR documents the override as intentionally global, so it is not a blocker; consider a follow-up that validates against the minimum SM count over visible devices, or makes the override per-device.
🤖 v5
| device the input tensors already live on. | ||
| """ | ||
| if device.type == 'cuda': | ||
| return torch.cuda.device(device) |
There was a problem hiding this comment.
🟡 warning: The device guard only fixes the first call per kernel-factory argument tuple. The get_*_kernel(...) factories are decorated with @tilelang.jit, which caches the compiled JITKernel by factory arguments. The first call compiles/loads the module on whichever device is current at that moment; every subsequent call with the same argument tuple returns that cached kernel regardless of the device set by get_device_guard. So calling e.g. get_fused_mapping with identical args on cuda:0 and then cuda:1 will reuse the cuda:0 module and the second launch either errors or targets the wrong device. This directly contradicts the PR statement that "allocation, compilation and launch all agree on the input device". Please verify with a 2-GPU test that invokes each entry point again on a non-current device after a first call on the current device, and if it fails, include the device in the factory arguments/cache key (or maintain a per-device kernel cache).
🤖 v4
| def get_device_num_sms(device_index: Optional[int] = None) -> int: | ||
| # The device index is part of the cache key: properties are looked up on the | ||
| # requested device and a second device never reuses the first device's value. | ||
| if device_index is None: |
There was a problem hiding this comment.
🟡 warning: Making device_index part of the lru_cache key only helps callers that pass it. When device_index is None, the resolved torch.cuda.current_device() is not recorded in the key, so the first current device's multi_processor_count stays cached under (None,) and is returned even after the current device changes. set_num_sms() (line 21) and tile_kernels/testing/generator.py::generate_num_sms() still call get_device_num_sms() with no argument, so the original multi-GPU SM-count bug survives on those paths, and set_num_sms's assertion can be checked against the wrong device. Consider resolving the device first and dispatching as get_device_num_sms(torch.cuda.current_device()) (so the concrete index is cached), or passing the device index from those callers.
🤖 v4
| if topk_weights is not None: | ||
| assert topk_weights.device == device, f'topk_weights is on {topk_weights.device}, expected {device}' | ||
| if x_sf is not None: | ||
| assert x_sf.device == device, f'x_sf is on {x_sf.device}, expected {device}' |
There was a problem hiding this comment.
🔵 suggestion: The new device validation covers token_topk_to_pos, topk_weights, x_sf and the optional out, but omits sf, which is also a kernel argument when fp8_format == 'e4m3' (passed at line 146). A cross-device sf therefore still slips past validation and fails at launch. Add if sf is not None: assert sf.device == device, ... alongside the other checks.
🤖 v4
🤖 ds-review-bot Code Reviewv6未发现本次变更引入的明确缺陷。8 个文件通过语法检查,模拟检查覆盖了 7 个入口的设备选择与恢复;当前环境缺少 PyTorch 和 TileLang,未验证真实多 GPU 编译和执行。 v5Reviewed commit cfce997 ([BugFix][Multi-GPU] Allocate and launch MoE outputs on the input device). Verdict: approve. The change matches its description: all 16 v4The change is directionally correct: the new Files reviewed: 8 📍 未定位到 diff 的评论🟡 warning 🟡 warning 🟡 warning 🔵 suggestion 🔵 suggestion |
…ice-specific Follow-up to the review on deepseek-ai#30: - inplace_unique_group_indices: derive the device from group_indices, size the grid from that device's SM count and guard build + launch. It was the one remaining moe/ path that also fed a wrong-device value into the kernel. - topk_gate, top2_sum_gate, mask_indices_by_tp, topk_sum_and_topk_group_idx: wrap kernel build and launch in get_device_guard so compilation and launch happen with the input device current, matching the six entry points fixed in the first commit. - config: resolve the current device index before the cached call, so the no-argument path is cached per concrete device instead of under (None,) and set_num_sms()/generate_num_sms() are no longer device-unsafe. - reduce_fused: validate sf's device alongside the other kernel arguments.
|
Thanks for the review. Fixed in the follow-up commit:
Deliberately not changed:
One thing we could not do here: we have no CUDA environment, so the two-GPU run described in the new |
Summary
Six modules under
tile_kernels/moe/(16 allocation sites in total, covering the five entrypoints named in the issue plus
normalize_weight, which carries the byte-identical pattern)allocate their output tensors with an unqualified
device='cuda'and then launch the kernelwithout a device guard.
device='cuda'resolves to the current CUDA device, not to the deviceof the input tensor, so when the input lives on a non-current device (for example input on
cuda:1whiletorch.cuda.current_device()is 0) the outputs are allocated on the wrong deviceand compilation/launch state is derived from the wrong device.
This PR derives the device from the input tensor, allocates and launches under
torch.cuda.device(device), and makes the device index part of the cache key of the twodevice-property helpers in
tile_kernels/config.py. A second commit extends the guard to fivesibling entry points that allocated correctly but still compiled and launched unguarded, and
fixes the no-argument paths of the property cache.
Fixes #28.
Affected allocation sites (default branch at 36d9e45)
get_fused_mappingtile_kernels/moe/get_fused_mapping_kernel.py:208-215(8 tensors)expand_to_fusedtile_kernels/moe/expand_to_fused_kernel.py:126expand_to_fused_with_sftile_kernels/moe/expand_to_fused_kernel.py:192-193reduce_fusedtile_kernels/moe/reduce_fused_kernel.py:115group_counttile_kernels/moe/group_count_kernel.py:66aux_fitile_kernels/moe/aux_fi_kernel.py:69normalize_weighttile_kernels/moe/normalize_weight_kernel.py:64-65normalize_weightis not listed in the issue but carries the same pattern in the same package, soit is fixed here too.
Newly guarded compile/launch sites (second commit)
These five entry points already allocated with
device=<input>.deviceortorch.empty_like, sothey had no wrong-device allocation, but their kernel was built and launched with no device guard.
Line numbers below are for the version in this PR.
inplace_unique_group_indicestile_kernels/moe/inplace_unique_group_indices_kernel.py:66-74(alsoget_num_sms(device.index))topk_gatetile_kernels/moe/topk_gate_kernel.py:86-92top2_sum_gatetile_kernels/moe/top2_sum_gate_kernel.py:408-422mask_indices_by_tptile_kernels/moe/mask_indices_by_tp_kernel.py:68-76topk_sum_and_topk_group_idxtile_kernels/moe/topk_sum_and_topk_group_idx_kernel.py:95-104The primary input chosen per entry point:
group_indicesforinplace_unique_group_indices,scoresfortopk_gate(its only tensor input),logitsfortop2_sum_gate(first argument;bias,mask,fix_routing_mask,to_physical_map,logical_countandunmapped_topk_idxareper-token/per-expert companions of
logits, and both outputs are already allocated withdevice=logits.device),indicesformask_indices_by_tp, andscoresfortopk_sum_and_topk_group_idx(its only tensor input, and the device its output is alreadyallocated from).
In
topk_sum_and_topk_group_idxthenum_tokens == 0early return sits between the build and thelaunch. It is kept verbatim and stays inside the guard block, so the guard covers the build and the
launch on the non-empty path and the empty path returns the empty tensor while still exiting the
context manager; no line was reordered and no condition was rewritten.
Mechanism
torch.empty(..., device='cuda')binds totorch.cuda.current_device(). With acuda:1input and
cuda:0current, every output lands oncuda:0.get_num_sms()readtorch.cuda.get_device_properties(torch.cuda.current_device())behind azero-argument
functools.lru_cache(maxsize=None), so the SM count was resolved once, forwhichever device happened to be current on the first call, and then reused for every later call
on every other device. This value sizes a real tensor (
num_experts_per_sm) and a compile-timekernel argument (
num_smsgrid size), not just a heuristic.so even a correctly allocated output can be paired with a kernel built/launched for another
device.
Fix
get_device_guard(device)intile_kernels/utils.py: returnstorch.cuda.device(device)for a CUDA device andcontextlib.nullcontext()otherwise. Theguard is entered before the kernel is built and before the outputs are allocated and launched,
so allocation, compilation and launch all observe the input device as the current device.
device = <primary input>.devicein every affected entry point; every automatically createdtensor now takes
device=device.config.get_device_num_sms/config.get_max_smem_per_sm/config.get_num_smskeep theirpublic signatures but dispatch to private
@functools.lru_cachefunctions keyed by a concreteintdevice index.device_index=Noneis resolved withtorch.cuda.current_device()beforethe cached call, so the key is never
Noneand zero-argument callers (includingset_num_sms()andtile_kernels/testing/generator.py::generate_num_sms()) are per-devicecorrect rather than pinned to the first current device. The MoE callers pass
device.index.(
expand_to_fused*), and inreduce_fusedthe optional preallocatedoutis validated insteadof being moved implicitly, as the issue suggests. In the second commit the optional
sftensoris validated the same way, alongside
token_topk_to_pos,topk_weightsandx_sf.Kernel-cache caveat / needs two GPUs
The guard makes the input device current around allocation, compilation and launch. It does not
give each device its own compiled kernel, because TileLang's JIT wrapper caches per factory
argument tuple. What is established from the source:
tilelang/jit/__init__.pyholdsself._kernel_cache: dict[tuple, Kernel] = {}and fills it fromkey, kernel_args = self.func.parse_args(*args, **kwargs)followed byif kernel is None: kernel = self.compile(...). Thatparse_args(
tilelang/language/eager/builder.py:1577-1590) returns(bound.p1_key, p2_key), wherep1_keycomes fromself._argument_binder.bind(args, kwargs)(the factory arguments) andp2_keyfromtir_temp._parse_phase2_key(**bound.tensor_args, **bound.compile_kwargs)(tensor/compile arguments). Nothing in that tuple identifies a CUDA device. So on a host with
identical GPUs, a second call with the same factory arguments hits the kernel object that was
compiled while the first device was current.
target(hence the architecture) but no explicit device index:tilelang/cache/kernel_cache.py:269-284buildskey_data = {"func": sha256(func_binary), "out_idx": ..., "args_repr": ..., "target": str(target), ...}.Same-architecture devices therefore share a disk entry; different-architecture devices do not.
tilelang/jit/adapter/base.py:72-95resolves thestream from the current device at call time (
get_stream(current_device())/torch.device("cuda", current_device())).What is therefore not established, and is the reason this PR no longer claims that
"allocation, compilation and launch all agree on the input device": whether launching a kernel
object that was compiled or loaded while device A was current, now with device B current, succeeds
or fails. That is a runtime question about per-context modules, not a cache-key question, and
answering it needs two GPUs.
Consequence for the code in this PR, stated precisely:
kernel.
topk_gateis the clearest case:get_topk_gate_kernel(num_experts, num_topk)has nodevice-derived argument, so a second device reuses the first device's kernel object.
get_fused_mappingandinplace_unique_group_indices,num_smsis a factory argument andis now device-specific (
get_num_sms(device.index)), so devices with different SM counts getseparate kernels, while identical GPUs still share one.
What a reviewer with a 2-GPU machine should test (this is the open item):
cuda:0current, call each of the twelve guarded entry points oncuda:0inputs, so thefirst kernel object for each factory argument tuple is built there.
torch.cuda.set_device(1)) and call the same entry points with thesame shapes and dtypes but
cuda:1inputs.cuda:1,whether the result matches the
cuda:0result numerically, and whethertorch.cuda.synchronize()succeeds on both devices afterwards.in-memory cache case (1) from the disk cache case (2).
Redesigning that cache (device in the factory arguments, a
target/device-scoped key, or aper-device kernel registry) is deliberately out of scope for this PR: it changes TileLang, not the
call sites, and its correct shape depends on the answer to the two-GPU question above. It is
offered as a follow-up.
Why it is safe
x.deviceis exactly the devicedevice='cuda'would have resolved to,torch.cuda.device(x.device)is a no-op when the device is already current, andget_num_sms(x.device.index)returns the same cached integer asget_num_sms(). Allocation,layout, dtype, shapes, kernel arguments and return values are unchanged.
set_num_sms()override keeps its exact previous semantics: a process-global override thatwins over the per-device value. Its bound is still checked against the current device only, and
the value is not re-validated per device when it is later used; that limitation is now stated in
a comment next to the function rather than being implicit.
GPUs) are unaffected; identical GPUs still hit the same cached SM count because the cache key is
the device index and the values are equal.
tile_kernels/torch/*.pyreference implementations already do.Changes since the first commit
tile_kernels/moe/inplace_unique_group_indices_kernel.py:device = group_indices.device,get_num_sms(device.index)instead ofget_num_sms(), and the kernel build plus launch wrappedin
get_device_guard(device).tile_kernels/moe/topk_gate_kernel.py: build plus launch wrapped inget_device_guard(scores.device). No co-location assertion was added: this module has nodevice-assertion style to follow, and
scoresis its only tensor input.tile_kernels/moe/top2_sum_gate_kernel.py: build plus launch wrapped inget_device_guard(logits.device)(see the primary-input note above).tile_kernels/moe/mask_indices_by_tp_kernel.py: build plus launch wrapped inget_device_guard(indices.device).tile_kernels/moe/topk_sum_and_topk_group_idx_kernel.py: build plus launch wrapped inget_device_guard(scores.device). Thenum_tokens == 0early return stays between them insidethe guard block (see the note above); no line was reordered.
tile_kernels/config.py: the cached helpers are now private functions keyed by a concreteint;Noneis resolved totorch.cuda.current_device()before the cached call, so theno-argument callers named in the review (
set_num_sms(),tile_kernels/testing/generator.py::generate_num_sms()) are per-device correct. Publicsignatures and the
set_num_smsoverride semantics are unchanged.tile_kernels/moe/reduce_fused_kernel.py: the optionalsftensor (a kernel argument whenfp8_format == 'e4m3') is now validated against the input device like the other tensors.Validation
py_compileon all seven changed files: passed (Python 3.12.14), compiled into ascratch directory so the payload files are untouched.
git apply --checkandgit applyof the follow-up patch onto the head commitcfce997: passed, and the applied tree is byte-identical (SHA-256) to the tree that was syntaxchecked; the patch touches exactly those seven files and every other file in the head tree is
byte-identical before and after.
of
blob <len>\0<bytes>equals the tree entry), so the diff base is the reviewed revision andnot a local approximation.
line-length = 150ruff limit; the files are LF-only,UTF-8 without BOM, have no trailing whitespace and keep their trailing newline; no reformatting.
no two-GPU reproduction was executed. The device-guard placement inside TileLang's JIT wrapper,
and the kernel-cache behaviour in the section above, are argued from the source, not measured.
The behaviour of
set_num_sms()as a global override under simultaneous multi-device use isleft as-is and only documented.
Out of scope
The same unqualified
device='cuda'pattern exists outsidemoe/(
tile_kernels/quant/per_channel_cast_fused_kernel.py:198-199,tile_kernels/quant/swiglu_forward_and_per_token_cast_kernel.py:252,tile_kernels/transpose/batched_transpose_kernel.py:115, and thedevice: torch.device = 'cuda'default oftile_kernels/quant/common.py::alloc_scaling_factors,which seven quant call sites rely on). Those are the same class of bug but a separate change set;
this PR stays inside
moe/plus the shared property cache the issue points at.tile_kernels/testing/generator.py(sixdevice='cuda'sites, test-data generation for thecurrent device) and the test modules are also unchanged.
Also out of scope, and offered as a follow-up: the TileLang kernel-cache redesign described in the
caveat above.
Prepared with AI assistance; reviewed before submission.