Files
2026-10-20 03:55:45 +00:00

167 lines
10 KiB
Python

from __future__ import annotations
from typing import cast
import functools, struct, operator, re, itertools
from tinygrad.device import Allocator, Buffer, BufferSpec, BufferStorage, Compiled, Device
from tinygrad.dtype import dtypes, DType
from tinygrad.helpers import round_up, ceildiv, unwrap, to_tuple, flatten
from tinygrad.engine.realize import get_call_arg_uops
from tinygrad.runtime.autogen import bnxt
from tinygrad.runtime.support.rdma.bnxtdev import BNXTDev, BNXTQP, db_value, send_wqe, recv_wqe, WQE_SIZE, RING_ENTRIES, CQ_ENTRIES, MTU
from tinygrad.runtime.support.hcq2 import unwrap_view, to_name, ins
from tinygrad.runtime.support.memory import AddrSpace, MMIOInterface, VirtMapping, MemoryManager
from tinygrad.runtime.support.system import PCIIfaceBase, PCIAllocationMeta, System
from tinygrad.runtime.support.system import filter_visible_devices
from tinygrad.uop.ops import Ops, PatternMatcher, UOp, UPat
RDMA_CHUNK = 1 << 30 # a wqe length is 32 bits
BNXT_IDS = (0x14e4, ((0xffff, (0x1760,)),), 0x02) # vendor, (mask, device ids), base class
class BNXTIface(PCIIfaceBase):
def __init__(self, dev:RDMADevice, index:int):
super().__init__(dev, index, *BNXT_IDS[:2], vram_bar=2, va_start=MemoryManager.va_allocator.base, va_size=MemoryManager.va_allocator.size,
dev_impl_t=BNXTDev, base_class=BNXT_IDS[2])
def device_fini(self): self.dev_impl.fini()
def storage(self, mem:MMIOInterface, paddrs:list[int], snooped:bool=True) -> BufferStorage: # nic memory any gpu of the node maps
va = MemoryManager.alloc_vaddr(size:=round_up(mem.nbytes, 0x1000), 0x1000)
mapping = VirtMapping(va, size, [(p, 0x1000) for p in paddrs], AddrSpace.SYS, uncached=True, snooped=snooped)
return BufferStorage(va, PCIAllocationMeta(mapping, True), mem)
def buffer(self, mem:MMIOInterface, paddrs:list[int], snooped:bool=True) -> Buffer:
return Buffer(self.dev.device, mem.nbytes, opaque=self.storage(mem, paddrs, snooped))
@functools.cached_property
def doorbell(self) -> Buffer:
off = self.dev_impl.db_off & ~0xfff
return self.buffer(self.pci_dev.map_bar(2, off=off, size=0x1000), [self.pci_dev.bar_info(2)[0] + off], snooped=False)
class BNXTAllocator(Allocator):
def _alloc(self, size:int, options:BufferSpec) -> BufferStorage: # sysmem: the counters, the batch slots, the timeline
return self.dev.iface.storage(*self.dev.iface.pci_dev.alloc_sysmem(round_up(size, 0x1000)))
def _copyin(self, dest:BufferStorage, src:memoryview): unwrap(dest.host)[:len(src)] = src
def _map(self, buf:Buffer) -> BufferStorage: # a memory region over the buffer's pages, keyed at the gpu virtual address
iface = getattr(Device[buf.device], "iface", None)
if not isinstance(iface, PCIIfaceBase) or iface.peer_group != self.dev.peer_group: raise RuntimeError("RDMA requires memory on its node")
mapping = buf.meta.mapping # the nic reaches vram over pcie: the bar, even where gpus reach each other over xgmi
paddrs = mapping.paddrs if mapping.aspace is AddrSpace.SYS else PCIIfaceBase.p2p_paddrs(iface, mapping.paddrs)[0]
align = buf._buf | functools.reduce(operator.or_, (p | s for p, s in paddrs)) # every address a multiple of the page: fewer pbl entries
log_page = max(l for l in (12, 13, 16, 18, 20, 21, 22, 30) if not align & ((1 << l) - 1)) # the page sizes the nic has
key = self.dev.iface.dev_impl.register_mem([p + off for p, size in paddrs for off in range(0, size, 1 << log_page)],
mapping.size, log_page, va=buf._buf)
return BufferStorage(key, key)
def _offset(self, buf, size:int, offset:int): return buf
def _unmap(self, storage:BufferStorage): self.dev.iface.dev_impl.unregister_mem(storage.meta)
@functools.cache
def rdma_nic_for(dev, anchor) -> RDMADevice|None:
def node(s:str) -> str: return ":".join(s.split(":")[:3]) if s.startswith("remote:") else ""
def bus(s:str) -> int: return int(re.findall(r":([0-9a-f]{2}):[0-9a-f]{2}\.[0-7]", s)[-1], 16)
gpu = dev.iface.pci_dev.pcibus
try: nics = [(i, n) for i, (_, n) in enumerate(filter_visible_devices(System.list_devices(*BNXT_IDS), "RDMA")) if node(n) == node(gpu)]
except RuntimeError: return None # no pcie on this machine
# the closest nic to the anchor on dev's node
return cast(RDMADevice, Device[f"RDMA:{min(nics, key=lambda x: abs(bus(x[1]) - bus(anchor.iface.pci_dev.pcibus)))[0]}"]) if nics else None
class RDMADevice(Compiled):
ifaces = [BNXTIface]
def __init__(self, device:str):
self.iface = self._select_iface(device)
super().__init__(device, BNXTAllocator(self), [], None)
# *****************
# UOps implementation
@functools.cache
def rdma_qp(pair:tuple[str, str]) -> dict[str, BNXTQP]:
# one qp per gpu pair
nics = [unwrap(rdma_nic_for(Device[d], Device[min(pair)])) for d in pair]
qps = {nic.device: BNXTQP(nic.iface.dev_impl) for nic in nics}
for nic, q in zip(nics, qps.values()):
bufs = {name: nic.iface.buffer(getattr(q, name).ring, getattr(q, name).paddrs) for name in ("sq", "rq", "scq", "rcq")}
bufs |= {name: Buffer(nic.device, 8, initial_value=bytes(8)) for name in ("sq_seq", "rq_seq", "psn")} | {"db": nic.iface.doorbell}
Compiled.pm_bufferize += PatternMatcher([(UPat(Ops.ALLOC, tag=to_name(nic.device, *pair, n)), lambda b=b: b) for n, b in bufs.items()])
for a, b in (nics, nics[::-1]): qps[a.device].connect(qps[b.device].qpn, b.iface.dev_impl.local_gid, b.iface.dev_impl.mac)
return qps
def rdma_mem(nic:str, pair:tuple[str, str], name:str, size:int, dtype:DType=dtypes.uint8) -> UOp:
return UOp.alloc((size,), dtype, 0, device=nic).rtag(to_name(nic, *pair, name))
def rdma_ring(nic:str, pair:tuple[str, str], is_recv:bool) -> UOp:
return rdma_mem(nic, pair, "rq" if is_recv else "sq", RING_ENTRIES * (WQE_SIZE if is_recv else WQE_SIZE + 8))
def rdma_cq(nic:str, pair:tuple[str, str], is_recv:bool) -> UOp: return rdma_mem(nic, pair, "rcq" if is_recv else "scq", CQ_ENTRIES * 32)
def rdma_seq(nic:str, pair:tuple[str, str], is_recv:bool) -> UOp: return rdma_mem(nic, pair, "rq_seq" if is_recv else "sq_seq", 1, dtypes.uint64)
def rdma_psn(nic:str, pair:tuple[str, str]) -> UOp: return rdma_mem(nic, pair, "psn", 1, dtypes.uint64) # the next psn of the sends
def rdma_db(nic:str, pair:tuple[str, str]) -> UOp: return rdma_mem(nic, pair, "db", 0x1000)
def rdma_wire(call:UOp) -> UOp|None:
if call.op is not Ops.CALL or call.src[0].op is not Ops.STORE: return None
return next((b for b in get_call_arg_uops(call) if to_tuple(b.device)[0].startswith("RDMA")), None)
def is_rdma(call:UOp) -> bool: return rdma_wire(call) is not None
def queue_of(call:UOp) -> tuple[tuple[str, str], bool]:
(dst, src), wire = get_call_arg_uops(call), unwrap(rdma_wire(call))
gpu = to_tuple((dst if wire is src else src).device)[0]
return (min(gpu, wire.tag), max(gpu, wire.tag)), wire is src
def rdma_copies(devs:tuple[str, ...], calls:list[UOp]) -> list[list[UOp]]: # the ops of each copy of a submit on one queue
(pair, is_recv), nic = queue_of(calls[0]), cast(RDMADevice, Device[unwrap(rdma_wire(calls[0])).device])
qp = rdma_qp(pair)[nic.device]
ring, cq = rdma_ring(nic.device, pair, is_recv), rdma_cq(nic.device, pair, is_recv)
seq, psn = rdma_seq(nic.device, pair, is_recv), rdma_psn(nic.device, pair)
bufs = [get_call_arg_uops(c)[0 if is_recv else 1] for c in calls]
chunks = [ceildiv(min(RDMA_CHUNK, b.nbytes() - o), MTU) for b in bufs for o in range(0, b.nbytes(), RDMA_CHUNK)]
wqes, packets = len(chunks), sum(chunks)
assert wqes <= min(RING_ENTRIES, CQ_ENTRIES), "a batch posts at most a ring of wqes per pair"
# next slot and psn persist in nic memory. read once per submit and own it
bumps = [seq.index(0).store(seq.index(0).load() + wqes)] + ([] if is_recv else [psn.index(0).store(psn.index(0).load() + packets)])
i = UOp.range(wqes, next(UOp.unique_num), dtype=dtypes.uint64)
psns = UOp(Ops.BINARY, arg=struct.pack(f"<{wqes + 1}I", *itertools.accumulate(chunks, initial=0))).bitcast(dtypes.uint32)
n, p = seq.after(*bumps).index(0).load() - wqes + i, psn.after(*bumps).index(0).load() - packets + psns[i]
ring_addr, cq_addr = ring.getaddr(devs), cq.getaddr(devs)
db = rdma_db(nic.device, pair).getaddr(devs) + (nic.iface.dev_impl.db_off & 0xfff)
ring_db = db_value(qp.qpn, bnxt.DBC_DBC_TYPE_RQ if is_recv else bnxt.DBC_DBC_TYPE_SQ, 0, 0)
cq_db = db_value(qp.rcq_id if is_recv else qp.scq_id, bnxt.DBC_DBC_TYPE_CQ, 0, 0)
copies = []
for buf in bufs:
ops:list[UOp] = []
for off in range(0, buf.nbytes(), RDMA_CHUNK): # a wqe per chunk, each completed
size = min(RDMA_CHUNK, buf.nbytes() - off)
# sdma fills in the wqe
hdr, key = struct.unpack("<8I", (recv_wqe if is_recv else send_wqe)(0, 0, size)[:32]), unwrap_view(buf)[0].getaddr(nic.device)
ops += [ins("write", ring_addr + (n % RING_ENTRIES) * WQE_SIZE, *hdr, buf.getaddr(devs) + off, key.cast(dtypes.uint32), size)]
# a send also fills in its msn entry: the slot, the psn after it (a psn per packet), its first psn
if not is_recv: ops += [ins("write", ring_addr + RING_ENTRIES * WQE_SIZE + (n % RING_ENTRIES) * 8,
((n % RING_ENTRIES) << 48) | (((p + psns[i + 1] - psns[i]) & 0xffffff) << 24) | (p & 0xffffff))]
# rings the doorbell: the slot after the wqe and the epoch of its pass
ops += [ins("write", db, ((n + 1) % RING_ENTRIES | ((n + 1) // RING_ENTRIES & 1) << bnxt.BNXT_QPLIB_DBR_EPOCH_SHIFT) | ring_db)]
# waits for the cqe, acks the cq
ops += [ins("wait_eq", cq_addr + (n % CQ_ENTRIES) * 32 + 24, (n // CQ_ENTRIES & 1 ^ 1 | (2 if is_recv else 0)).cast(dtypes.uint16)),
ins("write", db, ((n + 1) % CQ_ENTRIES | ((n + 1) // CQ_ENTRIES & 1) << bnxt.BNXT_QPLIB_DBR_EPOCH_SHIFT) | cq_db)]
# and invalidate the gpu caches on recv
copies.append(ops + ([ins("barrier")] if is_recv else []))
return copies
# *****************
# encode rewrite
def rdma_submit(ctx, submit:UOp, arg:UOp) -> UOp|None:
lin = arg.without_after
if not (queues:={i: queue_of(u) for i, u in enumerate(lin.src) if is_rdma(u)}): return None
ops = [[u] for u in lin.src]
for q in dict.fromkeys(queues.values()):
positions = [i for i in queues if queues[i] == q]
for i, copy_ops in zip(positions, rdma_copies(lin.arg[0], [lin.src[i] for i in positions])): ops[i] = copy_ops
return submit.replace(src=(submit.body, lin.replace(src=tuple(flatten(ops))).after(*arg.src[1:] if arg.op is Ops.AFTER else ())))
pm_rdma_encode = PatternMatcher([(UPat(Ops.CALL, src=(UPat(Ops.CUSTOM_FUNCTION), UPat(Ops.LINEAR).or_after("arg")), name="submit"), rdma_submit)])