loopyt ec5f37821d fix(h20): let the reference backend sync, and stop the reference raising under the guard
Running end to end surfaced two things the per-kernel A/B could not.

The engine wraps the forward pass in `forbid_cuda_sync()`, which sets torch's sync debug mode to
"error". The compiled kernels never sync; the torch references do, and not by oversight:

* `tile_kernels/torch/cast.py` built `torch.tensor(-1.0, device="cuda")` (and three more like it)
  for use as a `torch.where` operand. Constructing a device tensor from a Python scalar is a
  host-to-device copy, so it syncs -- and it is unnecessary, because `torch.where` accepts a plain
  scalar and passes it to the kernel. Patched via `patch_torch_ref_sync.py` (idempotent,
  reversible), which is a fix worth having on its own: the same cast raises for any caller that
  holds the device clean.
* `get_mapping_from_psum` reads the expert prefix-sum to the host to compute alignment ranges in
  Python, and `moe_topk_gate_forward` compacts rows with `nonzero()` and branches on `.any()`.
  Those cannot be made sync-free without rewriting how the reference addresses, which is the wrong
  trade for a fallback whose job is being runnable. So each compat module wraps its reference
  module once and every call through it runs inside `allow_cuda_sync()`.

Not done, deliberately: `set_sync_debug_mode("default")` process-wide. That would also stop the
guard catching accidental syncs in the compiled path, which is what it is for.

Also fixed: my own `bool(force_random.any())` in the gate adapter was itself a sync, outside the
reference, so it raised before the adapter could decide anything.

End to end now completes: exit=0, all 8 DP ranks agree, coherent answers. It is not token-identical
to the compiled path -- 16/16 samples differ, with a control run confirming the harness is
deterministic (two cuda runs are bit-identical), so the difference is real. Recorded in the doc as
the next thing to attribute rather than papered over.
2026-10-24 19:10:49 +08:00

84 lines
2.9 KiB
Python

"""Let the reference implementations sync, without turning the guard off everywhere.
The engine wraps the forward pass in `forbid_cuda_sync()`, which sets torch's sync debug mode to
"error"; one blocking copy or one `nonzero()` raises. The compiled kernels never sync, so the guard
is a real property of the production path and worth keeping.
The torch references cannot honour it, and not because of an oversight that could be patched away:
* `get_mapping_from_psum` reads `psum_num_tokens_per_expert` to the host to compute per-expert
alignment ranges in Python.
* `moe_topk_gate_forward` compacts valid rows with `active.nonzero()` and branches on
`normal_mask.any()`, both of which need the result on the host.
Making those sync-free means rewriting how the reference does its addressing. That is the wrong
trade: the reference exists to make the model runnable and checkable, and it is not the path whose
performance matters.
So each compat module wraps its reference module once:
_ref = reference_module(tile_kernels.torch)
and every call through it runs inside `allow_cuda_sync()`. The wrapper is at the module boundary
rather than in each branch, so no call site knows about it and a newly used reference entry point
is covered automatically.
Deliberately *not* done: `torch.cuda.set_sync_debug_mode("default")` for the whole process. That
would also stop the guard from catching accidental syncs in the compiled path, which is exactly
what it is for.
"""
from __future__ import annotations
import functools
from collections.abc import Callable
from typing import Any
try: # The guard lives in deepinfer; a bare install of this package would not have it.
from deepinfer.utils import allow_cuda_sync
_HAVE_GUARD = True
except Exception: # pragma: no cover
_HAVE_GUARD = False
def _wrap(fn: Callable[..., Any]) -> Callable[..., Any]:
if not _HAVE_GUARD:
return fn
@functools.wraps(fn)
def wrapper(*args: Any, **kwargs: Any) -> Any:
with allow_cuda_sync():
return fn(*args, **kwargs)
return wrapper
class _ReferenceProxy:
"""Attribute access that grants sync permission for every callable it returns."""
__slots__ = ("_module",)
def __init__(self, module: Any) -> None:
object.__setattr__(self, "_module", module)
def __getattr__(self, name: str) -> Any:
value = getattr(object.__getattribute__(self, "_module"), name)
return _wrap(value) if callable(value) else value
def __repr__(self) -> str:
return f"reference_module({object.__getattribute__(self, '_module')!r})"
def reference_module(module: Any) -> Any:
"""Wrap a `tile_kernels.torch` submodule so its calls may sync."""
return _ReferenceProxy(module)
if _HAVE_GUARD:
allow_sync = allow_cuda_sync
else: # pragma: no cover - no guard installed, nothing to lift
import contextlib
allow_sync = contextlib.nullcontext