whn09 42e05844d1 Neuron: nkilib rotational_topk for the sampler; single-host shared engram tables (#27)
- deep_select.topk on a NeuronCore with k >= 1024 (the sampler's force_topk
  4096 over the 129280 vocab) uses nkilib's rotational_topk: torch.topk with
  that k does not compile on the device (NCC_IXCG857). 8.4 ms on one core,
  values identical to torch.topk.
- EngramBuffer on Neuron: the ranks of a trn2 instance share one host, so the
  table shards are written into /dev/shm-backed tables (unlinked once mapped)
  and every rank gathers its own rows: no all-to-all and no host sync on split
  sizes per fetch. DEEPINFER_NEURON_ENGRAM_SHARED=0 restores the sharded path.
  Host EP=2 tiny: logits bitwise equal to the sharded path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 07:23:32 +08:00

153 lines
7.1 KiB
Python

"""Pure-torch drop-in for ``deep_select.interface`` (upstream:
``vendor/DeepSelect/deep_select/interface.py``).
Output contract taken from the upstream docstring and the checks in
``vendor/DeepSelect/tests/test.py`` (lines 70-148):
- ``output_val[i][j] == input[i][output_idx[i][j] - output_idx_offset[i]]`` bitwise, for the
``min(end[i], topk)`` valid positions of a row;
- indices are unique and inside ``[0, end[i])``;
- ``min(selected) >= max(unselected)`` within the valid range;
- rows with fewer valid entries than ``topk`` gather their valid results at the beginning and pad
the rest with ``value_oob_fill_value`` / ``idx_oob_fill_value`` (the fill index is *not*
shifted by ``output_idx_offset``);
- ``sorted=True`` returns values in descending order, ``sorted_index=True`` returns indices in
ascending order (upstream never sets both);
- a row containing NaN inside its valid range has its whole ``output_idx`` row filled with
``0x3F3F3F3F`` (the Ascend behaviour, ``csrc/ascend_kernels/kernel.asc:190``).
Deviation: ``abort_when_nan_found=True`` aborts the kernel upstream; aborting here would need a
host sync, so the NaN sentinel fill is used for both settings.
"""
from __future__ import annotations
import functools
from typing import Optional, Tuple
import torch
_NAN_SENTINEL = 0x3F3F3F3F
@functools.lru_cache(maxsize=1)
def get_stride_requirement() -> Tuple[int, int]:
"""(input, output) stride requirement in bytes. Ascend values
(``vendor/DeepSelect/csrc/structs.h:39-40``); CUDA requires 1024 for the input."""
return (32, 32)
def topk(
input: torch.Tensor,
topk: int,
sorted: bool = False,
begin: Optional[torch.Tensor] = None,
end: Optional[torch.Tensor] = None,
indices_type: torch.dtype = torch.int64,
sorted_index: bool = False,
hint: Optional[torch.Tensor] = None,
output_idx: Optional[torch.Tensor] = None,
output_idx_offset: Optional[torch.Tensor] = None,
idx_oob_fill_value: int = 2147483647,
value_oob_fill_value: float = float("-inf"),
return_value: bool = True,
abort_when_nan_found: bool = True,
) -> Tuple[Optional[torch.Tensor], torch.Tensor]:
"""Row-wise top-k selection; see the module docstring for the output contract.
Args:
input: (b, vocab_size), bfloat16 or float32.
topk: number of elements selected per row.
sorted: return ``output_val`` sorted descending.
begin: not supported (must be None), as upstream.
end: (b,) int32, exclusive right bound of each row's valid range (default vocab_size).
indices_type: torch.int32 or torch.int64.
sorted_index: return ``output_idx`` sorted ascending.
hint: not supported (must be None), as upstream.
output_idx: (b, topk) tensor of ``indices_type`` to write the indices into, in place.
output_idx_offset: (b,) int32 added to every valid index.
idx_oob_fill_value / value_oob_fill_value: padding for rows with fewer than ``topk`` valid
entries.
return_value: when False the first element of the result tuple is None.
abort_when_nan_found: see the module docstring (sentinel fill either way here).
"""
assert begin is None, "`begin` is not supported now"
assert hint is None, "`hint` is not supported now"
assert input.dim() == 2, f"input must be 2D, got {tuple(input.shape)}"
assert indices_type in (torch.int32, torch.int64), f"unsupported indices_type {indices_type}"
assert topk > 0
device = input.device
b, vocab_size = input.shape
# --- valid range -------------------------------------------------------
cols = torch.arange(vocab_size, device=device, dtype=torch.int32).view(1, vocab_size)
if end is None:
valid_len = torch.full((b, 1), vocab_size, device=device, dtype=torch.int32)
oob_cols = None
else:
assert end.numel() == b, (end.shape, b)
valid_len = end.reshape(b, 1).to(torch.int32)
oob_cols = cols >= valid_len # [b, vocab_size]
# --- select ------------------------------------------------------------
# Out-of-range columns must never be selected: push them below every real value.
scores = input.float()
if oob_cols is not None:
scores = torch.where(oob_cols, torch.full_like(scores, float("-inf")), scores)
# NaN would otherwise win every comparison in torch.topk; keep NaN rows detectable but
# deterministic by sorting NaN to the bottom (the row is overwritten with the sentinel below).
is_nan = scores != scores
scores = torch.where(is_nan, torch.full_like(scores, float("-inf")), scores)
k_eff = min(topk, vocab_size)
# torch.topk(sorted=True) returns values descending, which is what `sorted=True` asks for; it
# is used unconditionally so the result is deterministic for every flag combination.
from deepinfer.neuron_ops import nki_topk # NEURON: torch.topk with a large k does not compile on the device
if nki_topk.supported(scores, k_eff):
idx = nki_topk.topk(scores, k_eff)
else:
idx = torch.topk(scores, k_eff, dim=-1, sorted=True).indices
idx = idx.to(torch.int64)
counts = torch.clamp(valid_len.to(torch.int64), max=topk) # [b, 1]
kcols = torch.arange(topk, device=device, dtype=torch.int64).view(1, topk)
selected_mask = kcols < counts # [b, topk]
if k_eff < topk:
pad = torch.zeros((b, topk - k_eff), dtype=torch.int64, device=device)
idx = torch.cat([idx, pad], dim=1)
if sorted_index:
# Keep the valid prefix in ascending index order; the padding stays at the end.
# Any value above every real index works as the sort sentinel. Keep it in int32 range: the neuron
# compiler narrows int64 (--implicit-integer-downcast), which turned int64.max into -1 and sorted the
# padding in front of the selected indices for rows with end < topk.
big = torch.full_like(idx, vocab_size)
idx = torch.sort(torch.where(selected_mask, idx, big), dim=1).values
idx = torch.where(selected_mask, idx, torch.zeros_like(idx))
# --- values ------------------------------------------------------------
out_val = None
if return_value:
gathered = input.gather(1, idx) # bitwise equal to the input elements
fill = torch.full_like(gathered, value_oob_fill_value)
out_val = torch.where(selected_mask, gathered, fill)
# --- indices -----------------------------------------------------------
out_idx = idx
if output_idx_offset is not None:
out_idx = out_idx + output_idx_offset.reshape(b, 1).to(torch.int64)
out_idx = torch.where(selected_mask, out_idx, torch.full_like(out_idx, idx_oob_fill_value))
# NaN rows: the whole index row becomes the sentinel.
nan_rows = is_nan.any(dim=1, keepdim=True)
out_idx = torch.where(nan_rows, torch.full_like(out_idx, _NAN_SENTINEL), out_idx)
out_idx = out_idx.to(indices_type)
if output_idx is not None:
assert output_idx.dtype == indices_type
assert tuple(output_idx.shape) == (b, topk), (output_idx.shape, (b, topk))
output_idx.copy_(out_idx)
out_idx = output_idx
return out_val, out_idx