forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Prebuilt Release @ ab07000
This commit is contained in:
0
tinygrad_repo/tinygrad/runtime/support/__init__.py
Normal file
0
tinygrad_repo/tinygrad/runtime/support/__init__.py
Normal file
334
tinygrad_repo/tinygrad/runtime/support/am/amdev.py
Normal file
334
tinygrad_repo/tinygrad/runtime/support/am/amdev.py
Normal file
@@ -0,0 +1,334 @@
|
||||
from __future__ import annotations
|
||||
import ctypes, collections, dataclasses, functools, hashlib, array
|
||||
from tinygrad.helpers import mv_address, getenv, DEBUG, lo32, hi32, fetch_fw
|
||||
from tinygrad.runtime.autogen import pci
|
||||
from tinygrad.runtime.autogen.am import am, fw
|
||||
from tinygrad.runtime.support.amd import AMDReg, import_module, import_asic_regs
|
||||
from tinygrad.runtime.support.memory import TLSFAllocator, MemoryManager, AddrSpace
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
from tinygrad.runtime.support.am.ip import AM_IP, AM_SOC, AM_GMC, AM_IH, AM_PSP, AM_SMU, AM_GFX, AM_SDMA
|
||||
|
||||
AM_DEBUG = getenv("AM_DEBUG", 0)
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AMRegister(AMDReg):
|
||||
adev:AMDev
|
||||
|
||||
def read(self, inst=0): return self.adev.rreg(self.addr[inst])
|
||||
def read_bitfields(self, inst=0) -> dict[str, int]: return self.decode(self.read(inst=inst))
|
||||
|
||||
def write(self, _am_val:int=0, inst=0, **kwargs): self.adev.wreg(self.addr[inst], _am_val | self.encode(**kwargs))
|
||||
|
||||
def update(self, inst=0, **kwargs): self.write(self.read(inst=inst) & ~self.fields_mask(*kwargs.keys()), inst=inst, **kwargs)
|
||||
|
||||
class AMFirmware:
|
||||
def __init__(self, adev):
|
||||
self.adev = adev
|
||||
def fmt_ver(hwip): return '_'.join(map(str, adev.ip_ver[hwip]))
|
||||
|
||||
# Load SOS firmware
|
||||
self.sos_fw = {}
|
||||
|
||||
blob, sos_hdr = self.load_fw(f"psp_{fmt_ver(am.MP0_HWIP)}_sos.bin", versioned_header='struct_psp_firmware_header')
|
||||
fw_bin = sos_hdr.psp_fw_bin
|
||||
|
||||
for fw_i in range(sos_hdr.psp_fw_bin_count):
|
||||
fw_bin_desc = am.struct_psp_fw_bin_desc.from_address(ctypes.addressof(fw_bin) + fw_i * ctypes.sizeof(am.struct_psp_fw_bin_desc))
|
||||
ucode_start_offset = fw_bin_desc.offset_bytes + sos_hdr.header.ucode_array_offset_bytes
|
||||
self.sos_fw[fw_bin_desc.fw_type] = blob[ucode_start_offset:ucode_start_offset+fw_bin_desc.size_bytes]
|
||||
|
||||
# Load other fw
|
||||
self.ucode_start: dict[str, int] = {}
|
||||
self.descs: list[tuple[list[int], memoryview]] = []
|
||||
|
||||
# SMU firmware
|
||||
if adev.ip_ver[am.MP1_HWIP] != (13,0,12):
|
||||
blob, hdr = self.load_fw(f"smu_{fmt_ver(am.MP1_HWIP)}.bin", versioned_header="struct_smc_firmware_header")
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (11,0,0):
|
||||
self.smu_psp_desc = self.desc(blob, hdr.v1_0.header.ucode_array_offset_bytes, hdr.v1_0.header.ucode_size_bytes, am.GFX_FW_TYPE_SMU)
|
||||
else:
|
||||
p2stables = (am.struct_smc_soft_pptable_entry * hdr.pptable_count).from_buffer(blob[hdr.pptable_entry_offset:])
|
||||
for p2stable in p2stables:
|
||||
if p2stable.id == (__P2S_TABLE_ID_X:=0x50325358):
|
||||
self.descs += [self.desc(blob, p2stable.ppt_offset_bytes, p2stable.ppt_size_bytes, am.GFX_FW_TYPE_P2S_TABLE)]
|
||||
|
||||
# SDMA firmware
|
||||
blob, hdr = self.load_fw(f"sdma_{fmt_ver(am.SDMA0_HWIP)}.bin", versioned_header="struct_sdma_firmware_header")
|
||||
if hdr.header.header_version_major == 1:
|
||||
self.descs += [self.desc(blob, hdr.header.ucode_array_offset_bytes, hdr.header.ucode_size_bytes, am.GFX_FW_TYPE_SDMA0,
|
||||
am.GFX_FW_TYPE_SDMA1, am.GFX_FW_TYPE_SDMA2, am.GFX_FW_TYPE_SDMA3)]
|
||||
elif hdr.header.header_version_major == 2:
|
||||
self.descs += [self.desc(blob, hdr.ctl_ucode_offset, hdr.ctl_ucode_size_bytes, am.GFX_FW_TYPE_SDMA_UCODE_TH1)]
|
||||
self.descs += [self.desc(blob, hdr.header.ucode_array_offset_bytes, hdr.ctx_ucode_size_bytes, am.GFX_FW_TYPE_SDMA_UCODE_TH0)]
|
||||
else: self.descs += [self.desc(blob, hdr.header.ucode_array_offset_bytes, hdr.ucode_size_bytes, am.GFX_FW_TYPE_SDMA_UCODE_TH0)]
|
||||
|
||||
# PFP, ME, MEC firmware
|
||||
for (fw_name, fw_cnt) in ([('PFP', 1), ('ME', 1)] if self.adev.ip_ver[am.GC_HWIP] >= (12,0,0) else []) + [('MEC', 1)]:
|
||||
blob, hdr = self.load_fw(f"gc_{fmt_ver(am.GC_HWIP)}_{fw_name.lower()}.bin", versioned_header="struct_gfx_firmware_header")
|
||||
|
||||
ucode_off = hdr.header.ucode_array_offset_bytes
|
||||
if hdr.header.header_version_major == 1:
|
||||
# Code
|
||||
self.descs += [self.desc(blob, ucode_off, hdr.header.ucode_size_bytes - hdr.jt_size * 4, getattr(am, f'GFX_FW_TYPE_CP_{fw_name}'))]
|
||||
# JT
|
||||
self.descs += [self.desc(blob, ucode_off + hdr.jt_offset * 4, hdr.jt_size * 4, getattr(am, f'GFX_FW_TYPE_CP_{fw_name}_ME1'))]
|
||||
else:
|
||||
# Code
|
||||
self.descs += [self.desc(blob, ucode_off, hdr.ucode_size_bytes, getattr(am, f'GFX_FW_TYPE_RS64_{fw_name}'))]
|
||||
# Stack
|
||||
stack_fws = [getattr(am, f'GFX_FW_TYPE_RS64_{fw_name}_P{fwnum}_STACK') for fwnum in range(fw_cnt)]
|
||||
self.descs += [self.desc(blob, hdr.data_offset_bytes, hdr.data_size_bytes, *stack_fws)]
|
||||
self.ucode_start[fw_name] = hdr.ucode_start_addr_lo | (hdr.ucode_start_addr_hi << 32)
|
||||
|
||||
# IMU firmware
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (11,0,0):
|
||||
blob, hdr = self.load_fw(f"gc_{fmt_ver(am.GC_HWIP)}_imu.bin", am.struct_imu_firmware_header_v1_0)
|
||||
imu_i_off, imu_i_sz, imu_d_sz = hdr.header.ucode_array_offset_bytes, hdr.imu_iram_ucode_size_bytes, hdr.imu_dram_ucode_size_bytes
|
||||
self.descs += [self.desc(blob, imu_i_off, imu_i_sz, am.GFX_FW_TYPE_IMU_I), self.desc(blob, imu_i_off+imu_i_sz, imu_d_sz, am.GFX_FW_TYPE_IMU_D)]
|
||||
|
||||
# RLC firmware
|
||||
blob, hdr0, hdr1, hdr2, hdr3 = self.load_fw(f"gc_{fmt_ver(am.GC_HWIP)}_rlc.bin", am.struct_rlc_firmware_header_v2_0,
|
||||
am.struct_rlc_firmware_header_v2_1, am.struct_rlc_firmware_header_v2_2, am.struct_rlc_firmware_header_v2_3)
|
||||
|
||||
if hdr0.header.header_version_minor == 1:
|
||||
for mem,fmem in [('LIST_SRM_CNTL', 'list_cntl'), ('LIST_GPM_MEM', 'list_gpm'), ('LIST_SRM_MEM', 'list_srm')]:
|
||||
off, sz = getattr(hdr1, f'save_restore_{fmem}_offset_bytes'), getattr(hdr1, f'save_restore_{fmem}_size_bytes')
|
||||
self.descs += [self.desc(blob, off, sz, getattr(am, f'GFX_FW_TYPE_RLC_RESTORE_{mem}'))]
|
||||
|
||||
if hdr0.header.header_version_minor >= 2:
|
||||
for mem,fmem in [('IRAM', 'iram'), ('DRAM_BOOT', 'dram')]:
|
||||
off, sz = getattr(hdr2, f'rlc_{fmem}_ucode_offset_bytes'), getattr(hdr2, f'rlc_{fmem}_ucode_size_bytes')
|
||||
self.descs += [self.desc(blob, off, sz, getattr(am, f'GFX_FW_TYPE_RLC_{mem}'))]
|
||||
|
||||
if hdr0.header.header_version_minor == 3:
|
||||
for mem in ['P', 'V']:
|
||||
off, sz = getattr(hdr3, f'rlc{mem.lower()}_ucode_offset_bytes'), getattr(hdr3, f'rlc{mem.lower()}_ucode_size_bytes')
|
||||
self.descs += [self.desc(blob, off, sz, getattr(am, f'GFX_FW_TYPE_RLC_{mem}'))]
|
||||
|
||||
self.descs += [self.desc(blob, hdr0.header.ucode_array_offset_bytes, hdr0.header.ucode_size_bytes, am.GFX_FW_TYPE_RLC_G)]
|
||||
|
||||
def load_fw(self, fname:str, *headers, versioned_header:str|None=None):
|
||||
blob = memoryview(bytearray(fetch_fw("amdgpu", fname, fw.hashes[fname])))
|
||||
if AM_DEBUG >= 1: print(f"am {self.adev.devfmt}: loading firmware {fname}: {hashlib.sha256(blob).hexdigest()}")
|
||||
if versioned_header:
|
||||
chdr = am.struct_common_firmware_header.from_address(mv_address(blob))
|
||||
headers += (getattr(am, versioned_header + f"_v{chdr.header_version_major}_{chdr.header_version_minor}"),)
|
||||
return tuple([blob] + [hdr.from_address(mv_address(blob)) for hdr in headers])
|
||||
|
||||
def desc(self, blob:memoryview, offset:int, size:int, *types:int) -> tuple[list[int], memoryview]: return (list(types), blob[offset:offset+size])
|
||||
|
||||
class AMPageTableEntry:
|
||||
def __init__(self, adev, paddr, lv): self.adev, self.paddr, self.lv, self.entries = adev, paddr, lv, adev.vram.view(paddr, 0x1000, fmt='Q')
|
||||
|
||||
def set_entry(self, entry_id:int, paddr:int, table=False, uncached=False, aspace=AddrSpace.PHYS, snooped=False, frag=0, valid=True):
|
||||
is_sys = aspace is AddrSpace.SYS
|
||||
if aspace is AddrSpace.PHYS: paddr = self.adev.paddr2xgmi(paddr)
|
||||
assert paddr & self.adev.gmc.address_space_mask == paddr, f"Invalid physical address {paddr:#x}"
|
||||
self.entries[entry_id] = self.adev.gmc.get_pte_flags(self.lv, table, frag, uncached, is_sys, snooped, valid) | (paddr & 0x0000FFFFFFFFF000)
|
||||
|
||||
def entry(self, entry_id:int) -> int: return self.entries[entry_id]
|
||||
def valid(self, entry_id:int) -> bool: return (self.entries[entry_id] & am.AMDGPU_PTE_VALID) != 0
|
||||
def address(self, entry_id:int) -> int:
|
||||
assert self.entries[entry_id] & am.AMDGPU_PTE_SYSTEM == 0, "should not be system address"
|
||||
return self.adev.xgmi2paddr(self.entries[entry_id] & 0x0000FFFFFFFFF000)
|
||||
def is_page(self, entry_id:int) -> bool: return self.lv == am.AMDGPU_VM_PTB or self.adev.gmc.is_pte_huge_page(self.lv, self.entries[entry_id])
|
||||
def supports_huge_page(self, paddr:int): return self.lv >= am.AMDGPU_VM_PDB2
|
||||
|
||||
class AMMemoryManager(MemoryManager):
|
||||
va_allocator = TLSFAllocator((1 << 44), base=0x200000000000) # global for all devices.
|
||||
|
||||
def on_range_mapped(self):
|
||||
# Invalidate TLB after mappings.
|
||||
self.dev.gmc.flush_tlb(ip='GC', vmid=0)
|
||||
self.dev.gmc.flush_tlb(ip='MM', vmid=0)
|
||||
|
||||
class AMDev:
|
||||
Version = 0xA0000008
|
||||
|
||||
def __init__(self, pci_dev:PCIDevice, reset_mode=False):
|
||||
self.pci_dev, self.devfmt = pci_dev, pci_dev.pcibus
|
||||
self.vram, self.doorbell64, self.mmio = self.pci_dev.map_bar(0), self.pci_dev.map_bar(2, fmt='Q'), self.pci_dev.map_bar(5, fmt='I')
|
||||
|
||||
self._run_discovery()
|
||||
self._build_regs()
|
||||
|
||||
# AM boot Process:
|
||||
# The GPU being passed can be in one of several states: 1. Not initialized. 2. Initialized by amdgpu. 3. Initialized by AM.
|
||||
# The 1st and 2nd states require a full GPU setup since their states are unknown. The 2nd state also requires a mode1 reset to
|
||||
# reinitialize all components.
|
||||
#
|
||||
# The 3rd state can be set up partially to optimize boot time. In this case, only the GFX and SDMA IPs need to be initialized.
|
||||
# To enable this, AM uses a separate boot memory that is guaranteed not to be overwritten. This physical memory is utilized for
|
||||
# all blocks that are initialized only during the initial AM boot.
|
||||
# To determine if the GPU is in the third state, AM uses regSCRATCH_REG7 as a flag.
|
||||
# To determine if the previous AM session finalized correctly, AM uses regSCRATCH_REG6 as a flag.
|
||||
self.is_booting = True # During boot only boot memory can be allocated. This flag is to validate this.
|
||||
self.init_sw(smi_dev=False)
|
||||
|
||||
self.partial_boot = (self.reg("regSCRATCH_REG7").read() == AMDev.Version) and (getenv("AM_RESET", 0) != 1)
|
||||
if self.partial_boot and (self.reg("regSCRATCH_REG6").read() != 0 or self.reg(self.gmc.pf_status_reg("GC")).read() != 0):
|
||||
if DEBUG >= 2: print(f"am {self.devfmt}: Malformed state. Issuing a full reset.")
|
||||
self.partial_boot = False
|
||||
|
||||
# Init hw for IP blocks where it is needed
|
||||
if not self.partial_boot:
|
||||
if self.psp.is_sos_alive() and self.smu.is_smu_alive():
|
||||
self.pci_dev.write_config_flush(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) & ~pci.PCI_COMMAND_MASTER, 2)
|
||||
if self.is_hive():
|
||||
if reset_mode: return # in reset mode, do not raise
|
||||
raise RuntimeError("Malformed state. Use extra/amdpci/hive_reset.py to reset the hive")
|
||||
self.smu.mode1_reset()
|
||||
self.pci_dev.write_config_flush(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
self.init_hw(self.soc, self.gmc, self.ih, self.psp, self.smu)
|
||||
|
||||
# Booting done
|
||||
self.is_booting = False
|
||||
|
||||
# Re-initialize main blocks
|
||||
self.init_hw(self.gfx, self.sdma)
|
||||
|
||||
if (max_power:=getenv("AM_POWER_LIMIT", 0.0)) > 0:
|
||||
self.smu.set_power_limit(max_power)
|
||||
self.smu.set_clocks(level=None)
|
||||
else: self.smu.set_clocks(level=-1) # last level, max perf.
|
||||
for ip in [self.soc, self.gfx]: ip.set_clockgating_state()
|
||||
self.reg("regSCRATCH_REG7").write(AMDev.Version)
|
||||
self.reg("regSCRATCH_REG6").write(1) # set initialized state.
|
||||
if DEBUG >= 2: print(f"am {self.devfmt}: boot done")
|
||||
|
||||
def init_sw(self, smi_dev=False):
|
||||
self.smi_dev, self.is_err_state = smi_dev, False
|
||||
|
||||
# Memory manager & firmware
|
||||
self.mm = AMMemoryManager(self, self.vram_size - self.reserved_vram_size, boot_size=(32 << 20), pt_t=AMPageTableEntry, va_shifts=[12, 21, 30, 39],
|
||||
va_bits=48, first_lv=am.AMDGPU_VM_PDB2, va_base=AMMemoryManager.va_allocator.base, reserve_ptable=not self.large_bar,
|
||||
palloc_ranges=[(1 << (i + 12), (2 << 20) if i >= 9 else 0x1000) for i in range(9 * (3 - am.AMDGPU_VM_PDB2), -1, -1)])
|
||||
self.fw = AMFirmware(self)
|
||||
|
||||
# Initialize IP blocks
|
||||
self.soc:AM_SOC = AM_SOC(self)
|
||||
self.gmc:AM_GMC = AM_GMC(self)
|
||||
self.ih:AM_IH = AM_IH(self)
|
||||
self.psp:AM_PSP = AM_PSP(self)
|
||||
self.smu:AM_SMU = AM_SMU(self)
|
||||
self.gfx:AM_GFX = AM_GFX(self)
|
||||
self.sdma:AM_SDMA = AM_SDMA(self)
|
||||
|
||||
# Init sw for all IP blocks
|
||||
for ip in [self.soc, self.gmc, self.ih, self.psp, self.smu, self.gfx, self.sdma]: ip.init_sw()
|
||||
|
||||
def init_hw(self, *blocks:AM_IP):
|
||||
for ip in blocks:
|
||||
ip.init_hw()
|
||||
if DEBUG >= 2: print(f"am {self.devfmt}: {ip.__class__.__name__} initialized")
|
||||
|
||||
def fini(self):
|
||||
if DEBUG >= 2: print(f"am {self.devfmt}: Finalizing")
|
||||
for ip in [self.sdma, self.gfx]: ip.fini_hw()
|
||||
self.smu.set_clocks(level=0)
|
||||
self.ih.interrupt_handler()
|
||||
self.reg("regSCRATCH_REG6").write(self.is_err_state) # set finalized state.
|
||||
|
||||
def recover(self, force=False) -> bool:
|
||||
if not force and not self.is_err_state: return False
|
||||
if DEBUG >= 3: print(f"am {self.devfmt}: Start recovery")
|
||||
self.ih.interrupt_handler()
|
||||
self.gfx.reset_mec()
|
||||
self.is_err_state = False
|
||||
if DEBUG >= 3: print(f"am {self.devfmt}: Recovery complete")
|
||||
return True
|
||||
|
||||
def is_hive(self) -> bool: return self.gmc.xgmi_seg_sz > 0
|
||||
|
||||
def paddr2mc(self, paddr:int) -> int: return self.gmc.mc_base + paddr
|
||||
def paddr2xgmi(self, paddr:int) -> int: return self.gmc.paddr_base + paddr
|
||||
def xgmi2paddr(self, xgmi_paddr:int) -> int: return xgmi_paddr - self.gmc.paddr_base
|
||||
|
||||
def reg(self, reg:str) -> AMRegister: return self.__dict__[reg]
|
||||
|
||||
def rreg(self, reg:int) -> int:
|
||||
val = self.indirect_rreg(reg) if reg >= len(self.mmio) else self.mmio[reg]
|
||||
if AM_DEBUG >= 4 and getattr(self, '_prev_rreg', None) != (reg, val): print(f"am {self.devfmt}: Reading register {reg:#x} with value {val:#x}")
|
||||
self._prev_rreg = (reg, val)
|
||||
return val
|
||||
|
||||
def wreg(self, reg:int, val:int):
|
||||
if AM_DEBUG >= 4: print(f"am {self.devfmt}: Writing register {reg:#x} with value {val:#x}")
|
||||
if reg >= len(self.mmio): self.indirect_wreg(reg, val)
|
||||
else: self.mmio[reg] = val
|
||||
|
||||
def wreg_pair(self, reg_base:str, lo_suffix:str, hi_suffix:str, val:int, inst:int=0):
|
||||
self.reg(f"{reg_base}{lo_suffix}").write(lo32(val), inst=inst)
|
||||
self.reg(f"{reg_base}{hi_suffix}").write(hi32(val), inst=inst)
|
||||
|
||||
def indirect_rreg(self, reg:int) -> int:
|
||||
self.reg("regBIF_BX_PF0_RSMU_INDEX").write(reg * 4)
|
||||
return self.reg("regBIF_BX_PF0_RSMU_DATA").read()
|
||||
|
||||
def indirect_wreg(self, reg:int, val:int):
|
||||
self.reg("regBIF_BX_PF0_RSMU_INDEX").write(reg * 4)
|
||||
self.reg("regBIF_BX_PF0_RSMU_DATA").write(val)
|
||||
|
||||
def indirect_wreg_pcie(self, reg:int, val:int, aid:int=0):
|
||||
reg_addr = reg * 4 + ((((aid & 0b11) << 32) | (1 << 34)) if aid > 0 else 0)
|
||||
self.reg("regBIF_BX0_PCIE_INDEX2").write(lo32(reg_addr))
|
||||
if hi32(reg_addr) > 0: self.reg("regBIF_BX0_PCIE_INDEX2_HI").write(hi32(reg_addr) & 0xff)
|
||||
self.reg("regBIF_BX0_PCIE_DATA2").write(val)
|
||||
if hi32(reg_addr) > 0: self.reg("regBIF_BX0_PCIE_INDEX2_HI").write(0)
|
||||
|
||||
def _read_vram(self, addr, size) -> bytes:
|
||||
assert addr % 4 == 0 and size % 4 == 0, f"Invalid address {addr:#x} or size {size:#x}"
|
||||
res = []
|
||||
for caddr in range(addr, addr + size, 4):
|
||||
self.wreg(0x06, caddr >> 31)
|
||||
self.wreg(0x00, (caddr & 0x7FFFFFFF) | 0x80000000)
|
||||
res.append(self.rreg(0x01))
|
||||
return bytes(array.array('I', res))
|
||||
|
||||
def _run_discovery(self):
|
||||
# NOTE: Fixed register to query memory size without known ip bases to find the discovery table.
|
||||
# The table is located at the end of VRAM - 64KB and is 10KB in size.
|
||||
mmRCC_CONFIG_MEMSIZE = 0xde3
|
||||
self.vram_size = self.rreg(mmRCC_CONFIG_MEMSIZE) << 20
|
||||
self.large_bar = self.vram.nbytes >= self.vram_size
|
||||
tmr_offset, tmr_size = self.vram_size - (64 << 10), (10 << 10)
|
||||
|
||||
disc_tbl = self.vram.view(tmr_offset, tmr_size)[:] if self.large_bar else self._read_vram(tmr_offset, tmr_size)
|
||||
self.bhdr = am.struct_binary_header.from_buffer(bytearray(disc_tbl))
|
||||
ihdr = am.struct_ip_discovery_header.from_address(ctypes.addressof(self.bhdr) + self.bhdr.table_list[am.IP_DISCOVERY].offset)
|
||||
assert self.bhdr.binary_signature == am.BINARY_SIGNATURE and ihdr.signature == am.DISCOVERY_TABLE_SIGNATURE, "discovery signatures mismatch"
|
||||
|
||||
self.regs_offset:dict[int, dict[int, tuple]] = collections.defaultdict(dict)
|
||||
self.ip_ver:dict[int, tuple[int, int, int]] = {}
|
||||
|
||||
for num_die in range(ihdr.num_dies):
|
||||
dhdr = am.struct_die_header.from_address(ctypes.addressof(self.bhdr) + ihdr.die_info[num_die].die_offset)
|
||||
|
||||
ip_offset = ctypes.addressof(self.bhdr) + ctypes.sizeof(dhdr) + ihdr.die_info[num_die].die_offset
|
||||
for _ in range(dhdr.num_ips):
|
||||
ip = am.struct_ip_v4.from_address(ip_offset)
|
||||
ba = ((ctypes.c_uint64 if ihdr.base_addr_64_bit else ctypes.c_uint32) * ip.num_base_address).from_address(ip_offset + 8)
|
||||
for hw_ip in range(1, am.MAX_HWIP):
|
||||
if hw_ip in am.hw_id_map and am.hw_id_map[hw_ip] == ip.hw_id:
|
||||
self.regs_offset[hw_ip][ip.instance_number] = tuple(list(ba))
|
||||
self.ip_ver[hw_ip] = (ip.major, ip.minor, ip.revision)
|
||||
|
||||
ip_offset += 8 + (8 if ihdr.base_addr_64_bit else 4) * ip.num_base_address
|
||||
|
||||
gc_info = am.struct_gc_info_v1_0.from_address(gc_addr:=ctypes.addressof(self.bhdr) + self.bhdr.table_list[am.GC].offset)
|
||||
self.gc_info = getattr(am, f"struct_gc_info_v{gc_info.header.version_major}_{gc_info.header.version_minor}").from_address(gc_addr)
|
||||
self.reserved_vram_size = (384 << 20) if self.ip_ver[am.GC_HWIP][:2] in {(9,4), (9,5)} else (64 << 20)
|
||||
|
||||
@functools.cached_property
|
||||
def hwid_names(self) -> dict[int, str]: return {v:k.removesuffix('_HWID') for k,v in vars(am).items() if k.endswith('_HWID') and isinstance(v, int)}
|
||||
|
||||
def _ip_module(self, prefix:str, hwip): return import_module(prefix, self.ip_ver[hwip])
|
||||
|
||||
def _build_regs(self):
|
||||
mods = [("mp", am.MP0_HWIP), ("hdp", am.HDP_HWIP), ("gc", am.GC_HWIP), ("mmhub", am.MMHUB_HWIP), ("osssys", am.OSSSYS_HWIP),
|
||||
("nbio" if self.ip_ver[am.GC_HWIP] < (12,0,0) else "nbif", am.NBIO_HWIP)]
|
||||
if self.ip_ver[am.SDMA0_HWIP] in {(4,4,2), (4,4,4)}: mods += [("sdma", am.SDMA0_HWIP)]
|
||||
|
||||
for prefix, hwip in mods:
|
||||
self.__dict__.update(import_asic_regs(prefix, self.ip_ver[hwip], cls=functools.partial(AMRegister, adev=self, bases=self.regs_offset[hwip])))
|
||||
self.__dict__.update(import_asic_regs('mp', (11, 0, 0), cls=functools.partial(AMRegister, adev=self, bases=self.regs_offset[am.MP1_HWIP])))
|
||||
702
tinygrad_repo/tinygrad/runtime/support/am/ip.py
Normal file
702
tinygrad_repo/tinygrad/runtime/support/am/ip.py
Normal file
@@ -0,0 +1,702 @@
|
||||
import ctypes, time, contextlib, functools
|
||||
from typing import Literal
|
||||
from tinygrad.helpers import to_mv, data64, lo32, hi32, DEBUG, wait_cond, pad_bytes, getbits
|
||||
from tinygrad.runtime.autogen.am import am
|
||||
from tinygrad.runtime.support.amd import import_soc
|
||||
from tinygrad.runtime.support.memory import AddrSpace
|
||||
|
||||
class AM_IP:
|
||||
def __init__(self, adev): self.adev = adev
|
||||
def init_sw(self): pass # Prepare sw/allocations for this IP
|
||||
def init_hw(self): pass # Initialize hw for this IP
|
||||
def fini_hw(self): pass # Finalize hw for this IP
|
||||
def set_clockgating_state(self): pass # Set clockgating state for this IP
|
||||
|
||||
class AM_SOC(AM_IP):
|
||||
def init_sw(self):
|
||||
self.module = import_soc(self.adev.ip_ver[am.GC_HWIP])
|
||||
self.ih_clients = am.enum_soc21_ih_clientid if (ih_soc21:=self.adev.ip_ver[am.GC_HWIP][0] >= 11) else am.enum_soc15_ih_clientid
|
||||
|
||||
self.gfx_ih_clients = [am.SOC21_IH_CLIENTID_GRBM_CP, am.SOC21_IH_CLIENTID_GFX] \
|
||||
if ih_soc21 else [am.SOC15_IH_CLIENTID_GRBM_CP] + [getattr(am, f'SOC15_IH_CLIENTID_SE{i}SH') for i in range(4)]
|
||||
self.sdma_ih_clients = [] if ih_soc21 else [getattr(am, f'SOC15_IH_CLIENTID_SDMA{i}') for i in range(8)]
|
||||
|
||||
def _ih_srcs(pref:str, hwip:int) -> dict[int, str]:
|
||||
return {getattr(am, k): k[off+9:] for k in dir(am) if k.startswith(f'{pref}_{self.adev.ip_ver[hwip][0]}') and (off:=k.find('__SRCID__')) != -1}
|
||||
|
||||
gfx_srcs, sdma_srcs = _ih_srcs('GFX', am.GC_HWIP), _ih_srcs('SDMA0', am.SDMA0_HWIP)
|
||||
self.ih_srcs_names:dict[int, dict[int, str]] = {**{k: gfx_srcs for k in self.gfx_ih_clients}, **{k: sdma_srcs for k in self.sdma_ih_clients}}
|
||||
|
||||
def init_hw(self):
|
||||
if self.adev.ip_ver[am.NBIO_HWIP] in {(7,9,0), (7,9,1)}:
|
||||
self.adev.regXCC_DOORBELL_FENCE.write(0x0)
|
||||
for aid in range(1, self.adev.gmc.vmhubs):
|
||||
self.adev.indirect_wreg_pcie(self.adev.regXCC_DOORBELL_FENCE.addr[0], self.adev.regXCC_DOORBELL_FENCE.encode(shub_slv_mode=1), aid=aid)
|
||||
self.adev.regBIFC_GFX_INT_MONITOR_MASK.write(0x7ff)
|
||||
self.adev.regBIFC_DOORBELL_ACCESS_EN_PF.write(0xfffff)
|
||||
else: self.adev.regRCC_DEV0_EPF2_STRAP2.update(strap_no_soft_reset_dev0_f2=0x0)
|
||||
self.adev.regRCC_DEV0_EPF0_RCC_DOORBELL_APER_EN.write(0x1)
|
||||
def set_clockgating_state(self):
|
||||
if self.adev.ip_ver[am.HDP_HWIP] >= (5,2,1): self.adev.regHDP_MEM_POWER_CTRL.update(atomic_mem_power_ctrl_en=1, atomic_mem_power_ds_en=1)
|
||||
|
||||
def doorbell_enable(self, port, awid=0, awaddr_31_28_value=0, offset=0, size=0, aid=0):
|
||||
reg = self.adev.reg(f"{'regGDC_S2A0_S2A' if self.adev.ip_ver[am.GC_HWIP] >= (12,0,0) else 'regS2A'}_DOORBELL_ENTRY_{port}_CTRL")
|
||||
val = reg.encode(**{f"s2a_doorbell_port{port}_enable":1, f"s2a_doorbell_port{port}_awid":awid, f"s2a_doorbell_port{port}_range_size":size,
|
||||
f"s2a_doorbell_port{port}_awaddr_31_28_value":awaddr_31_28_value, f"s2a_doorbell_port{port}_range_offset":offset})
|
||||
|
||||
if self.adev.ip_ver[am.NBIO_HWIP] in {(7,9,0), (7,9,1)}: self.adev.indirect_wreg_pcie(reg.addr[0], val, aid=aid)
|
||||
else: reg.write(val)
|
||||
|
||||
class AM_GMC(AM_IP):
|
||||
def init_sw(self):
|
||||
self.vmhubs = len(self.adev.regs_offset[am.MMHUB_HWIP])
|
||||
|
||||
# XGMI (for supported systems)
|
||||
self.xgmi_phys_id = self.adev.regMMMC_VM_XGMI_LFB_CNTL.read_bitfields()['pf_lfb_region'] if hasattr(self.adev, 'regMMMC_VM_XGMI_LFB_CNTL') else 0
|
||||
self.xgmi_seg_sz = self.adev.regMMMC_VM_XGMI_LFB_SIZE.read_bitfields()['pf_lfb_size']<<24 if hasattr(self.adev, 'regMMMC_VM_XGMI_LFB_SIZE') else 0
|
||||
|
||||
self.paddr_base = self.xgmi_phys_id * self.xgmi_seg_sz
|
||||
|
||||
self.fb_base = (self.adev.regMMMC_VM_FB_LOCATION_BASE.read() & 0xFFFFFF) << 24
|
||||
self.fb_end = (self.adev.regMMMC_VM_FB_LOCATION_TOP.read() & 0xFFFFFF) << 24
|
||||
|
||||
# Memory controller aperture
|
||||
self.mc_base = self.fb_base + self.paddr_base
|
||||
|
||||
# VM aperture
|
||||
self.vm_base = self.adev.mm.va_base
|
||||
self.vm_end = min(self.vm_base + (1 << self.adev.mm.va_bits) - 1, 0x7fffffffffff)
|
||||
|
||||
self.trans_futher = self.adev.ip_ver[am.GC_HWIP] < (10, 0, 0)
|
||||
|
||||
# mi3xx has 48-bit, others have 44-bit address space
|
||||
self.address_space_mask = (1 << (48 if self.adev.ip_ver[am.GC_HWIP][:2] in {(9,4), (9,5)} else 44)) - 1
|
||||
|
||||
self.memscratch_xgmi_paddr = self.adev.paddr2xgmi(self.adev.mm.palloc(0x1000, zero=False, boot=True))
|
||||
self.dummy_page_xgmi_paddr = self.adev.paddr2xgmi(self.adev.mm.palloc(0x1000, zero=False, boot=True))
|
||||
|
||||
# MM hub is inited before any tlb flushes and is still valid during partial_boot, so set it to true
|
||||
self.hub_initted = {"MM": True, "GC": False}
|
||||
|
||||
self.pf_status_reg = lambda ip: f"reg{ip}VM_L2_PROTECTION_FAULT_STATUS{'_LO32' if self.adev.ip_ver[am.GC_HWIP] >= (12,0,0) else ''}"
|
||||
|
||||
def init_hw(self): self.init_hub("MM", inst_cnt=self.vmhubs)
|
||||
|
||||
def flush_hdp(self): self.adev.wreg(self.adev.reg("regBIF_BX0_REMAP_HDP_MEM_FLUSH_CNTL").read() // 4, 0x0)
|
||||
def flush_tlb(self, ip:Literal["MM", "GC"], vmid, flush_type=0):
|
||||
self.flush_hdp()
|
||||
|
||||
# Can't issue TLB invalidation if the hub isn't initialized.
|
||||
if not self.hub_initted[ip]: return
|
||||
|
||||
for inst in range(self.adev.gmc.vmhubs if ip == "MM" else self.adev.gfx.xccs):
|
||||
if ip == "MM": wait_cond(lambda: self.adev.regMMVM_INVALIDATE_ENG17_SEM.read(inst=inst) & 0x1, value=1, msg="mm flush_tlb timeout")
|
||||
|
||||
self.adev.reg(f"reg{ip}VM_INVALIDATE_ENG17_REQ").write(flush_type=flush_type, per_vmid_invalidate_req=(1 << vmid), invalidate_l2_ptes=1,
|
||||
invalidate_l2_pde0=1, invalidate_l2_pde1=1, invalidate_l2_pde2=1, invalidate_l1_ptes=1, clear_protection_fault_status_addr=0, inst=inst)
|
||||
|
||||
wait_cond(lambda: self.adev.reg(f"reg{ip}VM_INVALIDATE_ENG17_ACK").read(inst=inst) & (1 << vmid), value=(1 << vmid), msg="flush_tlb timeout")
|
||||
|
||||
if ip == "MM": self.adev.regMMVM_INVALIDATE_ENG17_SEM.write(0x0, inst=inst)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (11,0,0) and ip == "MM":
|
||||
self.adev.regMMVM_L2_BANK_SELECT_RESERVED_CID2.update(reserved_cache_private_invalidation=1, inst=inst)
|
||||
|
||||
# Read back the register to ensure the invalidation is complete
|
||||
self.adev.regMMVM_L2_BANK_SELECT_RESERVED_CID2.read(inst=inst)
|
||||
|
||||
def enable_vm_addressing(self, page_table, ip:Literal["MM", "GC"], vmid, inst):
|
||||
self.adev.wreg_pair(f"reg{ip}VM_CONTEXT{vmid}_PAGE_TABLE_START_ADDR", "_LO32", "_HI32", self.vm_base >> 12, inst=inst)
|
||||
self.adev.wreg_pair(f"reg{ip}VM_CONTEXT{vmid}_PAGE_TABLE_END_ADDR", "_LO32", "_HI32", self.vm_end >> 12, inst=inst)
|
||||
self.adev.wreg_pair(f"reg{ip}VM_CONTEXT{vmid}_PAGE_TABLE_BASE_ADDR", "_LO32", "_HI32", self.adev.paddr2xgmi(page_table.paddr) | 1, inst=inst)
|
||||
|
||||
fault_flags = {f'{x}_protection_fault_enable_interrupt':1 for x in ['pde0', 'dummy_page', 'range', 'valid', 'read', 'write', 'execute']}
|
||||
en_def_flags = {f'{x}_protection_fault_enable_default':1 for x in ['pde0', 'dummy_page', 'range', 'valid', 'read', 'write', 'execute']}
|
||||
self.adev.reg(f"reg{ip}VM_CONTEXT{vmid}_CNTL").write(0x1800000, **fault_flags, **en_def_flags, enable_context=1,
|
||||
page_table_depth=((2 if self.trans_futher else 3) - page_table.lv), page_table_block_size=9 if self.trans_futher else 0, inst=inst)
|
||||
|
||||
def init_hub(self, ip:Literal["MM", "GC"], inst_cnt:int):
|
||||
# Init system apertures
|
||||
for inst in range(inst_cnt):
|
||||
self.adev.reg(f"reg{ip}MC_VM_AGP_BASE").write(0, inst=inst)
|
||||
self.adev.reg(f"reg{ip}MC_VM_AGP_BOT").write(0xffffffffffff >> 24, inst=inst) # disable AGP
|
||||
self.adev.reg(f"reg{ip}MC_VM_AGP_TOP").write(0, inst=inst)
|
||||
|
||||
self.adev.reg(f"reg{ip}MC_VM_SYSTEM_APERTURE_LOW_ADDR").write(self.fb_base >> 18, inst=inst)
|
||||
self.adev.reg(f"reg{ip}MC_VM_SYSTEM_APERTURE_HIGH_ADDR").write(self.fb_end >> 18, inst=inst)
|
||||
self.adev.wreg_pair(f"reg{ip}MC_VM_SYSTEM_APERTURE_DEFAULT_ADDR", "_LSB", "_MSB", self.memscratch_xgmi_paddr >> 12, inst=inst)
|
||||
self.adev.wreg_pair(f"reg{ip}VM_L2_PROTECTION_FAULT_DEFAULT_ADDR", "_LO32", "_HI32", self.dummy_page_xgmi_paddr >> 12, inst=inst)
|
||||
|
||||
self.adev.reg(f"reg{ip}VM_L2_PROTECTION_FAULT_CNTL2").update(active_page_migration_pte_read_retry=1, inst=inst)
|
||||
|
||||
# Init TLB and cache
|
||||
self.adev.reg(f"reg{ip}MC_VM_MX_L1_TLB_CNTL").update(enable_l1_tlb=1, system_access_mode=3, enable_advanced_driver_model=1,
|
||||
system_aperture_unmapped_access=0, mtype=self.adev.soc.module.MTYPE_UC, inst=inst)
|
||||
|
||||
self.adev.reg(f"reg{ip}VM_L2_CNTL").update(enable_l2_cache=1, enable_default_page_out_to_system_memory=1,
|
||||
l2_pde0_cache_tag_generation_mode=0, pde_fault_classification=0, context1_identity_access_mode=1, identity_mode_fragment_size=0,
|
||||
enable_l2_fragment_processing=int(self.adev.ip_ver[am.GC_HWIP] < (10,0,0)), inst=inst)
|
||||
self.adev.reg(f"reg{ip}VM_L2_CNTL2").update(invalidate_all_l1_tlbs=1, invalidate_l2_cache=1, inst=inst)
|
||||
self.adev.reg(f"reg{ip}VM_L2_CNTL3").write(l2_cache_4k_associativity=1, l2_cache_bigk_associativity=1,
|
||||
bank_select=12 if self.trans_futher else 9, l2_cache_bigk_fragment_size=9 if self.trans_futher else 6, inst=inst)
|
||||
self.adev.reg(f"reg{ip}VM_L2_CNTL4").write(l2_cache_4k_partition_count=1, inst=inst)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (10,0,0): self.adev.reg(f"reg{ip}VM_L2_CNTL5").write(walker_priority_client_id=0x1ff, inst=inst)
|
||||
|
||||
self.enable_vm_addressing(self.adev.mm.root_page_table, ip, vmid=0, inst=inst)
|
||||
|
||||
# Disable identity aperture
|
||||
self.adev.wreg_pair(f"reg{ip}VM_L2_CONTEXT1_IDENTITY_APERTURE_LOW_ADDR", "_LO32", "_HI32", 0xfffffffff, inst=inst)
|
||||
self.adev.wreg_pair(f"reg{ip}VM_L2_CONTEXT1_IDENTITY_APERTURE_HIGH_ADDR", "_LO32", "_HI32", 0x0, inst=inst)
|
||||
self.adev.wreg_pair(f"reg{ip}VM_L2_CONTEXT_IDENTITY_PHYSICAL_OFFSET", "_LO32", "_HI32", 0x0, inst=inst)
|
||||
|
||||
for eng_i in range(18): self.adev.wreg_pair(f"reg{ip}VM_INVALIDATE_ENG{eng_i}_ADDR_RANGE", "_LO32", "_HI32", 0x1fffffffff, inst=inst)
|
||||
self.hub_initted[ip] = True
|
||||
|
||||
@functools.cache # pylint: disable=method-cache-max-size-none
|
||||
def get_pte_flags(self, pte_lv, is_table, frag, uncached, system, snooped, valid, extra=0):
|
||||
extra |= (am.AMDGPU_PTE_SYSTEM * system) | (am.AMDGPU_PTE_SNOOPED * snooped) | (am.AMDGPU_PTE_VALID * valid) | am.AMDGPU_PTE_FRAG(frag)
|
||||
if not is_table: extra |= (am.AMDGPU_PTE_WRITEABLE | am.AMDGPU_PTE_READABLE | am.AMDGPU_PTE_EXECUTABLE)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (12,0,0):
|
||||
extra |= am.AMDGPU_PTE_MTYPE_GFX12(0, self.adev.soc.module.MTYPE_UC if uncached else 0)
|
||||
extra |= (am.AMDGPU_PDE_PTE_GFX12 if not is_table and pte_lv != am.AMDGPU_VM_PTB else (am.AMDGPU_PTE_IS_PTE if not is_table else 0))
|
||||
elif self.adev.ip_ver[am.GC_HWIP] >= (10,0,0):
|
||||
extra |= am.AMDGPU_PTE_MTYPE_NV10(0, self.adev.soc.module.MTYPE_UC if uncached else 0)
|
||||
extra |= (am.AMDGPU_PDE_PTE if not is_table and pte_lv != am.AMDGPU_VM_PTB else 0)
|
||||
else:
|
||||
extra |= am.AMDGPU_PTE_MTYPE_VG10(0, self.adev.soc.module.MTYPE_UC if uncached else 0)
|
||||
if is_table and pte_lv == am.AMDGPU_VM_PDB1: extra |= am.AMDGPU_PDE_BFS(0x9)
|
||||
if is_table and pte_lv == am.AMDGPU_VM_PDB0: extra |= am.AMDGPU_PTE_TF
|
||||
if not is_table and pte_lv not in {am.AMDGPU_VM_PTB, am.AMDGPU_VM_PDB0}: extra |= am.AMDGPU_PDE_PTE
|
||||
return extra
|
||||
def is_pte_huge_page(self, pte_lv, pte):
|
||||
if self.adev.ip_ver[am.GC_HWIP] < (10,0,0): return (pte & am.AMDGPU_PDE_PTE) if pte_lv != am.AMDGPU_VM_PDB0 else not (pte & am.AMDGPU_PTE_TF)
|
||||
return pte & (am.AMDGPU_PDE_PTE_GFX12 if self.adev.ip_ver[am.GC_HWIP] >= (12,0,0) else am.AMDGPU_PDE_PTE)
|
||||
|
||||
class AM_SMU(AM_IP):
|
||||
def init_sw(self):
|
||||
self.smu_mod = self.adev._ip_module("smu", am.MP1_HWIP)
|
||||
self.driver_table_paddr = self.adev.mm.palloc(0x4000, zero=False, boot=True)
|
||||
|
||||
def init_hw(self):
|
||||
self._send_msg(self.smu_mod.PPSMC_MSG_SetDriverDramAddrHigh, hi32(self.adev.paddr2mc(self.driver_table_paddr)))
|
||||
self._send_msg(self.smu_mod.PPSMC_MSG_SetDriverDramAddrLow, lo32(self.adev.paddr2mc(self.driver_table_paddr)))
|
||||
self._send_msg(self.smu_mod.PPSMC_MSG_EnableAllSmuFeatures, 0)
|
||||
|
||||
def is_smu_alive(self):
|
||||
with contextlib.suppress(TimeoutError): self._send_msg(self.smu_mod.PPSMC_MSG_GetSmuVersion, 0, timeout=100)
|
||||
return self.adev.mmMP1_SMN_C2PMSG_90.read() != 0
|
||||
|
||||
def mode1_reset(self):
|
||||
if DEBUG >= 2: print(f"am {self.adev.devfmt}: mode1 reset")
|
||||
if self.adev.ip_ver[am.MP0_HWIP] >= (14,0,0): self._send_msg(__DEBUGSMC_MSG_Mode1Reset:=2, 0, debug=True)
|
||||
elif self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,12)}: self._send_msg(self.smu_mod.PPSMC_MSG_GfxDriverReset, 1)
|
||||
else: self._send_msg(self.smu_mod.PPSMC_MSG_Mode1Reset, 0)
|
||||
|
||||
if not self.adev.is_hive(): time.sleep(0.5) # 500ms
|
||||
|
||||
def read_table(self, table_t, arg):
|
||||
if self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6),(13,0,12)}: self._send_msg(self.smu_mod.PPSMC_MSG_GetMetricsTable, arg)
|
||||
else: self._send_msg(self.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, arg)
|
||||
return table_t.from_buffer(bytearray(self.adev.vram.view(self.driver_table_paddr, ctypes.sizeof(table_t))[:]))
|
||||
|
||||
@functools.cache # pylint: disable=method-cache-max-size-none
|
||||
def read_clocks(self, clk_list:tuple[int]) -> dict[int, list[int]]:
|
||||
return {clck: [self._send_msg(self.smu_mod.PPSMC_MSG_GetDpmFreqByIndex, (clck<<16)|i, read_back_arg=True)&0x7fffffff for i in range(cnt)]
|
||||
for clck in clk_list if (cnt:=self._send_msg(self.smu_mod.PPSMC_MSG_GetDpmFreqByIndex, (clck<<16)|0xff, read_back_arg=True)&0x7fffffff)}
|
||||
|
||||
def set_clocks(self, level:int|None):
|
||||
clks = tuple([self.smu_mod.PPCLK_UCLK, self.smu_mod.PPCLK_FCLK, self.smu_mod.PPCLK_SOCCLK])
|
||||
if self.adev.ip_ver[am.MP0_HWIP] not in {(13,0,6), (13,0,12)}: clks += (self.smu_mod.PPCLK_GFXCLK,)
|
||||
|
||||
if level is None:
|
||||
for clck in clks:
|
||||
with contextlib.suppress(TimeoutError): self._send_msg(self.smu_mod.PPSMC_MSG_SetSoftMinByFreq, clck << 16, timeout=20)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (10,0,0): self._send_msg(self.smu_mod.PPSMC_MSG_SetSoftMaxByFreq, clck << 16 | 0xffff)
|
||||
return
|
||||
|
||||
for clck, vals in self.read_clocks(clks).items():
|
||||
with contextlib.suppress(TimeoutError): self._send_msg(self.smu_mod.PPSMC_MSG_SetSoftMinByFreq, clck << 16 | (vals[level]), timeout=20)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (10,0,0): self._send_msg(self.smu_mod.PPSMC_MSG_SetSoftMaxByFreq, clck << 16 | (vals[level]))
|
||||
|
||||
def set_power_limit(self, watts:float):
|
||||
ppt_limit = max(int(round(watts)), 1)
|
||||
self._send_msg(self.smu_mod.PPSMC_MSG_SetPptLimit, ppt_limit)
|
||||
if DEBUG >= 2: print(f"am {self.adev.devfmt}: GPU power limit set to {ppt_limit}W")
|
||||
|
||||
def _aca_read_reg(self, bank_idx:int, reg_idx:int, ue=True) -> int:
|
||||
msg = self.smu_mod.PPSMC_MSG_McaBankDumpDW if ue else self.smu_mod.PPSMC_MSG_McaBankCeDumpDW
|
||||
return (self._send_msg(msg, (bank_idx << 16) | (reg_idx * 8 + 4), read_back_arg=True) << 32) | \
|
||||
self._send_msg(msg, (bank_idx << 16) | (reg_idx * 8), read_back_arg=True)
|
||||
|
||||
def _aca_read_banks(self, ue=True) -> list[list[int]]:
|
||||
if not hasattr(self.smu_mod, 'PPSMC_MSG_QueryValidMcaCount'): return []
|
||||
count_msg = self.smu_mod.PPSMC_MSG_QueryValidMcaCount if ue else self.smu_mod.PPSMC_MSG_QueryValidMcaCeCount
|
||||
return [[self._aca_read_reg(idx, reg_idx, ue=ue) for reg_idx in range(16)] for idx in range(self._send_msg(count_msg, 0, read_back_arg=True))]
|
||||
|
||||
def _smu_cmn_send_msg(self, msg:int, param=0, debug=False):
|
||||
(self.adev.mmMP1_SMN_C2PMSG_90 if not debug else self.adev.mmMP1_SMN_C2PMSG_54).write(0) # resp reg
|
||||
(self.adev.mmMP1_SMN_C2PMSG_82 if not debug else self.adev.mmMP1_SMN_C2PMSG_53).write(param)
|
||||
(self.adev.mmMP1_SMN_C2PMSG_66 if not debug else self.adev.mmMP1_SMN_C2PMSG_75).write(msg)
|
||||
|
||||
def _send_msg(self, msg:int, param:int, read_back_arg=False, timeout=10000, debug=False): # default timeout is 10 seconds
|
||||
self._smu_cmn_send_msg(msg, param, debug=debug)
|
||||
wait_cond((self.adev.mmMP1_SMN_C2PMSG_90 if not debug else self.adev.mmMP1_SMN_C2PMSG_54).read, value=1, timeout_ms=timeout,
|
||||
msg=f"SMU msg {msg:#x} timeout")
|
||||
return (self.adev.mmMP1_SMN_C2PMSG_82 if not debug else self.adev.mmMP1_SMN_C2PMSG_53).read() if read_back_arg else None
|
||||
|
||||
class AM_GFX(AM_IP):
|
||||
def init_sw(self):
|
||||
self.xccs = len(self.adev.regs_offset[am.GC_HWIP])
|
||||
self.mqd_paddr = [self.adev.mm.palloc(0x1000 * self.xccs, zero=False, boot=True) for i in range(2)]
|
||||
self.mqd_mc = [self.adev.paddr2mc(mqd_paddr) for mqd_paddr in self.mqd_paddr]
|
||||
|
||||
def init_hw(self):
|
||||
# Wait for RLC autoload to complete
|
||||
wait_cond(lambda: self.adev.regCP_STAT.read() == 0 or self.adev.regRLC_RLCS_BOOTLOAD_STATUS.read_bitfields()['bootload_complete'] == 0,
|
||||
value=True, msg="RLC autoload timeout")
|
||||
|
||||
self.adev.gmc.init_hub("GC", inst_cnt=self.xccs)
|
||||
if self.adev.partial_boot: return self.reset_mec()
|
||||
|
||||
self._config_mec()
|
||||
|
||||
# NOTE: Golden reg for gfx11. No values for this reg provided. The kernel just ors 0x20000000 to this reg.
|
||||
for xcc in range(self.xccs): self.adev.regTCP_CNTL.write(self.adev.regTCP_CNTL.read() | 0x20000000, inst=xcc)
|
||||
|
||||
for xcc in range(self.xccs): self.adev.regRLC_CNTL.write(0x1, inst=xcc)
|
||||
|
||||
for xcc in range(self.xccs): self.adev.regRLC_SRM_CNTL.update(srm_enable=1, auto_incr_addr=1, inst=xcc)
|
||||
|
||||
for xcc in range(self.xccs): self.adev.regRLC_SPM_MC_CNTL.write(0xf, inst=xcc)
|
||||
|
||||
if self.adev.ip_ver[am.NBIO_HWIP][:2] != (7,9):
|
||||
self.adev.soc.doorbell_enable(port=0, awid=0x3, awaddr_31_28_value=0x3)
|
||||
self.adev.soc.doorbell_enable(port=3, awid=0x6, awaddr_31_28_value=0x3)
|
||||
|
||||
for xcc in range(self.xccs):
|
||||
if self.adev.ip_ver[am.GC_HWIP] in {(9,4,3), (9,5,0)}:
|
||||
self.adev.regGB_ADDR_CONFIG.write(0x2a114042, inst=xcc) # Golden value for mi300/mi350
|
||||
self.adev.regTCP_UTCL1_CNTL2.update(spare=1, inst=xcc)
|
||||
|
||||
self.adev.regGRBM_CNTL.update(read_timeout=0xff, inst=xcc)
|
||||
for i in range(0, 16):
|
||||
self._grbm_select(vmid=i, inst=xcc)
|
||||
self.adev.regSH_MEM_CONFIG.write(**({'initial_inst_prefetch':3} if self.adev.ip_ver[am.GC_HWIP][0]>=10 else {'retry_disable':1}),
|
||||
**({'f8_mode':1} if self.adev.ip_ver[am.GC_HWIP][:2]==(9,4) else {}),
|
||||
address_mode=self.adev.soc.module.SH_MEM_ADDRESS_MODE_64, alignment_mode=self.adev.soc.module.SH_MEM_ALIGNMENT_MODE_UNALIGNED, inst=xcc)
|
||||
|
||||
# Configure apertures:
|
||||
# LDS: 0x10000000'00000000 - 0x10000001'00000000 (4GB)
|
||||
# Scratch: 0x20000000'00000000 - 0x20000001'00000000 (4GB)
|
||||
self.adev.regSH_MEM_BASES.write(shared_base=0x1, private_base=0x2, inst=xcc)
|
||||
self._grbm_select(inst=xcc)
|
||||
|
||||
# Configure MEC doorbell range
|
||||
self.adev.regCP_MEC_DOORBELL_RANGE_LOWER.write(0x100 * xcc, inst=xcc)
|
||||
self.adev.regCP_MEC_DOORBELL_RANGE_UPPER.write(0x100 * xcc + 0xf8, inst=xcc)
|
||||
|
||||
self._enable_mec()
|
||||
|
||||
# Set 1 partition
|
||||
if self.xccs > 1: self.adev.psp._spatial_partition_cmd(1)
|
||||
|
||||
def fini_hw(self): self._dequeue_hqds()
|
||||
|
||||
def reset_mec(self):
|
||||
self._dequeue_hqds()
|
||||
|
||||
if self.adev.ip_ver[am.GC_HWIP] < (12,0,0): # gfx12+ uses mec_pipe0_reset
|
||||
for xcc in range(self.xccs): self.adev.regGRBM_SOFT_RESET.write(soft_reset_cp=1, soft_reset_cpc=1, inst=xcc)
|
||||
time.sleep(0.05)
|
||||
for xcc in range(self.xccs): self.adev.regGRBM_SOFT_RESET.write(0x0, inst=xcc)
|
||||
|
||||
self._config_mec()
|
||||
self._enable_mec()
|
||||
|
||||
def setup_ring(self, ring_addr:int, ring_size:int, rptr_addr:int, wptr_addr:int, eop_addr:int, eop_size:int, idx:int, aql:bool) -> int:
|
||||
pipe, queue, doorbell = idx // 4, idx % 4, am.AMDGPU_NAVI10_DOORBELL_MEC_RING0
|
||||
|
||||
for xcc in range(self.xccs if aql else 1):
|
||||
self._grbm_select(me=1, pipe=pipe, queue=queue, inst=xcc)
|
||||
|
||||
struct_t = getattr(am, f"struct_v{self.adev.ip_ver[am.GC_HWIP][0]}{'_compute' if self.adev.ip_ver[am.GC_HWIP][0] >= 10 else ''}_mqd")
|
||||
mqd_struct = struct_t(header=0xC0310800, cp_mqd_base_addr_lo=lo32(self.mqd_mc[queue] + 0x1000*xcc),
|
||||
cp_mqd_base_addr_hi=hi32(self.mqd_mc[queue] + 0x1000*xcc), cp_hqd_pipe_priority=0x2, cp_hqd_queue_priority=0xf, cp_hqd_quantum=0x111,
|
||||
cp_hqd_persistent_state=self.adev.regCP_HQD_PERSISTENT_STATE.encode(preload_size=0x55, preload_req=1),
|
||||
cp_hqd_pq_base_lo=lo32(ring_addr>>8), cp_hqd_pq_base_hi=hi32(ring_addr>>8),
|
||||
cp_hqd_pq_rptr_report_addr_lo=lo32(rptr_addr), cp_hqd_pq_rptr_report_addr_hi=hi32(rptr_addr),
|
||||
cp_hqd_pq_wptr_poll_addr_lo=lo32(wptr_addr), cp_hqd_pq_wptr_poll_addr_hi=hi32(wptr_addr),
|
||||
cp_hqd_pq_doorbell_control=self.adev.regCP_HQD_PQ_DOORBELL_CONTROL.encode(doorbell_offset=doorbell*2, doorbell_en=1),
|
||||
cp_hqd_pq_control=self.adev.regCP_HQD_PQ_CONTROL.encode(rptr_block_size=5, unord_dispatch=0, queue_size=(ring_size//4).bit_length()-2,
|
||||
**({'queue_full_en':1, 'slot_based_wptr':2, 'no_update_rptr':xcc!=0 or self.xccs==1} if aql else {})),
|
||||
cp_hqd_ib_control=self.adev.regCP_HQD_IB_CONTROL.encode(min_ib_avail_size=0x3), cp_hqd_hq_status0=0x20004000,
|
||||
cp_mqd_control=self.adev.regCP_MQD_CONTROL.encode(priv_state=1), cp_hqd_vmid=0, cp_hqd_aql_control=int(aql),
|
||||
cp_hqd_eop_base_addr_lo=lo32(eop_addr>>8), cp_hqd_eop_base_addr_hi=hi32(eop_addr>>8),
|
||||
cp_hqd_eop_control=self.adev.regCP_HQD_EOP_CONTROL.encode(eop_size=(eop_size//4).bit_length()-2),
|
||||
**({'compute_tg_chunk_size':1, 'compute_current_logic_xcc_id':xcc, 'cp_mqd_stride_size':0x1000} if aql and self.xccs > 1 else {}))
|
||||
for se in range(8 if self.adev.ip_ver[am.GC_HWIP][0] >= 10 else 4): setattr(mqd_struct, f'compute_static_thread_mgmt_se{se}', 0xffffffff)
|
||||
|
||||
self.adev.vram.view(self.mqd_paddr[queue] + 0x1000*xcc, ctypes.sizeof(mqd_struct))[:] = memoryview(mqd_struct).cast('B')
|
||||
|
||||
mqd_st_mv = to_mv(ctypes.addressof(mqd_struct), ctypes.sizeof(mqd_struct)).cast('I')
|
||||
for i, reg in enumerate(range(self.adev.regCP_MQD_BASE_ADDR.addr[xcc], self.adev.regCP_HQD_PQ_WPTR_HI.addr[xcc] + 1)):
|
||||
self.adev.wreg(reg, mqd_st_mv[0x80 + i])
|
||||
self.adev.regCP_HQD_ACTIVE.write(0x1, inst=xcc)
|
||||
|
||||
self.adev.gmc.flush_hdp()
|
||||
self._grbm_select(inst=xcc)
|
||||
return doorbell
|
||||
|
||||
def set_clockgating_state(self):
|
||||
if hasattr(self.adev, 'regMM_ATC_L2_MISC_CG'): self.adev.regMM_ATC_L2_MISC_CG.write(enable=1, mem_ls_enable=1)
|
||||
|
||||
for xcc in range(self.xccs):
|
||||
self.adev.regRLC_SAFE_MODE.write(message=1, cmd=1, inst=xcc)
|
||||
wait_cond(lambda: self.adev.regRLC_SAFE_MODE.read(inst=xcc) & 0x1, value=0, msg="RLC safe mode timeout")
|
||||
|
||||
self.adev.regRLC_CGCG_CGLS_CTRL.update(cgcg_gfx_idle_threshold=0x36, cgcg_en=1, cgls_rep_compansat_delay=0xf, cgls_en=1, inst=xcc)
|
||||
|
||||
self.adev.regCP_RB_WPTR_POLL_CNTL.update(poll_frequency=0x100, idle_poll_count=0x90, inst=xcc)
|
||||
self.adev.regCP_INT_CNTL.update(cntx_busy_int_enable=1, cntx_empty_int_enable=1, cmp_busy_int_enable=1, inst=xcc)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (10,0,0):
|
||||
self.adev.regSDMA0_RLC_CGCG_CTRL.update(cgcg_int_enable=1, inst=xcc)
|
||||
self.adev.regSDMA1_RLC_CGCG_CTRL.update(cgcg_int_enable=1, inst=xcc)
|
||||
|
||||
feats_gfx9 = {'gfxip_mgls_override':0, 'gfxip_rep_fgcg_override':0} if self.adev.ip_ver[am.GC_HWIP][0] == 9 else {}
|
||||
feats_gfx11 = {'perfmon_clock_state':1, 'gfxip_repeater_fgcg_override':0} if self.adev.ip_ver[am.GC_HWIP][0] >= 11 else {}
|
||||
self.adev.regRLC_CGTT_MGCG_OVERRIDE.update(**feats_gfx9, **feats_gfx11, gfxip_fgcg_override=0, grbm_cgtt_sclk_override=0,
|
||||
rlc_cgtt_sclk_override=0, gfxip_mgcg_override=0, gfxip_cgls_override=0, gfxip_cgcg_override=0, inst=xcc)
|
||||
|
||||
self.adev.regRLC_SAFE_MODE.write(message=0, cmd=1, inst=xcc)
|
||||
|
||||
def _grbm_select(self, me=0, pipe=0, queue=0, vmid=0, inst=0):
|
||||
self.adev.regGRBM_GFX_CNTL.write(meid=me, pipeid=pipe, vmid=vmid, queueid=queue, inst=inst)
|
||||
|
||||
def _enable_mec(self):
|
||||
for xcc in range(self.xccs):
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (10,0,0): self.adev.regCP_MEC_RS64_CNTL.update(mec_pipe0_reset=0, mec_pipe0_active=1, mec_halt=0, inst=xcc)
|
||||
else: self.adev.regCP_MEC_CNTL.write(0x0, inst=xcc)
|
||||
time.sleep(0.05) # Wait for MEC to be ready
|
||||
|
||||
def _config_mec(self):
|
||||
def _config_helper(eng_name, cntl_reg, eng_reg, pipe_cnt, me=0, xcc=0):
|
||||
for pipe in range(pipe_cnt):
|
||||
self._grbm_select(me=me, pipe=pipe, inst=xcc)
|
||||
self.adev.wreg_pair(f"regCP_{eng_reg}_PRGRM_CNTR_START", "", "_HI", self.adev.fw.ucode_start[eng_name] >> 2, inst=xcc)
|
||||
self._grbm_select(inst=xcc)
|
||||
self.adev.reg(f"regCP_{cntl_reg}_CNTL").update(**{f"{eng_name.lower()}_pipe{pipe}_reset": 1 for pipe in range(pipe_cnt)}, inst=xcc)
|
||||
self.adev.reg(f"regCP_{cntl_reg}_CNTL").update(**{f"{eng_name.lower()}_pipe{pipe}_reset": 0 for pipe in range(pipe_cnt)}, inst=xcc)
|
||||
|
||||
for xcc in range(self.adev.gfx.xccs):
|
||||
if self.adev.ip_ver[am.GC_HWIP] < (10,0,0):
|
||||
self.adev.regCP_MEC_CNTL.update(mec_invalidate_icache=1, mec_me1_pipe0_reset=1, mec_me2_pipe0_reset=1, mec_me1_halt=1,mec_me2_halt=1,inst=xcc)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (12,0,0):
|
||||
_config_helper(eng_name="PFP", cntl_reg="ME", eng_reg="PFP", pipe_cnt=1, xcc=xcc)
|
||||
_config_helper(eng_name="ME", cntl_reg="ME", eng_reg="ME", pipe_cnt=1, xcc=xcc)
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (10,0,0):
|
||||
_config_helper(eng_name="MEC", cntl_reg="MEC_RS64", eng_reg="MEC_RS64", pipe_cnt=1, me=1, xcc=xcc)
|
||||
|
||||
def _dequeue_hqds(self):
|
||||
for q in range(2):
|
||||
for xcc in range(self.xccs):
|
||||
self._grbm_select(me=1, pipe=0, queue=q, inst=xcc)
|
||||
if self.adev.regCP_HQD_ACTIVE.read(inst=xcc) & 1:
|
||||
self.adev.regCP_HQD_DEQUEUE_REQUEST.write(0x2, inst=xcc) # 1 - DRAIN_PIPE; 2 - RESET_WAVES
|
||||
self.adev.regSPI_COMPUTE_QUEUE_RESET.write(0x1, inst=xcc)
|
||||
if not self.adev.is_err_state: wait_cond(lambda: self.adev.regCP_HQD_ACTIVE.read(inst=xcc) & 1, value=0, msg="HQD dequeue timeout")
|
||||
self._grbm_select()
|
||||
|
||||
class AM_IH(AM_IP):
|
||||
def init_sw(self):
|
||||
self.ring_size = 256 << 10
|
||||
def _alloc_ring(size): return (self.adev.mm.palloc(size, zero=False, boot=True), self.adev.mm.palloc(0x1000, zero=False, boot=True))
|
||||
self.rings = [(*_alloc_ring(self.ring_size), "", 0), (*_alloc_ring(self.ring_size), "_RING1", 1)]
|
||||
self.ring_view = self.adev.vram.view(offset=self.rings[0][0], size=self.ring_size, fmt='I')
|
||||
|
||||
def init_hw(self):
|
||||
for ring_vm, rwptr_vm, suf, ring_id in self.rings:
|
||||
self.adev.wreg_pair("regIH_RB_BASE", suf, f"_HI{suf}", self.adev.paddr2mc(ring_vm) >> 8)
|
||||
|
||||
self.adev.reg(f"regIH_RB_CNTL{suf}").write(mc_space=4, wptr_overflow_clear=1, rb_size=((self.ring_size//4)-1).bit_length(),
|
||||
mc_snoop=1, mc_ro=0, mc_vmid=0, **({'wptr_overflow_enable': 1, 'rptr_rearm': 1} if ring_id == 0 else {'rb_full_drain_enable': 1}))
|
||||
|
||||
if ring_id == 0: self.adev.wreg_pair("regIH_RB_WPTR_ADDR", "_LO", "_HI", self.adev.paddr2mc(rwptr_vm))
|
||||
|
||||
self.adev.reg(f"regIH_RB_WPTR{suf}").write(0)
|
||||
self.adev.reg(f"regIH_RB_RPTR{suf}").write(0)
|
||||
|
||||
self.adev.reg(f"regIH_DOORBELL_RPTR{suf}").write(enable=0)
|
||||
|
||||
if self.adev.ip_ver[am.OSSSYS_HWIP] != (4,4,2):
|
||||
self.adev.regIH_STORM_CLIENT_LIST_CNTL.update(client18_is_storm_client=1)
|
||||
self.adev.regIH_INT_FLOOD_CNTL.update(flood_cntl_enable=1)
|
||||
self.adev.regIH_MSI_STORM_CTRL.update(delay=3)
|
||||
|
||||
# toggle interrupts
|
||||
for _, rwptr_vm, suf, ring_id in self.rings:
|
||||
self.adev.reg(f"regIH_RB_CNTL{suf}").update(rb_enable=1, **({'enable_intr': 1} if ring_id == 0 else {}))
|
||||
|
||||
def drain(self):
|
||||
_, _, suf, _ = self.rings[0]
|
||||
wptr = self.adev.reg(f"regIH_RB_WPTR{suf}").read_bitfields()
|
||||
self.adev.regIH_RB_RPTR.write(wptr['offset'] % (self.ring_size // 4))
|
||||
|
||||
if wptr['rb_overflow']:
|
||||
self.adev.reg(f"regIH_RB_WPTR{suf}").update(rb_overflow=0)
|
||||
self.adev.reg(f"regIH_RB_CNTL{suf}").update(wptr_overflow_clear=1)
|
||||
self.adev.reg(f"regIH_RB_CNTL{suf}").update(wptr_overflow_clear=0)
|
||||
|
||||
def interrupt_handler(self):
|
||||
_, _, suf, _ = self.rings[0]
|
||||
wptr = self.adev.reg(f"regIH_RB_WPTR{suf}").read_bitfields()
|
||||
rptr = self.adev.regIH_RB_RPTR.read()
|
||||
|
||||
while rptr != wptr['offset']:
|
||||
entry = [self.ring_view[(rptr + i) % (self.ring_size // 4)] for i in range(8)]
|
||||
rptr = (rptr + 8) % (self.ring_size // 4)
|
||||
|
||||
client, src, ring_id, vmid, vmid_type, pasid, node = \
|
||||
[getattr(am, f'SOC15_{n}_FROM_IH_ENTRY')(entry) for n in ['CLIENT_ID', 'SOURCE_ID', 'RING_ID', 'VMID', 'VMID_TYPE', 'PASID', 'NODEID']]
|
||||
ctx = [getattr(am, f'SOC15_CONTEXT_ID{i}_FROM_IH_ENTRY')(entry) for i in range(4)]
|
||||
|
||||
src_name = self.adev.soc.ih_srcs_names.get(client, {}).get(src, '')
|
||||
if src_name in {"SDMA_TRAP", "CP_EOP_INTR"}: continue
|
||||
|
||||
print(f"am {self.adev.devfmt}: IH ({rptr:#x}/{wptr['offset']:#x}) client={self.adev.soc.ih_clients.get(client)} src={src_name}({src}) "
|
||||
f"ring={ring_id} vmid={vmid}({vmid_type}) pasid={pasid} node={node} ctx=[{ctx[0]:#x}, {ctx[1]:#x}, {ctx[2]:#x}, {ctx[3]:#x}]")
|
||||
|
||||
if src_name == "SQ_INTERRUPT_ID":
|
||||
enc_type = getbits(ctx[1], 6, 7) if (is_soc21:=self.adev.ip_ver[am.GC_HWIP][0] >= 11) else getbits(ctx[0], 26, 27)
|
||||
err_type = getbits(ctx[0], 21, 24) if is_soc21 else getbits((ctx[0] & 0xfff) | ((ctx[0]>>16) & 0xf000) | ((ctx[1]<<16) & 0xff0000), 20, 23)
|
||||
err_info = f" ({['EDC_FUE', 'ILLEGAL_INST', 'MEMVIOL', 'EDC_FED'][err_type]})" if enc_type == 2 else ""
|
||||
print(f"am {self.adev.devfmt}: sq_intr: {['auto', 'wave', 'error'][enc_type]}{err_info}")
|
||||
self.adev.is_err_state |= enc_type == 2
|
||||
elif src_name == "UTCL2_FAULT" or (self.adev.ip_ver[am.GC_HWIP][0] == 9 and client == am.SOC15_IH_CLIENTID_UTCL2):
|
||||
bf = self.adev.reg(self.adev.gmc.pf_status_reg('GC')).read_bitfields()
|
||||
va = (self.adev.reg('regGCVM_L2_PROTECTION_FAULT_ADDR_HI32').read()<<32) | self.adev.reg('regGCVM_L2_PROTECTION_FAULT_ADDR_LO32').read()
|
||||
print(f"am {self.adev.devfmt}: GCVM_L2_PROTECTION_FAULT_STATUS: {bf} {va<<12:#x}")
|
||||
self.adev.reg('regGCVM_L2_PROTECTION_FAULT_CNTL').update(clear_protection_fault_status_addr=1)
|
||||
self.adev.is_err_state = True
|
||||
else: self.adev.is_err_state = True
|
||||
|
||||
self.drain()
|
||||
|
||||
bif_intr = self.adev.regBIF_BX0_BIF_DOORBELL_INT_CNTL.read_bitfields()
|
||||
athub_err, cntlr_err = bif_intr['ras_athub_err_event_interrupt_status'], bif_intr['ras_cntlr_interrupt_status']
|
||||
if athub_err or cntlr_err:
|
||||
print(f"am {self.adev.devfmt}: fatal hardware error detected: {'RAS_ATHUB_ERR_EVENT ' if athub_err else ''}{'RAS_CNTLR' if cntlr_err else ''}")
|
||||
|
||||
acas = self.adev.smu._aca_read_banks(ue=True) + self.adev.smu._aca_read_banks(ue=False)
|
||||
for regs in acas:
|
||||
acatyp = 'Uncorrectable' if (regs[1] >> 61) & 1 and (regs[1] >> 57) & 1 else 'Correctable'
|
||||
hwname = f'{self.adev.hwid_names.get((regs[5] >> 32) & 0xFFF, "")} ({(regs[5] >> 32) & 0xFFF:#03x})'
|
||||
print(f"am {self.adev.devfmt}: {acatyp} ACA: {hwname} mcatype={(regs[5] >> 48) & 0xFFFF:#06x} regs=[{', '.join(f'{r:#x}' for r in regs)}]")
|
||||
|
||||
self.adev.regBIF_BX0_BIF_DOORBELL_INT_CNTL.write(ras_cntlr_interrupt_clear=cntlr_err, ras_athub_err_event_interrupt_clear=athub_err)
|
||||
self.adev.is_err_state = True
|
||||
|
||||
class AM_SDMA(AM_IP):
|
||||
def init_sw(self): self.sdma_reginst, self.sdma_name = [], "F32" if self.adev.ip_ver[am.SDMA0_HWIP] < (7,0,0) else "MCU"
|
||||
def init_hw(self):
|
||||
for pipe_id in range(16 if self.adev.ip_ver[am.SDMA0_HWIP] < (5,0,0) else 1):
|
||||
pipe, inst = ("", pipe_id) if self.adev.ip_ver[am.SDMA0_HWIP] < (5,0,0) else (str(pipe_id), 0)
|
||||
|
||||
if self.adev.ip_ver[am.SDMA0_HWIP] >= (6,0,0):
|
||||
self.adev.reg(f"regSDMA{pipe}_WATCHDOG_CNTL").update(queue_hang_count=100, inst=inst) # 10s, 100ms per unit
|
||||
self.adev.reg(f"regSDMA{pipe}_UTCL1_CNTL").update(resp_mode=3, redo_delay=9, inst=inst)
|
||||
|
||||
# rd=noa, wr=bypass
|
||||
self.adev.reg(f"regSDMA{pipe}_UTCL1_PAGE").update(rd_l2_policy=2, wr_l2_policy=3, **({'llc_noalloc':1} if self.sdma_name == "F32" else {}),
|
||||
inst=inst)
|
||||
self.adev.reg(f"regSDMA{pipe}_{self.sdma_name}_CNTL").update(halt=0, **{f"{'th1_' if self.sdma_name == 'F32' else ''}reset":0}, inst=inst)
|
||||
|
||||
self.adev.reg(f"regSDMA{pipe}_CNTL").update(trap_enable=1,
|
||||
**({'utc_l1_enable':1} if self.adev.ip_ver[am.SDMA0_HWIP] <= (5,2,0) else {}), inst=inst)
|
||||
|
||||
if self.adev.ip_ver[am.NBIO_HWIP] in {(7,9,0), (7,9,1)}:
|
||||
for aid_id in range(4):
|
||||
for dev_inst, (port, awid, offset, awaddr) in enumerate([(1, 0xe, 0xe, 0x1), (2, 0x8, 0x8, 0x2), (5, 0x9, 0x9, 0x8), (6, 0xa, 0xa, 0x9)]):
|
||||
entry = dev_inst + 1 + 4 * aid_id
|
||||
self.adev.reg(f"regDOORBELL0_CTRL_ENTRY_{entry}").write(**{f"bif_doorbell{entry}_range_size_entry": 20,
|
||||
f"bif_doorbell{entry}_range_offset_entry": (am.AMDGPU_NAVI10_DOORBELL_sDMA_ENGINE0 + (entry - 1) * 0xA) * 2})
|
||||
self.adev.soc.doorbell_enable(port=port, awid=awid, awaddr_31_28_value=awaddr, offset=offset, size=4, aid=aid_id)
|
||||
else: self.adev.soc.doorbell_enable(port=2, awid=0xe, awaddr_31_28_value=0x3, offset=am.AMDGPU_NAVI10_DOORBELL_sDMA_ENGINE0*2, size=4)
|
||||
|
||||
def fini_hw(self):
|
||||
for reg, inst in self.sdma_reginst:
|
||||
self.adev.reg(f"{reg}_RB_CNTL").update(rb_enable=0, inst=inst)
|
||||
self.adev.reg(f"{reg}_IB_CNTL").update(ib_enable=0, inst=inst)
|
||||
self.adev.reg(f"{reg}_DOORBELL").update(enable=0, inst=inst)
|
||||
self.adev.reg(f"{reg}_DOORBELL_OFFSET").update(offset=0, inst=inst)
|
||||
|
||||
if self.adev.ip_ver[am.SDMA0_HWIP] >= (6,0,0):
|
||||
self.adev.regGRBM_SOFT_RESET.write(soft_reset_sdma0=1)
|
||||
time.sleep(0.01)
|
||||
self.adev.regGRBM_SOFT_RESET.write(0x0)
|
||||
|
||||
def setup_ring(self, ring_addr:int, ring_size:int, rptr_addr:int, wptr_addr:int, idx:int) -> int:
|
||||
if self.adev.ip_ver[am.SDMA0_HWIP] >= (5,0,0) and idx > 0: raise RuntimeError(f"am {self.adev.devfmt}: sdma queue {idx} is not available")
|
||||
|
||||
pipe, queue = idx // 4, idx % 4
|
||||
reg, inst = ("regSDMA_GFX", pipe+queue*4) if self.adev.ip_ver[am.SDMA0_HWIP][:2] == (4,4) else (f"regSDMA{pipe}_QUEUE{queue}", 0)
|
||||
doorbell = am.AMDGPU_NAVI10_DOORBELL_sDMA_ENGINE0 + (pipe+queue*4) * 0xA
|
||||
self.sdma_reginst.append((reg, inst))
|
||||
|
||||
self.adev.reg(f"{reg}_MINOR_PTR_UPDATE").write(0x1, inst=inst)
|
||||
self.adev.wreg_pair(f"{reg}_RB_RPTR", "", "_HI", 0, inst=inst)
|
||||
self.adev.wreg_pair(f"{reg}_RB_WPTR", "", "_HI", 0, inst=inst)
|
||||
self.adev.wreg_pair(f"{reg}_RB_BASE", "", "_HI", ring_addr >> 8, inst=inst)
|
||||
self.adev.wreg_pair(f"{reg}_RB_RPTR_ADDR", "_LO", "_HI", rptr_addr, inst=inst)
|
||||
self.adev.wreg_pair(f"{reg}_RB_WPTR_POLL_ADDR", "_LO", "_HI", wptr_addr, inst=inst)
|
||||
self.adev.reg(f"{reg}_DOORBELL_OFFSET").update(offset=doorbell * 2, inst=inst)
|
||||
self.adev.reg(f"{reg}_DOORBELL").update(enable=1, inst=inst)
|
||||
self.adev.reg(f"{reg}_MINOR_PTR_UPDATE").write(0x0, inst=inst)
|
||||
self.adev.reg(f"{reg}_RB_CNTL").write(**({f'{self.sdma_name.lower()}_wptr_poll_enable':1} if self.adev.ip_ver[am.SDMA0_HWIP][:2]!=(4,4) else {}),
|
||||
rb_vmid=0, rptr_writeback_enable=1, rptr_writeback_timer=4, rb_enable=1, rb_priv=1, rb_size=(ring_size//4).bit_length()-1, inst=inst)
|
||||
self.adev.reg(f"{reg}_IB_CNTL").update(ib_enable=1, inst=inst)
|
||||
return doorbell
|
||||
|
||||
class AM_PSP(AM_IP):
|
||||
def init_sw(self):
|
||||
self.reg_pref = "regMP0_SMN_C2PMSG" if self.adev.ip_ver[am.MP0_HWIP] < (14,0,0) else "regMPASP_SMN_C2PMSG"
|
||||
|
||||
if self.adev.devfmt.startswith("usb:"):
|
||||
self.msg1_view, paddrs = self.adev.pci_dev.alloc_sysmem(512 << 10)
|
||||
self.msg1_addr = self.adev.mm.alloc_vaddr(size=self.msg1_view.nbytes, align=am.PSP_1_MEG)
|
||||
self.adev.mm.map_range(self.msg1_addr, self.msg1_view.nbytes, [(paddrs[0], self.msg1_view.nbytes)], AddrSpace.SYS, uncached=True, boot=True)
|
||||
else:
|
||||
self.msg1_paddr = self.adev.mm.palloc(am.PSP_1_MEG, align=am.PSP_1_MEG, zero=False, boot=True)
|
||||
self.msg1_addr, self.msg1_view = self.adev.paddr2mc(self.msg1_paddr), self.adev.vram.view(self.msg1_paddr, am.PSP_1_MEG, 'B')
|
||||
|
||||
self.cmd_paddr = self.adev.mm.palloc(am.PSP_CMD_BUFFER_SIZE, zero=False, boot=True)
|
||||
self.fence_paddr = self.adev.mm.palloc(am.PSP_FENCE_BUFFER_SIZE, zero=True, boot=True)
|
||||
|
||||
self.ring_size = 0x10000
|
||||
self.ring_paddr = self.adev.mm.palloc(self.ring_size, zero=False, boot=True)
|
||||
|
||||
self.max_tmr_size, self.tmr_size = 0x1300000, 0
|
||||
self.boot_time_tmr = self.adev.ip_ver[am.MP0_HWIP] in {(13,0,6), (13,0,14), (14,0,2), (14,0,3)}
|
||||
self.autoload_tmr = self.adev.ip_ver[am.MP0_HWIP] not in {(13,0,6), (13,0,14)}
|
||||
self.tmr_paddr = self.adev.mm.palloc(self.max_tmr_size, align=am.PSP_TMR_ALIGNMENT, zero=False, boot=True) if not self.boot_time_tmr else 0
|
||||
|
||||
def init_hw(self):
|
||||
spl_key = am.PSP_FW_TYPE_PSP_SPL if self.adev.ip_ver[am.MP0_HWIP] >= (14,0,0) else am.PSP_FW_TYPE_PSP_KDB
|
||||
sos_components = [(am.PSP_FW_TYPE_PSP_KDB, am.PSP_BL__LOAD_KEY_DATABASE), (spl_key, am.PSP_BL__LOAD_TOS_SPL_TABLE),
|
||||
(am.PSP_FW_TYPE_PSP_SYS_DRV, am.PSP_BL__LOAD_SYSDRV), (am.PSP_FW_TYPE_PSP_SOC_DRV, am.PSP_BL__LOAD_SOCDRV),
|
||||
(am.PSP_FW_TYPE_PSP_INTF_DRV, am.PSP_BL__LOAD_INTFDRV), (am.PSP_FW_TYPE_PSP_DBG_DRV, am.PSP_BL__LOAD_DBGDRV),
|
||||
(am.PSP_FW_TYPE_PSP_RAS_DRV, am.PSP_BL__LOAD_RASDRV), (am.PSP_FW_TYPE_PSP_SOS, am.PSP_BL__LOAD_SOSDRV)]
|
||||
|
||||
if not self.is_sos_alive():
|
||||
for fw, compid in sos_components: self._bootloader_load_component(fw, compid)
|
||||
wait_cond(self.is_sos_alive, value=True, msg="sOS failed to start")
|
||||
|
||||
self._ring_create()
|
||||
if am.PSP_FW_TYPE_PSP_TOC in self.adev.fw.sos_fw: self._tmr_init()
|
||||
|
||||
# SMU fw should be loaded before TMR.
|
||||
if hasattr(self.adev.fw, 'smu_psp_desc'): self._load_ip_fw_cmd(*self.adev.fw.smu_psp_desc)
|
||||
if not self.boot_time_tmr or not self.autoload_tmr: self._tmr_load_cmd()
|
||||
|
||||
for psp_desc in self.adev.fw.descs: self._load_ip_fw_cmd(*psp_desc)
|
||||
|
||||
if self.adev.ip_ver[am.GC_HWIP] >= (11,0,0): self._rlc_autoload_cmd()
|
||||
else: self._load_ip_fw_cmd([am.GFX_FW_TYPE_REG_LIST], self.adev.fw.sos_fw[am.PSP_FW_TYPE_PSP_RL])
|
||||
|
||||
def is_sos_alive(self): return self.adev.reg(f"{self.reg_pref}_81").read() != 0x0
|
||||
|
||||
def _wait_for_bootloader(self): wait_cond(lambda: self.adev.reg(f"{self.reg_pref}_35").read() & 0x80000000, value=0x80000000, msg="BL not ready")
|
||||
|
||||
def _prep_msg1(self, data:memoryview):
|
||||
assert len(data) <= self.msg1_view.nbytes, f"msg1 buffer is too small {len(data):#x} > {self.msg1_view.nbytes:#x}"
|
||||
padded_data = pad_bytes(bytes(data) + b'\x00' * 4, 16) # HACK: apple's memcpy requires 16-bytes alignment
|
||||
self.msg1_view[:len(padded_data)] = padded_data
|
||||
self.adev.gmc.flush_hdp()
|
||||
|
||||
def _bootloader_load_component(self, fw:int, compid:int):
|
||||
if fw not in self.adev.fw.sos_fw: return 0
|
||||
|
||||
self._wait_for_bootloader()
|
||||
|
||||
if DEBUG >= 2: print(f"am {self.adev.devfmt}: loading sos component: {am.enum_psp_fw_type.get(fw)}")
|
||||
|
||||
self._prep_msg1(self.adev.fw.sos_fw[fw])
|
||||
self.adev.reg(f"{self.reg_pref}_36").write(self.msg1_addr >> 20)
|
||||
self.adev.reg(f"{self.reg_pref}_35").write(compid)
|
||||
|
||||
return self._wait_for_bootloader() if compid != am.PSP_BL__LOAD_SOSDRV else 0
|
||||
|
||||
def _tmr_init(self):
|
||||
# Load TOC and calculate TMR size
|
||||
self._prep_msg1(fwm:=self.adev.fw.sos_fw[am.PSP_FW_TYPE_PSP_TOC])
|
||||
self.tmr_size = self._load_toc_cmd(len(fwm)).resp.tmr_size
|
||||
assert self.tmr_size <= self.max_tmr_size
|
||||
|
||||
def _ring_create(self):
|
||||
# If the ring is already created, destroy it
|
||||
if self.adev.reg(f"{self.reg_pref}_71").read() != 0:
|
||||
self.adev.reg(f"{self.reg_pref}_64").write(am.GFX_CTRL_CMD_ID_DESTROY_RINGS)
|
||||
|
||||
# There might be handshake issue with hardware which needs delay
|
||||
time.sleep(0.02)
|
||||
|
||||
# Wait until the sOS is ready
|
||||
wait_cond(lambda: self.adev.reg(f"{self.reg_pref}_64").read() & 0x80000000, value=0x80000000, msg="sOS not ready")
|
||||
|
||||
self.adev.wreg_pair(self.reg_pref, "_69", "_70", self.adev.paddr2mc(self.ring_paddr))
|
||||
self.adev.reg(f"{self.reg_pref}_71").write(self.ring_size)
|
||||
self.adev.reg(f"{self.reg_pref}_64").write(am.PSP_RING_TYPE__KM << 16)
|
||||
|
||||
# There might be handshake issue with hardware which needs delay
|
||||
time.sleep(0.02)
|
||||
|
||||
wait_cond(lambda: self.adev.reg(f"{self.reg_pref}_64").read() & 0x8000FFFF, value=0x80000000, msg="sOS ring not created")
|
||||
|
||||
def _ring_submit(self, cmd:am.struct_psp_gfx_cmd_resp) -> am.struct_psp_gfx_cmd_resp:
|
||||
msg = am.struct_psp_gfx_rb_frame(fence_value=(prev_wptr:=self.adev.reg(f"{self.reg_pref}_67").read()) + 1,
|
||||
cmd_buf_addr_lo=lo32(self.adev.paddr2mc(self.cmd_paddr)), cmd_buf_addr_hi=hi32(self.adev.paddr2mc(self.cmd_paddr)),
|
||||
fence_addr_lo=lo32(self.adev.paddr2mc(self.fence_paddr)), fence_addr_hi=hi32(self.adev.paddr2mc(self.fence_paddr)))
|
||||
|
||||
self.adev.vram.view(self.cmd_paddr, ctypes.sizeof(cmd))[:] = memoryview(cmd).cast('B')
|
||||
self.adev.vram.view(self.ring_paddr + prev_wptr * 4, ctypes.sizeof(msg))[:] = memoryview(msg).cast('B')
|
||||
|
||||
# Move the wptr
|
||||
self.adev.reg(f"{self.reg_pref}_67").write(prev_wptr + ctypes.sizeof(am.struct_psp_gfx_rb_frame) // 4)
|
||||
|
||||
wait_cond(lambda: self.adev.vram.view(self.fence_paddr, 4, 'I')[0], value=msg.fence_value, msg="sOS ring not responding")
|
||||
|
||||
resp = type(cmd).from_buffer(bytearray(self.adev.vram.view(self.cmd_paddr, ctypes.sizeof(cmd))[:]))
|
||||
if resp.resp.status != 0: raise RuntimeError(f"PSP command failed {resp.cmd_id} {resp.resp.status}")
|
||||
|
||||
return resp
|
||||
|
||||
def _load_ip_fw_cmd(self, fw_types:list[int], fw_bytes:memoryview):
|
||||
self._prep_msg1(fw_bytes)
|
||||
for fw_type in fw_types:
|
||||
if DEBUG >= 2: print(f"am {self.adev.devfmt}: loading fw: {am.enum_psp_gfx_fw_type.get(fw_type)}")
|
||||
cmd = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_IP_FW)
|
||||
cmd.cmd.cmd_load_ip_fw.fw_phy_addr_hi, cmd.cmd.cmd_load_ip_fw.fw_phy_addr_lo = data64(self.msg1_addr)
|
||||
cmd.cmd.cmd_load_ip_fw.fw_size = len(fw_bytes)
|
||||
cmd.cmd.cmd_load_ip_fw.fw_type = fw_type
|
||||
self._ring_submit(cmd)
|
||||
|
||||
def _tmr_load_cmd(self) -> am.struct_psp_gfx_cmd_resp:
|
||||
tmr_paddr = self.adev.paddr2xgmi(self.tmr_paddr) if self.tmr_paddr else 0
|
||||
|
||||
cmd = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_SETUP_TMR)
|
||||
cmd.cmd.cmd_setup_tmr.buf_phy_addr_hi, cmd.cmd.cmd_setup_tmr.buf_phy_addr_lo = data64(self.adev.paddr2mc(self.tmr_paddr) if self.tmr_paddr else 0)
|
||||
cmd.cmd.cmd_setup_tmr.system_phy_addr_hi, cmd.cmd.cmd_setup_tmr.system_phy_addr_lo = data64(tmr_paddr)
|
||||
cmd.cmd.cmd_setup_tmr.bitfield.virt_phy_addr = 1
|
||||
cmd.cmd.cmd_setup_tmr.buf_size = self.tmr_size if self.tmr_paddr else 0
|
||||
return self._ring_submit(cmd)
|
||||
|
||||
def _load_toc_cmd(self, toc_size:int) -> am.struct_psp_gfx_cmd_resp:
|
||||
cmd = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_LOAD_TOC)
|
||||
cmd.cmd.cmd_load_toc.toc_phy_addr_hi, cmd.cmd.cmd_load_toc.toc_phy_addr_lo = data64(self.msg1_addr)
|
||||
cmd.cmd.cmd_load_toc.toc_size = toc_size
|
||||
return self._ring_submit(cmd)
|
||||
|
||||
def _spatial_partition_cmd(self, mode):
|
||||
cmd = am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_SRIOV_SPATIAL_PART)
|
||||
cmd.cmd.cmd_spatial_part.mode = mode
|
||||
return self._ring_submit(cmd)
|
||||
|
||||
def _rlc_autoload_cmd(self): return self._ring_submit(am.struct_psp_gfx_cmd_resp(cmd_id=am.GFX_CMD_ID_AUTOLOAD_RLC))
|
||||
47
tinygrad_repo/tinygrad/runtime/support/amd.py
Normal file
47
tinygrad_repo/tinygrad/runtime/support/amd.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import functools, tinygrad.runtime.autogen.am
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.helpers import getbits
|
||||
|
||||
@dataclass
|
||||
class AMDReg:
|
||||
name:str; offset:int; segment:int; fields:dict[str, tuple[int, int]]; bases:dict[int, tuple[int, ...]] # noqa: E702
|
||||
def __post_init__(self): self.addr:dict[int, int] = { inst: bases[self.segment] + self.offset for inst, bases in self.bases.items() }
|
||||
|
||||
def encode(self, **kwargs) -> int: return functools.reduce(int.__or__, (value << self.fields[name][0] for name,value in kwargs.items()), 0)
|
||||
def decode(self, val: int) -> dict: return {name:getbits(val, start, end) for name,(start,end) in self.fields.items()}
|
||||
|
||||
def fields_mask(self, *names) -> int:
|
||||
return functools.reduce(int.__or__, ((((1 << (self.fields[nm][1]-self.fields[nm][0]+1)) - 1) << self.fields[nm][0]) for nm in names), 0)
|
||||
|
||||
@dataclass
|
||||
class AMDIP:
|
||||
name:str; version:tuple[int, int, int]; bases:dict[int, tuple[int, ...]] # noqa: E702
|
||||
|
||||
@functools.cached_property
|
||||
def regs(self): return import_asic_regs(self.name, self.version, cls=functools.partial(AMDReg, bases=self.bases))
|
||||
|
||||
def __getattr__(self, name:str):
|
||||
if name in self.regs: return self.regs[name]
|
||||
if (name10:=name.replace('reg', 'mm')) in self.regs: return self.regs[name10]
|
||||
raise AttributeError(f"{self.name.upper()} has no register {name}")
|
||||
|
||||
# load the greatest module with matching major version that's less than or equal to the target version
|
||||
# this is not universally correct, see below for an example, but appears reliable for most recent gpus
|
||||
# https://github.com/torvalds/linux/blob/9207d47f966be9f4d52e7e0119ac2b7a7e366f3e/drivers/gpu/drm/amd/amdgpu/amdgpu_discovery.c#L3163
|
||||
def import_module(name:str, target:tuple[int, int, int], submod=""):
|
||||
# version overrides
|
||||
target = {("smu", (13, 0, 7)): (13, 0, 0)}.get((name, target), target)
|
||||
mod = getattr(tinygrad.runtime.autogen.am, submod) if submod else tinygrad.runtime.autogen.am
|
||||
if (children:=[c for c in mod.__all__ if c.startswith(name) and (v:=tuple(map(int, c.split('_')[1:])))[0] == target[0] and v <= target]):
|
||||
return getattr(mod, children[-1])
|
||||
raise ImportError(f"Failed to import {submod+'.' if submod else ''}{name} {'.'.join(map(str, target))}")
|
||||
|
||||
def import_soc(ip): return getattr(tinygrad.runtime.autogen.am, f"soc_{ip[0]}")
|
||||
|
||||
def import_pmc(ip) -> dict[str, tuple[str, int]]:
|
||||
from tinygrad.runtime.autogen.am import pmc
|
||||
# NOTE: precise arch for mi300+, generic for others, since rocm headers lack some archs
|
||||
return {k:x for k,v in pmc.counters.items() if (x:=v.get(f"gfx{ip[0]}{ip[1]:x}{ip[2]:x}" if ip[0] == 9 else f"gfx{ip[0]}", None)) is not None}
|
||||
|
||||
def import_asic_regs(prefix:str, version:tuple[int, int, int], cls=AMDReg) -> dict[str, AMDReg]:
|
||||
return {reg:cls(name=reg, offset=off, segment=seg, fields=fields) for reg,(off,seg,fields) in import_module(prefix, version, submod="regs").items()}
|
||||
289
tinygrad_repo/tinygrad/runtime/support/autogen.py
Normal file
289
tinygrad_repo/tinygrad/runtime/support/autogen.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import ctypes, itertools, re, functools, os, keyword
|
||||
from tinygrad.helpers import unwrap
|
||||
import tinygrad.runtime.autogen.libclang as clang # use REGEN=1 to regenerate libclang bindings
|
||||
|
||||
def unwrap_cursor(c: clang.CXCursor) -> clang.CXCursor:
|
||||
assert c != clang.clang_getNullCursor()
|
||||
return c
|
||||
|
||||
def children(c: clang.CXCursor) -> list[clang.CXCursor]:
|
||||
ret = []
|
||||
@clang.CXCursorVisitor
|
||||
def visitor(child, _0, _1):
|
||||
nonlocal ret
|
||||
ret.append(child)
|
||||
return clang.CXChildVisit_Continue
|
||||
clang.clang_visitChildren(c, visitor, None)
|
||||
return ret
|
||||
|
||||
def fields(t: clang.CXType) -> list[clang.CXCursor]:
|
||||
ret = []
|
||||
@clang.CXFieldVisitor
|
||||
def visitor(child, _):
|
||||
nonlocal ret
|
||||
ret.append(child)
|
||||
return clang.CXVisit_Continue
|
||||
clang.clang_Type_visitFields(t, visitor, None)
|
||||
return ret
|
||||
|
||||
# flattens anonymous structs/unions
|
||||
def all_fields(t, off=0):
|
||||
for f in fields(t):
|
||||
if clang.clang_Cursor_isAnonymousRecordDecl(clang.clang_getTypeDeclaration(clang.clang_getCursorType(f))):
|
||||
yield from all_fields(clang.clang_getCursorType(f), off + clang.clang_Cursor_getOffsetOfField(f) // 8)
|
||||
elif nm(f): yield f, off+clang.clang_Cursor_getOffsetOfField(f) // 8 # ignores unnamed fields
|
||||
|
||||
def arguments(c: clang.CXCursor|clang.CXType):
|
||||
yield from ((clang.clang_Cursor_getArgument if isinstance(c, clang.CXCursor) else clang.clang_getArgType)(c, i)
|
||||
for i in range(clang.clang_Cursor_getNumArguments(c) if isinstance(c, clang.CXCursor) else clang.clang_getNumArgTypes(c)))
|
||||
|
||||
class Tokens:
|
||||
def __init__(self, c: clang.CXCursor):
|
||||
clang.clang_tokenize(tu:=clang.clang_Cursor_getTranslationUnit(c), clang.clang_getCursorExtent(c),
|
||||
toks:=(ctypes.POINTER(clang.CXToken)()), cnt:=ctypes.c_uint32())
|
||||
self.tu, self.toks = tu, toks[:cnt.value]
|
||||
for t in self.toks: t._tu = tu
|
||||
|
||||
def __getitem__(self, idx): return self.toks[idx]
|
||||
def __len__(self): return len(self.toks)
|
||||
|
||||
def __del__(self):
|
||||
if self.toks: clang.clang_disposeTokens(self.tu, self.toks[0], len(self.toks))
|
||||
|
||||
def cxs(fn):
|
||||
@functools.wraps(fn)
|
||||
def wrap(*args, **kwargs) -> str:
|
||||
if ctypes.cast(clang.clang_getCString(cxs:=fn(*args, **kwargs)), ctypes.c_void_p).value is None: return ""
|
||||
ret = ctypes.string_at(clang.clang_getCString(cxs)).decode()
|
||||
clang.clang_disposeString(cxs)
|
||||
return ret
|
||||
return wrap
|
||||
|
||||
# TODO: caching this would be nice?
|
||||
nm = cxs(lambda c: getattr(clang, f"clang_get{c.__class__.__name__[2:]}Spelling")(*([c._tu, c] if isinstance(c, clang.CXToken) else [c])))
|
||||
def extent(c): return getattr(clang, f"clang_get{c.__class__.__name__[2:]}Extent")(*([c._tu, c] if isinstance(c, clang.CXToken) else [c]))
|
||||
def loc(c): return getattr(clang, f"clang_get{c.__class__.__name__[2:]}Location")(*([c._tu, c] if isinstance(c, clang.CXToken) else [c]))
|
||||
def gel(loc: clang.CXSourceLocation):
|
||||
clang.clang_getExpansionLocation(loc, file:=clang.CXFile(), line:=ctypes.c_uint32(), None, offset:=ctypes.c_uint32())
|
||||
return {"file":clang.clang_getFileName(file), "line":line.value, "offset":offset.value}
|
||||
loc_file = cxs(lambda loc: gel(loc)['file'])
|
||||
def loc_off(loc: clang.CXSourceLocation) -> int: return gel(loc)['offset']
|
||||
def loc_line(loc: clang.CXSourceLocation) -> int: return gel(loc)['line']
|
||||
|
||||
def readext(f, fst, snd=None):
|
||||
with open(f, "r") as f: # reopening this every time is dumb...
|
||||
f.seek(start:=loc_off(clang.clang_getRangeStart(fst) if isinstance(fst, clang.CXSourceRange) else fst))
|
||||
return f.read(loc_off(clang.clang_getRangeEnd(fst) if isinstance(fst, clang.CXSourceRange) else snd)-start)
|
||||
def attrs(c): return list(filter(lambda k: k >= 400 and k < 500, map(lambda c: c.kind, children(c))))
|
||||
|
||||
def protocols(t): yield from (clang.clang_Type_getObjCProtocolDecl(t, i) for i in range(clang.clang_Type_getNumObjCProtocolRefs(t)))
|
||||
def basetype(t): return clang.clang_Type_getObjCObjectBaseType(t)
|
||||
|
||||
base_rules = [(r'\s*\\\n\s*', ' '), (r'\s*\n\s*', ' '), (r'//.*', ''), (r'/\*.*?\*/', ''), (r'\b(0[xX][0-9a-fA-F]+|\d+)[uUlL]+\b', r'\1'),
|
||||
(r'\b0+(?=\d)', ''), (r'\s*&&\s*', r' and '), (r'\s*\|\|\s*', r' or '), (r'\s*!\s*', ' not '),
|
||||
(r'(struct|union|enum)\s*([a-zA-Z_][a-zA-Z0-9_]*\b)', r'\1_\2'),
|
||||
(r'\((unsigned )?(char|uint64_t)\)', ''), (r'^.*\d+:\d+.*$', ''), (r'^.*\w##\w.*$', '')]
|
||||
|
||||
uints = (clang.CXType_Char_U, clang.CXType_UChar, clang.CXType_UShort, clang.CXType_UInt, clang.CXType_ULong, clang.CXType_ULongLong)
|
||||
ints = uints + (clang.CXType_Char_S, clang.CXType_SChar, clang.CXType_Short, clang.CXType_Int, clang.CXType_Long, clang.CXType_LongLong)
|
||||
fps, specs = (clang.CXType_FunctionProto, clang.CXType_FunctionNoProto), (clang.CXCursor_ObjCSuperClassRef,) # this could include protocols
|
||||
|
||||
tmap = {clang.CXType_Void:"None", clang.CXType_Char_U:"ctypes.c_ubyte", clang.CXType_UChar:"ctypes.c_ubyte", clang.CXType_WChar:"ctypes.c_wchar",
|
||||
clang.CXType_Char_S:"ctypes.c_char", clang.CXType_SChar:"ctypes.c_byte", clang.CXType_Bool:"ctypes.c_bool",
|
||||
**{getattr(clang, f'CXType_{k}'):f"ctypes.c_{k.lower()}" for k in ["Float", "Double", "LongDouble"]},
|
||||
**{getattr(clang, f'CXType_{k}'):f"ctypes.c_{'u' if 'U' in k else ''}int{sz}" for sz,k in
|
||||
[(16, "UShort"), (16, "Short"), (32, "UInt"), (32, "Int"), (64, "ULong"), (64, "Long"), (64, "ULongLong"), (64, "LongLong")]}}
|
||||
|
||||
# https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-method-families
|
||||
arc_families = ['alloc', 'copy', 'mutableCopy', 'new']
|
||||
|
||||
def normalize(a): return ("_" + n if keyword.iskeyword(n:=nm(a)) else n)
|
||||
|
||||
def gen(name, files, dll="", args=[], prolog=[], rules=[], epilog=[], recsym=False, errno=False, anon_names={}, types={}, macros=True, paths=[],
|
||||
patterns=[]):
|
||||
extras, lines, anoncnt, types, objc, fns = [], [], itertools.count().__next__, {k:(v,True) for k,v in types.items()}, False, set()
|
||||
|
||||
# ctypes automatically "unboxes" simple types
|
||||
def typehint(ty) -> str:
|
||||
if (v:={**{i:"int" for i in ints}, **{getattr(clang, f"CXType_{f}"):"float" for f in ['Float', 'Double', 'LongDouble']}, clang.CXType_Enum:"int",
|
||||
clang.CXType_WChar:"str", clang.CXType_SChar:"int", clang.CXType_Char_S:"bytes", clang.CXType_Bool:"bool",}.get(ty.kind, None)): return v
|
||||
return tname(ty)
|
||||
|
||||
def tname(t, suggested_name=None, typedef=None) -> str:
|
||||
suggested_name = anon_names.get(f"{loc_file(loc(decl:=clang.clang_getTypeDeclaration(t)))}:{loc_line(loc(decl))}", suggested_name)
|
||||
nonlocal lines, types, anoncnt, objc
|
||||
|
||||
if t.kind in tmap: return tmap[t.kind]
|
||||
if nm(t) in types and types[nm(t)][1]: return types[nm(t)][0]
|
||||
if ((f:=t).kind in fps) or (t.kind == clang.CXType_Pointer and (f:=clang.clang_getPointeeType(t)).kind in fps):
|
||||
return (f"c.CFUNCTYPE[{tname(clang.clang_getResultType(f))}, [" + ', '.join(map(tname, arguments(f))) + "]]")
|
||||
match t.kind:
|
||||
case clang.CXType_Pointer:
|
||||
return "ctypes.c_void_p" if (p:=clang.clang_getPointeeType(t)).kind==clang.CXType_Void else f"c.POINTER[{tname(p)}]"
|
||||
case clang.CXType_ObjCObjectPointer: return tname(clang.clang_getPointeeType(t)) # TODO: this seems wrong
|
||||
case clang.CXType_Elaborated: return tname(clang.clang_Type_getNamedType(t), suggested_name)
|
||||
case clang.CXType_Typedef if nm(t) == nm(canon:=clang.clang_getCanonicalType(t)): return tname(canon)
|
||||
case clang.CXType_Typedef:
|
||||
defined, cnm = nm(canon:=clang.clang_getCanonicalType(t)) in types, tname(canon, typedef=nm(t))
|
||||
types[nm(t)] = cnm if nm(t).startswith("__") else nm(t).replace('::', '_'), True
|
||||
# RECORDs need to handle typedefs specially to allow for self-reference
|
||||
if canon.kind != clang.CXType_Record or defined: lines.append(f"{nm(t).replace('::', '_')}: TypeAlias = {cnm}")
|
||||
return types[nm(t)][0]
|
||||
case clang.CXType_Record:
|
||||
# TODO: packed unions
|
||||
# libclang does not use CXType_Elaborated for function parameters with type qualifiers (eg. void (*)(const struct foo))
|
||||
if (_nm:=re.sub(r"^const ", "", nm(t))) in types and types[_nm][1]: return types[_nm][0]
|
||||
|
||||
# check if previously declared
|
||||
if _nm in types: types[_nm] = (tnm:=types[_nm][0]), types[_nm][1] or len(fields(t)) != 0, (ln:=types[_nm][2])
|
||||
else:
|
||||
real_nm = ((suggested_name or (f"_anon{'struct' if decl.kind==clang.CXCursor_StructDecl else 'union'}{anoncnt()}"))
|
||||
if clang.clang_Cursor_isAnonymous(decl) else _nm)
|
||||
types[_nm] = (tnm:=real_nm.replace(' ', '_').replace('::', '_')), len(fields(t)) != 0, (ln:=len(lines))
|
||||
lines.append(f"class {tnm}(c.Struct): pass")
|
||||
if typedef:
|
||||
lines.append(f"{typedef.replace('::', '_')}: TypeAlias = {tnm}")
|
||||
types[typedef] = typedef.replace('::', '_'), True
|
||||
ff = [(normalize(f), clang.clang_getCanonicalType(ty:=clang.clang_getCursorType(f)), tname(ty, f"{tnm}_{nm(f)}"), offset) +
|
||||
((clang.clang_getFieldDeclBitWidth(f), clang.clang_Cursor_getOffsetOfField(f) % 8) * clang.clang_Cursor_isBitField(f))
|
||||
for f,offset in all_fields(t)]
|
||||
if ff:
|
||||
lines[ln] = "\n".join(["@c.record", f"class {tnm}(c.Struct):", f" SIZE = {clang.clang_Type_getSizeOf(t)}"] +
|
||||
[f" {f}: {typehint(ty)}" for f,ty,*args in ff])
|
||||
lines.append(f"{tnm}.register_fields([" + ", ".join([f"('{f}', {', '.join(str(a) for a in args)})" for f,ty,*args in ff]) + "])")
|
||||
return tnm
|
||||
case clang.CXType_Enum:
|
||||
# TODO: C++ and GNU C have forward declared enums
|
||||
types[nm(t)] = tname(ety:=clang.clang_getEnumDeclIntegerType(decl)), True
|
||||
enm = suggested_name or f"_anonenum{anoncnt()}" if clang.clang_Cursor_isAnonymous(decl) else nm(t).replace(' ', '_').replace('::', '_')
|
||||
def value(e): return (clang.clang_getEnumConstantDeclUnsignedValue if ety.kind in uints else clang.clang_getEnumConstantDeclValue)(e)
|
||||
lines.append(f"{enm}: dict[int, str] = {{" + ", ".join(f"({nm(e)}:={value(e)}): '{nm(e)}'" for e in children(decl)
|
||||
if e.kind == clang.CXCursor_EnumConstantDecl) + "}")
|
||||
return types[nm(t)][0]
|
||||
case clang.CXType_ConstantArray: return (f"c.Array[{tname(clang.clang_getArrayElementType(t), suggested_name and suggested_name.rstrip('s'))}, "
|
||||
f"Literal[{clang.clang_getArraySize(t)}]]")
|
||||
case clang.CXType_IncompleteArray:
|
||||
return f"c.Array[{tname(clang.clang_getArrayElementType(t), suggested_name and suggested_name.rstrip('s'))}, Literal[0]]"
|
||||
case clang.CXType_ObjCInterface:
|
||||
is_defn = bool([f.kind for f in children(decl) if f.kind in (clang.CXCursor_ObjCInstanceMethodDecl, clang.CXCursor_ObjCClassMethodDecl)])
|
||||
if (tnm:=nm(t)) not in types: lines.append(f"class {tnm}(objc.Spec): pass")
|
||||
types[tnm] = tnm, is_defn
|
||||
if is_defn:
|
||||
ims, cms = parse_objc_spec(decl, tnm, clang.CXCursor_ObjCInstanceMethodDecl), parse_objc_spec(decl, tnm, clang.CXCursor_ObjCClassMethodDecl)
|
||||
bases = [tname(clang.clang_getCursorType(b)) for b in children(decl) if b.kind in specs]
|
||||
lines.extend([*([f"{tnm}._bases_ = [{', '.join(bases)}]"] if bases else []),
|
||||
*([f"{tnm}._methods_ = [", *ims, ']'] if ims else []), *([f"{tnm}._classmethods_ = [", *cms, ']'] if cms else [])])
|
||||
return tnm
|
||||
case clang.CXType_ObjCSel: return "objc.id_"
|
||||
case clang.CXType_ObjCId: return (objc:=True, "objc.id_")[1]
|
||||
case clang.CXType_ObjCObject:
|
||||
if basetype(t).kind != clang.CXType_ObjCId: raise NotImplementedError(f"generics unsupported: {nm(t)}")
|
||||
if len(ps:=[proto(p) for p in protocols(t)]) == 0:
|
||||
types[nm(t)] = "objc.id_", True
|
||||
return "objc.id_"
|
||||
if len(ps) == 1:
|
||||
types[nm(t)] = ps[0], True
|
||||
return ps[0]
|
||||
types[nm(t)] = (tnm:=f"_anondynamic{anoncnt()}"), True
|
||||
lines.append(f"class {tnm}({', '.join(ps)}): pass # {nm(t)}")
|
||||
return tnm
|
||||
case _: raise NotImplementedError(f"unsupported type {t.kind}")
|
||||
|
||||
# parses an objc @interface or @protocol, returning a list of declerations that objc.Spec can parse, for the specified kind
|
||||
# NB: ivars are unsupported
|
||||
def parse_objc_spec(decl:clang.CXCursor, dnm:str, kind) -> list[str]:
|
||||
nonlocal lines, types
|
||||
ms = []
|
||||
for d in filter(lambda d: d.kind == kind, children(decl)):
|
||||
rollback = lines, types
|
||||
try: ms.append(f" ('{nm(d)}', {repr('instancetype') if nm(rt:=clang.clang_getCursorResultType(d))=='instancetype' else tname(rt)}, " +
|
||||
f"[{', '.join('instancetype' if nm(a) == 'instancetype' else tname(clang.clang_getCursorType(a)) for a in arguments(d))}]" +
|
||||
(", True" * (clang.CXCursor_NSReturnsRetained in attrs(d) or (any(nm(d).startswith(s) for s in arc_families) and rt.kind!=clang.CXType_Void)))
|
||||
+ "),")
|
||||
except NotImplementedError as e:
|
||||
print(f"skipping {dnm}.{nm(d)}: {e}")
|
||||
lines, types = rollback
|
||||
return ms
|
||||
|
||||
# libclang doesn't have a "type" for @protocol, so we have to do this here...
|
||||
def proto(decl):
|
||||
nonlocal lines, types
|
||||
if (dnm:=nm(decl)) in types and types[dnm][1]: return types[dnm][0]
|
||||
# check if this is a forward declaration
|
||||
is_defn = bool([f.kind for f in children(decl) if f.kind in (clang.CXCursor_ObjCInstanceMethodDecl, clang.CXCursor_ObjCClassMethodDecl)])
|
||||
if dnm not in types: lines.append(f"class {dnm}(objc.Spec): pass")
|
||||
types[dnm] = dnm, is_defn
|
||||
if is_defn:
|
||||
bases = [proto(b) for b in children(decl) if b.kind==clang.CXCursor_ObjCProtocolRef and nm(b) != nm(decl)]
|
||||
ims, cms = parse_objc_spec(decl, dnm, clang.CXCursor_ObjCInstanceMethodDecl), parse_objc_spec(decl, dnm, clang.CXCursor_ObjCClassMethodDecl)
|
||||
lines.extend([*([f"{dnm}._bases_ = [{', '.join(bases)}]"] if bases else []),
|
||||
*([f"{dnm}._methods_ = [", *ims, "]"] if ims else []), *([f"{dnm}._classmethods_ = [", *cms, "]"] if cms else [])])
|
||||
return dnm
|
||||
|
||||
for f in files:
|
||||
aa = ctypes.cast((ctypes.c_char_p * len(args))(*[x.encode() for x in args]), ctypes.POINTER(ctypes.POINTER(ctypes.c_char))) if len(args) else None
|
||||
tu = clang.clang_parseTranslationUnit(idx:=clang.clang_createIndex(False, 0), os.fspath(f).encode(), aa, len(args), None, 0,
|
||||
clang.CXTranslationUnit_DetailedPreprocessingRecord)
|
||||
q = list(children(unwrap_cursor(clang.clang_getTranslationUnitCursor(tu))))[::-1]
|
||||
while q:
|
||||
c = q.pop()
|
||||
if loc_file(loc(c)) != str(f) and (not recsym or c.kind not in (clang.CXCursor_FunctionDecl,)): continue
|
||||
if patterns and not any(re.match(p, nm(c)) for p in patterns): continue
|
||||
rollback = lines, types
|
||||
try:
|
||||
match c.kind:
|
||||
case clang.CXCursor_FunctionDecl if clang.clang_getCursorLinkage(c) == clang.CXLinkage_External and dll and nm(c) not in fns:
|
||||
# TODO: we could support name-mangling
|
||||
fns.add(nm(c))
|
||||
rt, ats = clang.clang_getCursorResultType(c), [clang.clang_getCursorType(arg) for arg in arguments(c)]
|
||||
anms = [normalize(arg) or '_' + str(i) for i, arg in enumerate(arguments(c))]
|
||||
lines.extend([f"@dll.bind({', '.join([tname(at) for at in [rt] + ats])})",
|
||||
f"def {nm(c)}({', '.join([f'{anm}:{typehint(at)}' for anm, at in zip(anms, ats)])}) -> {typehint(rt)}: ..."])
|
||||
if clang.CXCursor_NSReturnsRetained in attrs(c): lines.append(f"{nm(c)} = objc.returns_retained({nm(c)})")
|
||||
case (clang.CXCursor_StructDecl | clang.CXCursor_UnionDecl | clang.CXCursor_TypedefDecl | clang.CXCursor_EnumDecl
|
||||
| clang.CXCursor_ObjCInterfaceDecl): tname(clang.clang_getCursorType(c))
|
||||
case clang.CXCursor_MacroDefinition if macros and nm(c) and len(toks:=Tokens(c)) > 1:
|
||||
if nm(toks[1])=='(' and clang.clang_equalLocations(clang.clang_getRangeEnd(extent(toks[0])), clang.clang_getRangeStart(extent(toks[1]))):
|
||||
it = iter(toks[1:])
|
||||
_args = [nm(t) for t in itertools.takewhile(lambda t:nm(t)!=')', it) if clang.clang_getTokenKind(t) == clang.CXToken_Identifier]
|
||||
if len(body:=list(it)) == 0: continue
|
||||
extras += [f"{nm(c)} = lambda{' ' * bool(_args)}{','.join(_args)}: {readext(f,loc(body[0]),clang.clang_getRangeEnd(extent(toks[-1])))}"]
|
||||
else: extras += [f"{nm(c)} = {readext(f, loc(toks[1]), clang.clang_getRangeEnd(extent(toks[-1])))}"]
|
||||
case clang.CXCursor_VarDecl if clang.clang_getCursorLinkage(c) == clang.CXLinkage_Internal:
|
||||
ty = clang.clang_getCursorType(c)
|
||||
if (ty.kind == clang.CXType_ConstantArray and clang.clang_getCanonicalType(clang.clang_getArrayElementType(ty)).kind in ints and
|
||||
(init:=children(c)[-1]).kind == clang.CXCursor_InitListExpr):
|
||||
cs = children(init)
|
||||
if all(re.match(r"\[.*\].*=", readext(f, extent(ch))) for ch in cs):
|
||||
items = ','.join(f'{readext(f, extent(next(it:=iter(children(ch)))))}:{readext(f, extent(next(it)))}' for ch in cs)
|
||||
extras += [f"{nm(c)} = {{{items}}}"]
|
||||
else: extras += [f"{nm(c)} = ({','.join(readext(f, extent(ch)) for ch in cs)},)"]
|
||||
elif clang.clang_getCanonicalType(ty).kind in ints: extras += [f"{nm(c)} = {readext(f, extent(children(c)[-1]))}"]
|
||||
else: extras += [f"{nm(c)} = {tname(ty)}({readext(f, extent(children(c)[-1]))})"]
|
||||
case clang.CXCursor_VarDecl if clang.clang_getCursorLinkage(c) == clang.CXLinkage_External and dll:
|
||||
lines.append(f"try: {nm(c)} = {tname(clang.clang_getCursorType(c))}.in_dll(dll, '{nm(c)}') # type: ignore\n" +
|
||||
"except (ValueError,AttributeError): pass")
|
||||
case clang.CXCursor_ObjCProtocolDecl: proto(c)
|
||||
case clang.CXCursor_Namespace | clang.CXCursor_LinkageSpec: q.extend(list(children(c))[::-1])
|
||||
except NotImplementedError as e:
|
||||
print(f"skipping {nm(c)}: {e}")
|
||||
lines, types = rollback
|
||||
clang.clang_disposeTranslationUnit(tu)
|
||||
clang.clang_disposeIndex(idx)
|
||||
main = '\n'.join(['# mypy: disable-error-code="empty-body"', "from __future__ import annotations", "import ctypes",
|
||||
"from typing import Literal, TypeAlias", "from tinygrad.runtime.support.c import _IO, _IOW, _IOR, _IOWR",
|
||||
"from tinygrad.runtime.support import c", *prolog, *(["from tinygrad.runtime.support import objc"]*objc),
|
||||
*([f"dll = c.DLL('{name}', {dll}{f', {paths}'*bool(paths)}{', use_errno=True'*errno})"] if dll else []), *lines]) + '\n'
|
||||
extras = [f"{r} # type: ignore" if "lambda" in r else r
|
||||
for m in extras if (r:=functools.reduce(lambda s,r:re.sub(r[0], r[1], s), rules + base_rules, m))]
|
||||
while True:
|
||||
try:
|
||||
exec(main + '\n'.join(extras), {})
|
||||
break
|
||||
except (SyntaxError, NameError, TypeError) as e:
|
||||
extrano = unwrap(e.lineno if isinstance(e, SyntaxError) else unwrap(unwrap(e.__traceback__).tb_next).tb_lineno) - main.count('\n') - 1
|
||||
assert extrano >= 0 and extrano < len(extras), f"error outside extra range: {e}"
|
||||
print(f"skipping {extras[extrano]}: {e}")
|
||||
del extras[extrano]
|
||||
except Exception as e: raise Exception("parsing failed") from e
|
||||
return main + '\n'.join(extras + epilog)
|
||||
141
tinygrad_repo/tinygrad/runtime/support/c.py
Normal file
141
tinygrad_repo/tinygrad/runtime/support/c.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
import ctypes, functools, os, pathlib, re, sys, sysconfig
|
||||
from tinygrad.helpers import ceildiv, getenv, DEBUG, OSX, WIN
|
||||
from typing import TYPE_CHECKING, get_args, Generic, ParamSpec, TypeVar
|
||||
|
||||
def _do_ioctl(__idir, __base, __nr, __struct, __fd, *args, __payload=None, **kwargs):
|
||||
assert not WIN, "ioctl not supported"
|
||||
import tinygrad.runtime.support.hcq as hcq, fcntl
|
||||
ioctl = __fd.ioctl if isinstance(__fd, hcq.FileIOInterface) else functools.partial(fcntl.ioctl, __fd)
|
||||
if __struct is None: return ioctl((__base<<8)|__nr, __payload or (args[0] if args else 0))
|
||||
if (rc:=ioctl((__idir<<30)|(ctypes.sizeof(out:=(__payload or __struct(*args, **kwargs)))<<16)|(__base<<8)|__nr, out)):
|
||||
raise RuntimeError(f"ioctl returned {rc}")
|
||||
return out
|
||||
|
||||
def _IO(base, nr): return functools.partial(_do_ioctl, 0, ord(base) if isinstance(base, str) else base, nr, None)
|
||||
def _IOW(base, nr, typ): return functools.partial(_do_ioctl, 1, ord(base) if isinstance(base, str) else base, nr, typ)
|
||||
def _IOR(base, nr, typ): return functools.partial(_do_ioctl, 2, ord(base) if isinstance(base, str) else base, nr, typ)
|
||||
def _IOWR(base, nr, typ): return functools.partial(_do_ioctl, 3, ord(base) if isinstance(base, str) else base, nr, typ)
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
P = ParamSpec("P")
|
||||
|
||||
# mypy can't understand eg. ctypes.POINTER(ctypes.c_int), and python < 3.14 cannot understand ctypes.POINTER[ctypes.c_int]
|
||||
class POINTER(Generic[T], ctypes._Pointer):
|
||||
def __class_getitem__(cls, key): return ctypes.POINTER(key)
|
||||
def pointer(x: T) -> POINTER[T]: return ctypes.pointer(x) # type: ignore
|
||||
|
||||
if TYPE_CHECKING: _CFuncPtr = ctypes._CFunctionType
|
||||
else: _CFuncPtr = ctypes._CFuncPtr
|
||||
|
||||
class CFUNCTYPE(Generic[T, P], _CFuncPtr):
|
||||
_flags_ = 0
|
||||
def __class_getitem__(cls, key): return ctypes.CFUNCTYPE(key[0], *key[1])
|
||||
class Array(Generic[T, U], ctypes.Array):
|
||||
_type_, _length_ = ctypes.c_byte, 0
|
||||
def __class_getitem__(cls, key): return key[0] * get_args(key[1])[0]
|
||||
def __new__(cls, ty, l): return ty * l
|
||||
|
||||
class Struct(ctypes.Structure):
|
||||
SIZE = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
ctypes.Structure.__init__(self)
|
||||
for f,v in [*zip((rf[0] for rf in self._real_fields_), args), *kwargs.items()]: setattr(self, f, v)
|
||||
|
||||
@classmethod
|
||||
def register_fields(cls, fields):
|
||||
setattr(cls, "_real_fields_", fields)
|
||||
for i, (name, *args) in enumerate(fields): setattr(cls, name, Field(*args, name=name, idx=i))
|
||||
|
||||
def record(cls) -> type[Struct]:
|
||||
setattr(cls, "_fields_", [('_mem_', ctypes.c_byte * cls.SIZE)])
|
||||
return cls
|
||||
|
||||
class Field:
|
||||
def __init__(self, typ, off, bit_width=None, bit_off=0, *, name=None, idx=0):
|
||||
self.typ, self.off, self.bit_width, self.bit_off, self.name, self.idx = typ, off, bit_width, bit_off, name, idx
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
entry = (name, self.typ, self.off) + ((self.bit_width, self.bit_off) if self.bit_width else ())
|
||||
if hasattr(owner, "_real_fields_"): owner._real_fields_.append(entry)
|
||||
else: setattr(owner, "_real_fields_", [entry])
|
||||
self.name, self.idx = name, len(owner._real_fields_) - 1
|
||||
|
||||
# lazily resolve field descriptors
|
||||
def _resolve(self, cls):
|
||||
if self.bit_width: # handle bitfields ourselves
|
||||
sl, set_mask = slice(self.off, self.off+(sz:=ceildiv(self.bit_width+self.bit_off, 8))), ~((mask:=(1 << self.bit_width) - 1) << self.bit_off)
|
||||
def b2i(obj): return int.from_bytes(memoryview(obj).cast("B")[sl], sys.byteorder)
|
||||
def bset(obj, v): memoryview(obj).cast("B")[sl] = ((b2i(obj) & set_mask) | v << self.bit_off).to_bytes(sz, sys.byteorder)
|
||||
# FIXME: signedness
|
||||
cf = property(lambda obj: b2i(obj) >> self.bit_off & mask, bset)
|
||||
# pull the CField descriptor from a dummy class, zero length arrays are so ctypes manages references to child objects for us
|
||||
else: cf = type(self.name, (ctypes.Structure,), {"_layout_": "ms", "_pack_": 1, "_fields_": [(str(i), ctypes.c_byte*0) for i in range(self.idx)] +
|
||||
[("_", ctypes.c_byte * self.off), ("v", self.typ)]}).v # type: ignore
|
||||
setattr(cls, self.name, cf)
|
||||
return cf
|
||||
|
||||
def __get__(self, obj, objtype=None): return self._resolve(objtype).__get__(obj, objtype) if objtype else self
|
||||
def __set__(self, obj, value): self._resolve(obj.__class__).__set__(obj, value)
|
||||
|
||||
@functools.cache
|
||||
def init_c_struct_t(sz:int, fields: tuple[tuple, ...]):
|
||||
(CStruct:=type("CStruct", (Struct,), {'_fields_': [('_mem_', ctypes.c_byte * sz)]})).register_fields(fields) # type: ignore
|
||||
return CStruct
|
||||
def init_c_var(ty, creat_cb): return (creat_cb(v:=ty()), v)[1]
|
||||
|
||||
class DLL(ctypes.CDLL):
|
||||
_loaded_: set[str] = set()
|
||||
|
||||
@staticmethod
|
||||
def findlib(nm:str, paths:list[str], extra_paths=[]):
|
||||
if nm == 'libc' and OSX: return '/usr/lib/libc.dylib'
|
||||
if pathlib.Path(path:=getenv(nm.replace('-', '_').upper()+"_PATH", '')).is_file(): return path
|
||||
for p in paths:
|
||||
libpaths = {"posix": [d for d in os.environ.get('LD_LIBRARY_PATH', '').split(os.pathsep) if d] + ["/usr/lib64", "/usr/lib", "/usr/local/lib"],
|
||||
"nt": os.environ['PATH'].split(os.pathsep),
|
||||
"darwin": ["/opt/homebrew/lib", f"/System/Library/Frameworks/{p}.framework", f"/System/Library/PrivateFrameworks/{p}.framework"],
|
||||
'linux': ['/lib', '/lib64', f"/lib/{sysconfig.get_config_var('MULTIARCH')}", "/usr/lib/wsl/lib/"]}
|
||||
if (pth:=pathlib.Path(p)).is_absolute():
|
||||
if pth.is_file(): return p
|
||||
else: continue
|
||||
for pre in (pathlib.Path(pre) for pre in ([path] if path else []) + libpaths.get(os.name, []) + libpaths.get(sys.platform, []) + extra_paths):
|
||||
if not pre.is_dir(): continue
|
||||
if WIN or OSX:
|
||||
for base in ([f"lib{p}.dylib", f"{p}.dylib", str(p)] if OSX else [f"{p}.dll"]):
|
||||
if (l:=pre / base).is_file() or (OSX and 'framework' in str(l) and l.is_symlink()): return str(l)
|
||||
else:
|
||||
for l in (l for l in pre.iterdir() if l.is_file() and re.fullmatch(f"lib{p}\\.so\\.?[0-9]*", l.name)):
|
||||
# filter out linker scripts
|
||||
with open(l, 'rb') as f:
|
||||
if f.read(4) == b'\x7FELF': return str(l)
|
||||
|
||||
def __init__(self, nm:str, paths:str|list[str], extra_paths=[], emsg="", **kwargs):
|
||||
self.nm, self.emsg = nm, emsg or f"try setting {nm.upper()+'_PATH'}?"
|
||||
if (path:= DLL.findlib(nm, paths if isinstance(paths, list) else [paths], extra_paths if isinstance(extra_paths, list) else [extra_paths])):
|
||||
if DEBUG >= 3: print(f"loading {nm} from {path}")
|
||||
try:
|
||||
super().__init__(path, **kwargs)
|
||||
self._loaded_.add(self.nm)
|
||||
except OSError as e:
|
||||
self.emsg = str(e)
|
||||
if DEBUG >= 3: print(f"loading {nm} failed: {e}")
|
||||
elif DEBUG >= 3: print(f"loading {nm} failed: not found on system")
|
||||
|
||||
def bind(self, restype, *argtypes):
|
||||
def wrap(fn):
|
||||
cfunc = None
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args):
|
||||
nonlocal cfunc
|
||||
if cfunc is None: (cfunc:=getattr(self, fn.__name__)).argtypes, cfunc.restype = argtypes, restype
|
||||
return cfunc(*args)
|
||||
wrapper.restype, wrapper.argtypes = restype, argtypes # type: ignore
|
||||
return wrapper
|
||||
return wrap
|
||||
|
||||
def __getattr__(self, nm):
|
||||
if self.nm not in self._loaded_: raise AttributeError(f"failed to load library {self.nm}: {self.emsg}")
|
||||
return super().__getattr__(nm)
|
||||
133
tinygrad_repo/tinygrad/runtime/support/compiler_amd.py
Normal file
133
tinygrad_repo/tinygrad/runtime/support/compiler_amd.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import ctypes, hashlib, tempfile, subprocess, pathlib, shutil
|
||||
from tinygrad.helpers import system, getenv
|
||||
from tinygrad.runtime.autogen import comgr
|
||||
try:
|
||||
comgr.amd_comgr_get_version(ctypes.byref(major:=ctypes.c_uint64()), ctypes.byref(minor:=ctypes.c_uint64()))
|
||||
if major.value >= 3:
|
||||
# in comgr 3 the values of enums in headers were changed: https://github.com/ROCm/llvm-project/issues/272
|
||||
import tinygrad.runtime.autogen.comgr_3 as comgr # type: ignore[no-redef]
|
||||
assert comgr.AMD_COMGR_LANGUAGE_HIP == 3
|
||||
except AttributeError: pass # ignore if ROCm isn't installed
|
||||
from tinygrad.device import Compiler, CompileError
|
||||
from tinygrad.runtime.support.compiler_cpu import LLVMCompiler
|
||||
from tinygrad.runtime.support import c
|
||||
from tinygrad.helpers import OSX, to_char_p_p
|
||||
|
||||
def _find_llvm_objdump():
|
||||
if OSX: return '/opt/homebrew/opt/llvm/bin/llvm-objdump'
|
||||
# Try ROCm path first, then versioned, then unversioned
|
||||
for p in ['/opt/rocm/llvm/bin/llvm-objdump', 'llvm-objdump-21', 'llvm-objdump-20', 'llvm-objdump']:
|
||||
if shutil.which(p): return p
|
||||
raise FileNotFoundError("llvm-objdump not found")
|
||||
|
||||
def amdgpu_disassemble(lib:bytes):
|
||||
asm = system(f"{_find_llvm_objdump()} -d -", input=lib).splitlines()
|
||||
while asm and ("s_nop 0" in asm[-1] or "s_code_end" in asm[-1]): asm.pop()
|
||||
print("\n".join(asm))
|
||||
|
||||
def check(status):
|
||||
if status != 0:
|
||||
comgr.amd_comgr_status_string(status, ctypes.byref(status_str := ctypes.POINTER(ctypes.c_char)()))
|
||||
raise RuntimeError(f"comgr fail {status}, {ctypes.string_at(status_str).decode()}")
|
||||
|
||||
def _get_comgr_data(data_set, data_type):
|
||||
check(comgr.amd_comgr_action_data_get_data(data_set, data_type, 0, ctypes.byref(data_exec := comgr.amd_comgr_data_t())))
|
||||
check(comgr.amd_comgr_get_data(data_exec, ctypes.byref(sz := ctypes.c_uint64()), None))
|
||||
check(comgr.amd_comgr_get_data(data_exec, ctypes.byref(sz), (dat := ctypes.create_string_buffer(sz.value))))
|
||||
check(comgr.amd_comgr_release_data(data_exec))
|
||||
return bytes(dat)
|
||||
|
||||
# amd_comgr_action_info_set_options was deprecated
|
||||
def set_options(action_info, options:bytes):
|
||||
# TODO: this type should be correct in the autogen stub
|
||||
@comgr.dll.bind(comgr.amd_comgr_status_t, comgr.amd_comgr_action_info_t, c.POINTER[c.POINTER[ctypes.c_char]], comgr.size_t)
|
||||
def amd_comgr_action_info_set_option_list(ai, o, c) -> comgr.amd_comgr_status_t: pass # type: ignore[empty-body]
|
||||
return amd_comgr_action_info_set_option_list(action_info, to_char_p_p(options_list:=options.split(b' ')), len(options_list))
|
||||
|
||||
# AMD_COMGR_SAVE_TEMPS=1 AMD_COMGR_REDIRECT_LOGS=stdout AMD_COMGR_EMIT_VERBOSE_LOGS=1
|
||||
def compile_hip(prg:str, arch="gfx1100", asm=False) -> bytes:
|
||||
check(comgr.amd_comgr_create_action_info(ctypes.byref(action_info := comgr.amd_comgr_action_info_t())))
|
||||
check(comgr.amd_comgr_action_info_set_language(action_info, comgr.AMD_COMGR_LANGUAGE_HIP))
|
||||
check(comgr.amd_comgr_action_info_set_isa_name(action_info, b"amdgcn-amd-amdhsa--" + arch.encode()))
|
||||
check(comgr.amd_comgr_action_info_set_logging(action_info, True))
|
||||
|
||||
check(comgr.amd_comgr_create_data_set(ctypes.byref(data_set_src := comgr.amd_comgr_data_set_t())))
|
||||
check(comgr.amd_comgr_create_data_set(ctypes.byref(data_set_bc := comgr.amd_comgr_data_set_t())))
|
||||
check(comgr.amd_comgr_create_data_set(ctypes.byref(data_set_reloc := comgr.amd_comgr_data_set_t())))
|
||||
check(comgr.amd_comgr_create_data_set(ctypes.byref(data_set_exec := comgr.amd_comgr_data_set_t())))
|
||||
|
||||
check(comgr.amd_comgr_create_data(comgr.AMD_COMGR_DATA_KIND_SOURCE, ctypes.byref(data_src := comgr.amd_comgr_data_t())))
|
||||
check(comgr.amd_comgr_set_data(data_src, len(rprg := prg.encode()), rprg))
|
||||
|
||||
if asm:
|
||||
check(comgr.amd_comgr_set_data_name(data_src, b"<null>.s"))
|
||||
check(comgr.amd_comgr_data_set_add(data_set_src, data_src))
|
||||
status = comgr.amd_comgr_do_action(comgr.AMD_COMGR_ACTION_ASSEMBLE_SOURCE_TO_RELOCATABLE, action_info, data_set_src, data_set_reloc)
|
||||
if status != 0:
|
||||
print(_get_comgr_data(data_set_reloc, comgr.AMD_COMGR_DATA_KIND_LOG).decode())
|
||||
raise RuntimeError("assemble failed")
|
||||
else:
|
||||
check(comgr.amd_comgr_set_data_name(data_src, b"<null>"))
|
||||
check(comgr.amd_comgr_data_set_add(data_set_src, data_src))
|
||||
# -include hiprtc_runtime.h was removed
|
||||
options = [
|
||||
"-O3", "-mcumode", "--hip-version=6.0.32830", "-DHIP_VERSION_MAJOR=6", "-DHIP_VERSION_MINOR=0", "-DHIP_VERSION_PATCH=32830",
|
||||
"-D__HIPCC_RTC__", "-std=c++14", "-nogpuinc", "-Wno-gnu-line-marker", "-Wno-missing-prototypes", f"--offload-arch={arch}",
|
||||
"-I/opt/rocm/include", "-Xclang -disable-llvm-passes", "-Xclang -aux-triple", "-Xclang x86_64-unknown-linux-gnu"]
|
||||
check(set_options(action_info, ' '.join(options).encode()))
|
||||
status = comgr.amd_comgr_do_action(comgr.AMD_COMGR_ACTION_COMPILE_SOURCE_WITH_DEVICE_LIBS_TO_BC, action_info, data_set_src, data_set_bc)
|
||||
if status != 0:
|
||||
print(_get_comgr_data(data_set_bc, comgr.AMD_COMGR_DATA_KIND_LOG).decode())
|
||||
raise RuntimeError("compile failed")
|
||||
check(set_options(action_info, b"-O3 -mllvm -amdgpu-internalize-symbols"))
|
||||
check(comgr.amd_comgr_do_action(comgr.AMD_COMGR_ACTION_CODEGEN_BC_TO_RELOCATABLE, action_info, data_set_bc, data_set_reloc))
|
||||
|
||||
check(set_options(action_info, b""))
|
||||
check(comgr.amd_comgr_do_action(comgr.AMD_COMGR_ACTION_LINK_RELOCATABLE_TO_EXECUTABLE, action_info, data_set_reloc, data_set_exec))
|
||||
ret = _get_comgr_data(data_set_exec, comgr.AMD_COMGR_DATA_KIND_EXECUTABLE)
|
||||
check(comgr.amd_comgr_release_data(data_src))
|
||||
for x in [data_set_src, data_set_bc, data_set_reloc, data_set_exec]: check(comgr.amd_comgr_destroy_data_set(x))
|
||||
check(comgr.amd_comgr_destroy_action_info(action_info))
|
||||
return ret
|
||||
|
||||
class HIPCompiler(Compiler):
|
||||
def __init__(self, arch:str):
|
||||
assert comgr.dll.nm in c.DLL._loaded_, f"comgr not available: {comgr.dll.emsg}"
|
||||
self.arch = arch
|
||||
super().__init__(f"compile_hip_{self.arch}")
|
||||
def compile(self, src:str) -> bytes:
|
||||
try: return compile_hip(src, self.arch, src.split('\n', 1)[0].strip() == '.text')
|
||||
except RuntimeError as e: raise CompileError(e) from e
|
||||
def disassemble(self, lib:bytes): amdgpu_disassemble(lib)
|
||||
|
||||
class HIPCCCompiler(Compiler):
|
||||
def __init__(self, arch:str, extra_options:list[str]=[]):
|
||||
self.arch, self.extra_options = arch, extra_options
|
||||
super().__init__(f"compile_hipcc_{self.arch}_{hashlib.sha256(' '.join(extra_options).encode()).hexdigest()[:8]}")
|
||||
def compile(self, src:str) -> bytes:
|
||||
with tempfile.NamedTemporaryFile(suffix=".cpp") as srcf, tempfile.NamedTemporaryFile(suffix=".bc") as bcf:
|
||||
with tempfile.NamedTemporaryFile(suffix=".hsaco") as libf:
|
||||
srcf.write(src.encode())
|
||||
srcf.flush()
|
||||
|
||||
rocm_path = getenv("ROCM_PATH", "/opt/rocm")
|
||||
subprocess.run(["hipcc", "-c", "-emit-llvm", "--cuda-device-only", "-O3", "-mcumode",
|
||||
f"--offload-arch={self.arch}", f"-I{rocm_path}/include/hip", "-o", bcf.name, srcf.name] + self.extra_options, check=True)
|
||||
subprocess.run(["hipcc", "-target", "amdgcn-amd-amdhsa", f"-mcpu={self.arch}",
|
||||
"-O3", "-mllvm", "-amdgpu-internalize-symbols", "-c", "-o", libf.name, bcf.name] + self.extra_options, check=True)
|
||||
|
||||
return pathlib.Path(libf.name).read_bytes()
|
||||
def disassemble(self, lib:bytes): amdgpu_disassemble(lib)
|
||||
|
||||
class AMDLLVMCompiler(LLVMCompiler):
|
||||
jit = False
|
||||
def __init__(self, arch: str):
|
||||
self.arch = arch
|
||||
super().__init__("AMDGPU", self.arch, "+cumode")
|
||||
def __reduce__(self): return (AMDLLVMCompiler, (self.arch,))
|
||||
def compile(self, src:str) -> bytes:
|
||||
try: return super().compile(src)
|
||||
except RuntimeError as e:
|
||||
if "undefined value '@llvm.amdgcn." in str(e): raise CompileError(str(e) + "AMD with LLVM backend requires LLVM >= 18") from e
|
||||
raise CompileError(e) from e
|
||||
def disassemble(self, lib:bytes): amdgpu_disassemble(lib)
|
||||
114
tinygrad_repo/tinygrad/runtime/support/compiler_cpu.py
Normal file
114
tinygrad_repo/tinygrad/runtime/support/compiler_cpu.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import ctypes, subprocess, signal
|
||||
from tinygrad.device import Compiler
|
||||
from tinygrad.helpers import getenv, capstone_flatdump, DEBUG, unwrap
|
||||
from tinygrad.runtime.support.elf import jit_loader
|
||||
from tinygrad.runtime.autogen import llvm
|
||||
|
||||
def _block_sigusr2():
|
||||
try: signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGUSR2})
|
||||
except (ValueError, OSError): pass
|
||||
|
||||
class ClangCompiler(Compiler):
|
||||
def __init__(self, arch:list[str], cachekey="compile_clang_jit"):
|
||||
assert len(arch) >= 2, f"invalid arch string: {','.join(arch)!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
|
||||
self.arch, cpu, *feats = arch
|
||||
match self.arch:
|
||||
case "x86_64": self.args = [f"-march={cpu}"] + [f"-mno{f}" if f.startswith("-") else f"-m{f}" for f in feats]
|
||||
# on arm march means "runs on this arch and superset" instead of "optimize for this arch". x86 march == arm mcpu
|
||||
# x18 is a reserved platform register. It is clobbered on context switch in macos and is used to store TEB pointer in windows on arm
|
||||
case "arm64": self.args = ["-ffixed-x18", "-mcpu=" + "+".join([cpu] + ["no"+f[1:] if f.startswith("-") else f for f in feats])]
|
||||
case "riscv64": self.args = ["-march=" + "_".join(["rv64g" if cpu == "native" else cpu] + feats)]
|
||||
case _: raise RuntimeError(f"unsupported arch: {self.arch!r}")
|
||||
super().__init__(f"{cachekey}_{'_'.join(arch)}")
|
||||
|
||||
def compile_to_obj(self, src:str) -> bytes:
|
||||
"""Compile C source to ELF object file (before linking)."""
|
||||
# -fno-math-errno is required for __builtin_sqrt to become an instruction instead of a function call
|
||||
return subprocess.check_output([getenv("CC", 'clang'), '-c', '-x', 'c', '-O2', '-fPIC', '-ffreestanding', '-fno-math-errno', '-nostdlib',
|
||||
'-fno-ident', f'--target={self.arch}-none-unknown-elf', *self.args, '-', '-o', '-'], input=src.encode('utf-8'),
|
||||
preexec_fn=_block_sigusr2)
|
||||
|
||||
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src))
|
||||
|
||||
def disassemble(self, lib:bytes): return capstone_flatdump(lib, self.arch)
|
||||
|
||||
def cerr(): return ctypes.pointer(ctypes.pointer(ctypes.c_char()))
|
||||
|
||||
def expect(x, err, ret=None):
|
||||
if x: raise RuntimeError(unwrap(ctypes.cast(err.contents, ctypes.c_char_p).value).decode() if not isinstance(err, str) else err)
|
||||
return ret
|
||||
|
||||
class LLVMCompiler(Compiler):
|
||||
jit = True
|
||||
def __init__(self, arch:str, processor:str, feats:str, cache_key=None):
|
||||
for component in ['Target', 'TargetInfo', 'TargetMC', 'AsmParser', 'AsmPrinter']:
|
||||
getattr(llvm, "LLVMInitialize" + {'arm64': 'AArch64', 'x86_64': 'X86', 'riscv64': 'riscv64'}.get(arch, "AMDGPU") + component)()
|
||||
|
||||
triple = {'arm64': b'aarch64-none-unknown-elf', 'x86_64': b'x86_64-none-unknown-elf', 'AMDGPU': b'amdgcn-amd-amdhsa'}[arch]
|
||||
target = expect(llvm.LLVMGetTargetFromTriple(triple, ctypes.pointer(tgt:=llvm.LLVMTargetRef()), err:=cerr()), err, tgt)
|
||||
if DEBUG >= 3: print(f"LLVM init for {processor!r} with {feats!r}")
|
||||
self.target_machine = llvm.LLVMCreateTargetMachine(target, triple, processor.encode(), feats.encode(),
|
||||
llvm.LLVMCodeGenLevelDefault, llvm.LLVMRelocPIC, llvm.LLVMCodeModelDefault)
|
||||
|
||||
self.pbo = llvm.LLVMCreatePassBuilderOptions()
|
||||
if (opt:=bool(getenv("LLVMOPT", "1"))):
|
||||
self.passes = b'default<O2>'
|
||||
llvm.LLVMPassBuilderOptionsSetLoopUnrolling(self.pbo, True)
|
||||
llvm.LLVMPassBuilderOptionsSetLoopVectorization(self.pbo, True)
|
||||
llvm.LLVMPassBuilderOptionsSetSLPVectorization(self.pbo, True)
|
||||
llvm.LLVMPassBuilderOptionsSetVerifyEach(self.pbo, True)
|
||||
else:
|
||||
self.passes = b'default<O0>'
|
||||
|
||||
# Create a per-instance context instead of using the global context to avoid shared state between parallel test processes
|
||||
self.context = llvm.LLVMContextCreate()
|
||||
self.diag_msgs: list[str] = []
|
||||
@llvm.LLVMDiagnosticHandler
|
||||
def handle_diag(diag_ref, _arg):
|
||||
severity = llvm.LLVMGetDiagInfoSeverity(diag_ref)
|
||||
msg = ctypes.string_at(llvm.LLVMGetDiagInfoDescription(diag_ref)).decode()
|
||||
if severity == llvm.LLVMDSError:
|
||||
self.diag_msgs.append(msg)
|
||||
self.handle_diag = handle_diag
|
||||
llvm.LLVMContextSetDiagnosticHandler(self.context, handle_diag, None)
|
||||
super().__init__(cache_key or f"compile_llvm_{processor}_{feats}{'_jit' if self.jit else ''}{'_opt' if opt else ''}")
|
||||
|
||||
def __del__(self):
|
||||
llvm.LLVMDisposePassBuilderOptions(self.pbo)
|
||||
llvm.LLVMContextDispose(self.context)
|
||||
|
||||
def compile_to_obj(self, src:str) -> bytes:
|
||||
self.diag_msgs.clear()
|
||||
src_buf = llvm.LLVMCreateMemoryBufferWithMemoryRangeCopy(ctypes.create_string_buffer(src_bytes:=src.encode()), len(src_bytes), b'src')
|
||||
mod = expect(llvm.LLVMParseIRInContext(self.context, src_buf, ctypes.pointer(m:=llvm.LLVMModuleRef()), err:=cerr()), err, m)
|
||||
expect(llvm.LLVMVerifyModule(mod, llvm.LLVMReturnStatusAction, err:=cerr()), err)
|
||||
expect(llvm.LLVMRunPasses(mod, self.passes, self.target_machine, self.pbo), 'failed to run passes')
|
||||
if DEBUG >= 7: print(ctypes.string_at(llvm.LLVMPrintModuleToString(mod)).decode())
|
||||
obj_buf = expect(llvm.LLVMTargetMachineEmitToMemoryBuffer(self.target_machine, mod, llvm.LLVMObjectFile, err:=cerr(),
|
||||
buf:=llvm.LLVMMemoryBufferRef()), err, buf)
|
||||
llvm.LLVMDisposeModule(mod)
|
||||
obj = ctypes.string_at(llvm.LLVMGetBufferStart(obj_buf), llvm.LLVMGetBufferSize(obj_buf))
|
||||
llvm.LLVMDisposeMemoryBuffer(obj_buf)
|
||||
if self.diag_msgs: raise RuntimeError("llvm diagnostic: " + "\n".join(self.diag_msgs))
|
||||
return obj
|
||||
|
||||
def compile(self, src:str) -> bytes: return jit_loader(self.compile_to_obj(src)) if self.jit else self.compile_to_obj(src)
|
||||
|
||||
|
||||
class CPULLVMCompiler(LLVMCompiler):
|
||||
def __init__(self, arch:list[str], cache_key=None):
|
||||
assert len(arch) >= 2, f"invalid arch string: {','.join(arch)!r}, expected '<arch>,<cpu>,[<feats>]' (eg. 'x86_64,znver2')"
|
||||
self.arch, cpu, *feats = arch
|
||||
featstr = ','.join(f if f.startswith('-') else '+'+f for f in feats)
|
||||
if cpu == "native":
|
||||
cpu = ctypes.string_at(llvm.LLVMGetHostCPUName()).decode()
|
||||
featstr = (featstr + "," if featstr else "") + ctypes.string_at(llvm.LLVMGetHostCPUFeatures()).decode()
|
||||
# +reserve-x18 here does the same thing as -ffixed-x18 in ClangCompiler, see comments there for why it's needed on arm osx
|
||||
super().__init__(self.arch, cpu, ('+reserve-x18,' if self.arch == "arm64" else '') + featstr, cache_key)
|
||||
|
||||
def disassemble(self, lib:bytes): capstone_flatdump(lib, self.arch)
|
||||
|
||||
class X86Compiler(Compiler):
|
||||
def __init__(self): super().__init__(None)
|
||||
def compile(self, src:str) -> bytes: return bytes.fromhex(src)
|
||||
def disassemble(self, lib:bytes): return capstone_flatdump(lib, "x86_64")
|
||||
92
tinygrad_repo/tinygrad/runtime/support/compiler_cuda.py
Normal file
92
tinygrad_repo/tinygrad/runtime/support/compiler_cuda.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import hashlib, tempfile, ctypes, re, pathlib
|
||||
from tinygrad.helpers import to_char_p_p, colored, getenv, system
|
||||
from tinygrad.runtime.support.c import init_c_var
|
||||
from tinygrad.runtime.autogen import nvrtc, nvjitlink as jitlink
|
||||
from tinygrad.device import Compiler, CompileError
|
||||
|
||||
CUDA_PATH = getenv("CUDA_PATH", "")
|
||||
|
||||
def _get_bytes(arg, get_str, get_sz, check) -> bytes:
|
||||
x = ctypes.create_string_buffer(init_c_var(ctypes.c_size_t, lambda x: check(get_sz(arg, ctypes.byref(x)))).value)
|
||||
check(get_str(arg, x))
|
||||
return ctypes.string_at(x, size=len(x))
|
||||
|
||||
def nvrtc_check(status, ctx=None):
|
||||
if status != 0:
|
||||
err_log = _get_bytes(ctx, nvrtc.nvrtcGetProgramLog, nvrtc.nvrtcGetProgramLogSize, lambda _: None).decode() if ctx else ""
|
||||
raise CompileError(f"Nvrtc Error {status}, {ctypes.string_at(nvrtc.nvrtcGetErrorString(status)).decode()}\n{err_log}")
|
||||
|
||||
def jitlink_check(status, ctx=None):
|
||||
if status != 0:
|
||||
err_log = _get_bytes(ctx, jitlink.nvJitLinkGetErrorLog, jitlink.nvJitLinkGetErrorLogSize, lambda _: None).decode() if ctx else ""
|
||||
raise CompileError(f"jitlink Error {status}, {jitlink.nvJitLinkResult.get(status)}\n{err_log}")
|
||||
|
||||
def pretty_ptx(s):
|
||||
# all expressions match `<valid_before><expr><valid_after>` and replace it with `<valid_before>color(<expr>)<valid_after>`
|
||||
s = re.sub(r'([!@<\[\s,\+\-;\n])((?:[_%$][\w%\$_]+(?:\.[xyz])?\:?)|(?:buf\d+))([<>\]\s,\+\-;\n\)])',
|
||||
lambda m:m[1]+colored(m[2], "blue")+m[3], s, flags=re.M) # identifiers
|
||||
s = re.sub(r'(.)((?:b|s|u|f)(?:8|16|32|64)|pred)([\.\s])', lambda m:m[1]+colored(m[2], "green")+m[3], s, flags=re.M) # types
|
||||
s = re.sub(r'^(\s*)([\w]+)(.*?;$)', lambda m:m[1]+colored(m[2], "yellow")+m[3], s, flags=re.M) # instructions
|
||||
s = re.sub(r'([<>\[\]\s,\+\-;])((?:0[fF][0-9a-fA-F]{8})|(?:[0-9]+)|(?:0[xX][0-9a-fA-F]+))([<>\[\]\s,\+\-;])',
|
||||
lambda m:m[1]+colored(m[2], "yellow")+m[3], s, flags=re.M) # numbers
|
||||
s = re.sub(r'(\.)(param|reg|global)', lambda m:m[1]+colored(m[2], "magenta"), s, flags=re.M) # space
|
||||
s = re.sub(r'(\.)(version|target|address_size|visible|entry)', lambda m:m[1]+colored(m[2], "magenta"), s, flags=re.M) # derivatives
|
||||
return s
|
||||
|
||||
def cuda_disassemble(lib:bytes, arch:str, ptx=False):
|
||||
try:
|
||||
fn = (pathlib.Path(tempfile.gettempdir()) / f"tinycuda_{hashlib.md5(lib).hexdigest()}").as_posix()
|
||||
with open(fn, "wb") as f: f.write(lib.rstrip(b'\x00') if ptx else lib)
|
||||
if ptx: system(f"ptxas -arch={arch} -o {fn} {fn}")
|
||||
print(system(f'nvdisasm {fn}'))
|
||||
except Exception as e: print("Failed to generate SASS", str(e), "Make sure your PATH contains ptxas/nvdisasm binary of compatible version.")
|
||||
|
||||
class NVRTCCompiler(Compiler):
|
||||
def __init__(self, arch:str, ptx=True, cache_key:str="cuda"):
|
||||
self.ptx, self.arch, self.compile_options = ptx, arch, [f'--gpu-architecture={arch}']
|
||||
self.compile_options += [f"-I{CUDA_PATH}/include"] if CUDA_PATH else ["-I/usr/local/cuda/include", "-I/usr/include", "-I/opt/cuda/include"]
|
||||
nvrtc_check(nvrtc.nvrtcVersion((nvrtcMajor := ctypes.c_int()), (nvrtcMinor := ctypes.c_int())))
|
||||
if (nvrtcMajor.value, nvrtcMinor.value) >= (12, 4): self.compile_options.append("--minimal")
|
||||
super().__init__(f"compile_{cache_key}_{self.arch}")
|
||||
def compile(self, src:str) -> bytes:
|
||||
nvrtc_check(nvrtc.nvrtcCreateProgram(ctypes.byref(prog := nvrtc.nvrtcProgram()), src.encode(), "<null>".encode(), 0, None, None))
|
||||
nvrtc_check(nvrtc.nvrtcCompileProgram(prog, len(self.compile_options), to_char_p_p([o.encode() for o in self.compile_options])), prog)
|
||||
data = _get_bytes(prog, nvrtc.nvrtcGetPTX if self.ptx else nvrtc.nvrtcGetCUBIN,
|
||||
nvrtc.nvrtcGetPTXSize if self.ptx else nvrtc.nvrtcGetCUBINSize, nvrtc_check)
|
||||
nvrtc_check(nvrtc.nvrtcDestroyProgram(ctypes.byref(prog)))
|
||||
return data
|
||||
def disassemble(self, lib:bytes): cuda_disassemble(lib, self.arch, ptx=self.ptx)
|
||||
|
||||
class NVCCCompiler(Compiler):
|
||||
def __init__(self, arch:str, ptx:bool=True, cache_key:str="cuda", extra_options:list[str]=[]):
|
||||
self.ptx, self.arch, self.extra_options = ptx, arch, extra_options
|
||||
super().__init__(f"compile_nvcc_{cache_key+'ptx' if ptx else ''}_{self.arch}_{hashlib.sha256(' '.join(extra_options).encode()).hexdigest()[:8]}")
|
||||
def compile(self, src:str) -> bytes:
|
||||
mode, suffix = ("-ptx", ".ptx") if self.ptx else ("-cubin", ".cubin")
|
||||
with tempfile.NamedTemporaryFile(suffix=".cu") as srcf, tempfile.NamedTemporaryFile(suffix=suffix) as libf:
|
||||
srcf.write(src.encode())
|
||||
srcf.flush()
|
||||
system(f"nvcc -arch={self.arch} {mode} -o {libf.name} {srcf.name} " + ' '.join(self.extra_options))
|
||||
return libf.read()
|
||||
def disassemble(self, lib:bytes): cuda_disassemble(lib, self.arch, ptx=self.ptx)
|
||||
|
||||
class PTXCompiler(Compiler):
|
||||
def __init__(self, arch:str, cache_key="ptx"):
|
||||
self.arch = arch
|
||||
super().__init__(f"compile_{cache_key}_{self.arch}")
|
||||
def compile(self, src:str) -> bytes:
|
||||
return src.replace("TARGET", self.arch).replace("VERSION", "8.7" if (ver:=int(self.arch[3:]))>=120 else ("7.8" if ver>=89 else "7.5")).encode()
|
||||
def disassemble(self, lib:bytes): cuda_disassemble(lib, self.arch, ptx=True)
|
||||
|
||||
class NVPTXCompiler(PTXCompiler):
|
||||
def __init__(self, arch:str):
|
||||
nvrtc_check(jitlink.nvJitLinkVersion(ctypes.byref(ctypes.c_uint()), ctypes.byref(ctypes.c_uint())))
|
||||
super().__init__(arch, cache_key="nv_ptx")
|
||||
def compile(self, src:str) -> bytes:
|
||||
jitlink_check(jitlink.nvJitLinkCreate(handle := jitlink.nvJitLinkHandle(), 1, to_char_p_p([f'-arch={self.arch}'.encode()])), handle)
|
||||
jitlink_check(jitlink.nvJitLinkAddData(handle, jitlink.NVJITLINK_INPUT_PTX, ptxsrc:=super().compile(src), len(ptxsrc), "<null>".encode()), handle)
|
||||
jitlink_check(jitlink.nvJitLinkComplete(handle), handle)
|
||||
data = _get_bytes(handle, jitlink.nvJitLinkGetLinkedCubin, jitlink.nvJitLinkGetLinkedCubinSize, jitlink_check)
|
||||
jitlink_check(jitlink.nvJitLinkDestroy(handle))
|
||||
return data
|
||||
def disassemble(self, lib:bytes): cuda_disassemble(lib, self.arch)
|
||||
131
tinygrad_repo/tinygrad/runtime/support/compiler_mesa.py
Normal file
131
tinygrad_repo/tinygrad/runtime/support/compiler_mesa.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import base64, ctypes, pathlib, tempfile, hashlib
|
||||
from tinygrad.device import Compiler
|
||||
from tinygrad.helpers import cpu_objdump, system, data64
|
||||
from tinygrad.runtime.autogen import mesa, llvm
|
||||
from tinygrad.runtime.support.compiler_cpu import CPULLVMCompiler, expect, cerr
|
||||
|
||||
# NB: compilers assume mesa's glsl type cache is managed externally with mesa.glsl_type_singleton_init_or_ref() and mesa.glsl_type_singleton_decref()
|
||||
|
||||
def rzalloc(typ, ctx=None, **kwargs):
|
||||
s = ctypes.cast(mesa.rzalloc_size(ctypes.cast(ctx, ctypes.c_void_p), ctypes.sizeof(typ)), ctypes.POINTER(typ))
|
||||
for k,v in kwargs.items(): setattr(s.contents, k, v)
|
||||
return s
|
||||
|
||||
def deserialize(enc_src, opts):
|
||||
blobreader = mesa.struct_blob_reader()
|
||||
mesa.blob_reader_init(blobreader, src:=base64.b64decode(enc_src), len(src))
|
||||
return mesa.nir_deserialize(None, ctypes.cast(opts, ctypes.POINTER(mesa.nir_shader_compiler_options)), blobreader)
|
||||
|
||||
class LVPCompiler(CPULLVMCompiler):
|
||||
def __init__(self, arch): CPULLVMCompiler.__init__(self, arch.split(","), cache_key="compile_lvp")
|
||||
|
||||
def compile(self, src) -> bytes:
|
||||
shader, ctx = deserialize(src, mesa.lvp_nir_options), llvm.LLVMGetGlobalContext()
|
||||
gallivm = mesa.gallivm_create(None, mesa.lp_context_ref(ctypes.cast(ctx, ctypes.POINTER(mesa.struct_LLVMOpaqueContext)), True), None).contents
|
||||
module, builder = ctypes.cast(gallivm.module, llvm.LLVMModuleRef), ctypes.cast(gallivm.builder, llvm.LLVMBuilderRef)
|
||||
|
||||
params = mesa.struct_lp_build_tgsi_params(mesa.struct_lp_type(floating=True, sign=True, width=32, length=4),
|
||||
resources_type=mesa.lp_build_jit_resources_type(gallivm), mask=ctypes.pointer(mesa.struct_lp_build_mask_context()))
|
||||
|
||||
pt = llvm.LLVMPointerType(ctypes.cast(params.resources_type, llvm.LLVMTypeRef), 0)
|
||||
fn = llvm.LLVMAddFunction(module, shader.contents.info.name, llvm.LLVMFunctionType(llvm.LLVMVoidTypeInContext(ctx), pt, 1, 0))
|
||||
llvm.LLVMPositionBuilderAtEnd(builder, llvm.LLVMAppendBasicBlockInContext(ctx, fn, b"entry"))
|
||||
|
||||
params.consts_ptr = mesa.lp_build_struct_get_ptr2(gallivm, params.resources_type,
|
||||
ctypes.cast(llvm.LLVMGetParam(fn, 0), mesa.LLVMValueRef), mesa.LP_JIT_RES_CONSTANTS, b"constants")
|
||||
mesa.lp_build_mask_begin(params.mask, gallivm, params.type, mesa.lp_build_one(gallivm, params.type))
|
||||
mesa.lp_build_mask_end(params.mask)
|
||||
|
||||
mesa.lp_build_nir_soa(gallivm, shader, params, None)
|
||||
llvm.LLVMBuildRetVoid(builder)
|
||||
mesa.gallivm_verify_function(gallivm, ctypes.cast(fn, mesa.LLVMValueRef))
|
||||
mesa.lp_passmgr_run(gallivm.passmgr, gallivm.module, ctypes.cast(self.target_machine, mesa.LLVMTargetMachineRef), gallivm.module_name)
|
||||
obj_buf = expect(llvm.LLVMTargetMachineEmitToMemoryBuffer(self.target_machine, module, llvm.LLVMObjectFile, err:=cerr(),
|
||||
ctypes.pointer(buf:=llvm.LLVMMemoryBufferRef())), err, buf)
|
||||
obj = ctypes.string_at(llvm.LLVMGetBufferStart(obj_buf), llvm.LLVMGetBufferSize(obj_buf))
|
||||
|
||||
mesa.gallivm_destroy(gallivm)
|
||||
mesa.ralloc_free(shader)
|
||||
return obj
|
||||
|
||||
def disassemble(self, lib: bytes): cpu_objdump(lib)
|
||||
|
||||
class NAKCompiler(Compiler):
|
||||
# simplified from https://elixir.bootlin.com/mesa/mesa-26.0.3/source/src/nouveau/winsys/nouveau_device.c#L118
|
||||
@staticmethod
|
||||
def warps_per_sm(arch): return 48 if arch in ("sm_86", "sm_87", "sm_89", "sm_120") else 64
|
||||
def __init__(self, arch):
|
||||
self.arch = arch
|
||||
self.cc = mesa.nak_compiler_create(mesa.struct_nv_device_info(sm=int(arch[3:]), max_warps_per_mp=self.warps_per_sm(arch)))
|
||||
self.nir_options = bytes(mesa.nak_nir_options(self.cc).contents)
|
||||
super().__init__(f"compile_nak_{arch}")
|
||||
|
||||
def __del__(self): mesa.nak_compiler_destroy(self.cc)
|
||||
|
||||
def __reduce__(self): return NAKCompiler, (self.arch,)
|
||||
|
||||
def compile(self, src) -> bytes:
|
||||
shader = deserialize(src, self.nir_options)
|
||||
mesa.nak_preprocess_nir(shader, self.cc)
|
||||
ret = bytes((out:=mesa.nak_compile_shader(shader, False, self.cc, 0, None).contents).info) + ctypes.string_at(out.code, out.code_size)
|
||||
mesa.nak_shader_bin_destroy(out)
|
||||
mesa.ralloc_free(shader)
|
||||
return ret
|
||||
|
||||
def disassemble(self, lib: bytes):
|
||||
try:
|
||||
fn = (pathlib.Path(tempfile.gettempdir()) / f"tinynak_{hashlib.md5(lib).hexdigest()}").as_posix()
|
||||
with open(fn, "wb") as f: f.write(lib[ctypes.sizeof(mesa.struct_nak_shader_info):])
|
||||
print(system(f"nvdisasm -b SM{self.arch[3:]} {fn}"))
|
||||
except Exception as e: print("Failed to generate SASS", str(e), "Make sure your PATH contains nvdisasm binary of compatible version.")
|
||||
|
||||
def disas_adreno(lib:bytes, gpu_id=630):
|
||||
with tempfile.TemporaryFile('w+', buffering=1) as tf:
|
||||
@ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p)
|
||||
def hd(data, n, instr):
|
||||
fst, snd = data64(ctypes.cast(instr, ctypes.POINTER(ctypes.c_uint64)).contents.value)
|
||||
print(f"{n:04} [{fst:08x}_{snd:08x}] ", end="", flush=True, file=tf)
|
||||
|
||||
ctypes.CDLL(None).setlinebuf(fp:=ctypes.cast(ctypes.CDLL(None).fdopen(tf.fileno(), b"w"), ctypes.POINTER(mesa.struct__IO_FILE)))
|
||||
mesa.ir3_isa_disasm(lib, len(lib), fp, mesa.struct_isa_decode_options(gpu_id, True, 0, True, pre_instr_cb=hd))
|
||||
tf.seek(0)
|
||||
print(tf.read())
|
||||
|
||||
class IR3Compiler(Compiler):
|
||||
def __init__(self, arch):
|
||||
assert arch.split(',')[0] == "a630", "only a630 supported, for now"
|
||||
self.arch, self.dev_id = arch, mesa.struct_fd_dev_id(630, 0x6030001)
|
||||
self.cc = mesa.ir3_compiler_create(None, self.dev_id, mesa.fd_dev_info(self.dev_id),
|
||||
mesa.struct_ir3_compiler_options(disable_cache=True)).contents
|
||||
self.cc.has_preamble = False
|
||||
self.nir_options = bytes(mesa.ir3_get_compiler_options(self.cc).contents)
|
||||
super().__init__(f"compile_ir3_{arch}")
|
||||
|
||||
def __del__(self): mesa.ir3_compiler_destroy(self.cc)
|
||||
|
||||
def __reduce__(self): return IR3Compiler, (self.arch,)
|
||||
|
||||
# ir3_shader_variant info: https://elixir.bootlin.com/mesa/mesa-25.3.0/source/src/freedreno/ir3/ir3_shader.c#L1099
|
||||
def compile(self, src) -> bytes:
|
||||
nir_shader = deserialize(src, self.nir_options)
|
||||
mesa.ir3_nir_lower_io_vars_to_temporaries(nir_shader)
|
||||
mesa.ir3_finalize_nir(self.cc, mesa.struct_ir3_shader_nir_options(), nir_shader)
|
||||
shader = rzalloc(mesa.struct_ir3_shader, compiler=ctypes.pointer(self.cc), type=mesa.MESA_SHADER_COMPUTE, nir=nir_shader).contents
|
||||
mesa.ir3_nir_post_finalize(shader)
|
||||
v = rzalloc(mesa.struct_ir3_shader_variant, type=shader.type, compiler=ctypes.pointer(self.cc), key=mesa.struct_ir3_shader_key()).contents
|
||||
v.const_state, shader.variants, shader.variant_count = rzalloc(mesa.struct_ir3_const_state, ctypes.pointer(v)), ctypes.pointer(v), 1
|
||||
v.num_uavs = (info:=nir_shader.contents.info).num_ssbos + info.num_images
|
||||
assert not mesa.ir3_compile_shader_nir(self.cc, shader, v), "compilation failed"
|
||||
lib = ctypes.cast(mesa.ir3_shader_assemble(v), ctypes.POINTER(ctypes.c_uint32))
|
||||
# NB: bytes(v) means the pointers in v are no longer safe! a custom __reduce__ that supports pointers for c.Struct would make this simpler
|
||||
ret = bytes(v) + bytes(v.const_state.contents) + ctypes.string_at(v.imm_state.values, v.imm_state.count * 4) + ctypes.string_at(lib, v.info.size)
|
||||
mesa.ralloc_free(ctypes.pointer(v))
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def unpack_lib(lib: bytes) -> tuple[mesa.struct_ir3_shader_variant, mesa.struct_ir3_const_state, bytes, bytes]:
|
||||
shifted = lib[ctypes.sizeof(v:=mesa.struct_ir3_shader_variant.from_buffer_copy(lib)):]
|
||||
shifted = shifted[ctypes.sizeof(cs:=mesa.struct_ir3_const_state.from_buffer_copy(shifted)):]
|
||||
return v, cs, shifted[:v.imm_state.count * 4], shifted[v.imm_state.count * 4:]
|
||||
|
||||
def disassemble(self, lib: bytes): disas_adreno(self.unpack_lib(lib)[3], self.dev_id.gpu_id)
|
||||
38
tinygrad_repo/tinygrad/runtime/support/compiler_qcom.py
Normal file
38
tinygrad_repo/tinygrad/runtime/support/compiler_qcom.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import ctypes, struct
|
||||
from tinygrad.device import Compiler
|
||||
from tinygrad.helpers import DEBUG, system
|
||||
from tinygrad.runtime.support.compiler_mesa import disas_adreno
|
||||
# see https://github.com/sirhcm/tinydreno
|
||||
from tinygrad.runtime.autogen import llvm_qcom
|
||||
|
||||
def _read_lib(lib, off) -> int: return struct.unpack("I", lib[off:off+4])[0]
|
||||
|
||||
class QCOMCompiler(Compiler):
|
||||
def __init__(self, arch:str):
|
||||
assert arch.split(',')[0] == "a630", "only a630 supported"
|
||||
self.arch, self.chip_id, self.llvm_inst = arch, 0x6030001, llvm_qcom.cl_compiler_create_llvm_instance()
|
||||
super().__init__(f"compile_qcomcl_{arch}")
|
||||
|
||||
def __del__(self): llvm_qcom.cl_compiler_destroy_llvm_instance(self.llvm_inst)
|
||||
|
||||
def __reduce__(self): return QCOMCompiler, (self.arch,)
|
||||
|
||||
def checked(self, handle):
|
||||
if not handle or (data:=(hc.executable if (hc:=handle.contents).type == llvm_qcom.CL_HANDLE_LINKED else hc.compiled).contents).error_code != 0:
|
||||
llvm_qcom.cl_compiler_destroy_llvm_instance(self.llvm_inst)
|
||||
self.llvm_inst = llvm_qcom.cl_compiler_create_llvm_instance()
|
||||
raise RuntimeError("QCOM Compilation Error" + ("" if not handle else f": {ctypes.string_at(data.build_log).decode()}"))
|
||||
return handle
|
||||
|
||||
def compile(self, src) -> bytes:
|
||||
ch = self.checked(llvm_qcom.cl_compiler_compile_source(self.llvm_inst, self.chip_id, llvm_qcom.CL_MODE_64BIT, b"", 0, 0, 0, src.encode(), 0,
|
||||
llvm_qcom.CL_SRC_STR, None))
|
||||
if DEBUG >= 8: print(system("llvm-dis", input=ctypes.string_at((comp:=ch.contents.compiled.contents).llvm_bitcode, comp.llvm_bitcode_size)))
|
||||
lh = self.checked(llvm_qcom.cl_compiler_link_program(self.llvm_inst, self.chip_id, llvm_qcom.CL_MODE_64BIT, None, 1, ch))
|
||||
llvm_qcom.cl_compiler_handle_create_binary(lh, ctypes.byref(ptr:=ctypes.c_void_p()), ctypes.byref(sz:=ctypes.c_size_t()))
|
||||
for h in [ch, lh]: llvm_qcom.cl_compiler_free_handle(h)
|
||||
ret = ctypes.string_at(ptr, sz.value)
|
||||
llvm_qcom.cl_compiler_free_assembly(ptr)
|
||||
return ret
|
||||
|
||||
def disassemble(self, lib: bytes): disas_adreno(lib[(ofs:=_read_lib(lib, 0xc0)):ofs+_read_lib(lib, 0x100)], self.chip_id)
|
||||
82
tinygrad_repo/tinygrad/runtime/support/elf.py
Normal file
82
tinygrad_repo/tinygrad/runtime/support/elf.py
Normal file
@@ -0,0 +1,82 @@
|
||||
import struct, ctypes, ctypes.util
|
||||
from dataclasses import dataclass
|
||||
from tinygrad.helpers import getbits, i2u, unwrap
|
||||
from tinygrad.runtime.autogen import libc
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ElfSection: name:str; header:libc.Elf64_Shdr|libc.Elf32_Shdr; content:bytes # noqa: E702
|
||||
|
||||
def link_sym(sym:str, libs:list[str]) -> int:
|
||||
for lib in libs:
|
||||
try: return unwrap(ctypes.cast(getattr(ctypes.CDLL(ctypes.util.find_library(lib)), sym), ctypes.c_void_p).value)
|
||||
except (OSError, AttributeError): pass
|
||||
raise RuntimeError(f'Attempting to relocate against an undefined symbol {sym}')
|
||||
|
||||
def elf_loader(blob:bytes, force_section_align:int=1, link_libs:list[str]|None=None) -> tuple[memoryview, list[ElfSection], list[tuple]]:
|
||||
assert blob[:4] == libc.ELFMAG.encode(), "blob is not an ELF, missing magic bytes"
|
||||
ecls = {libc.ELFCLASS32: "Elf32", libc.ELFCLASS64: "Elf64"}[blob[libc.EI_CLASS]]
|
||||
|
||||
def _strtab(blob: bytes, idx: int) -> str: return blob[idx:blob.find(b'\x00', idx)].decode('utf-8')
|
||||
|
||||
header = getattr(libc, f"{ecls}_Ehdr").from_buffer_copy(blob)
|
||||
section_headers = (getattr(libc, f"{ecls}_Shdr") * header.e_shnum).from_buffer_copy(blob[header.e_shoff:])
|
||||
sh_strtab = blob[(shstrst:=section_headers[header.e_shstrndx].sh_offset):shstrst+section_headers[header.e_shstrndx].sh_size]
|
||||
sections = [ElfSection(_strtab(sh_strtab, sh.sh_name), sh, blob[sh.sh_offset:sh.sh_offset+sh.sh_size]) for sh in section_headers]
|
||||
|
||||
def _to_carray(sh, ctype): return (ctype * (sh.header.sh_size // sh.header.sh_entsize)).from_buffer_copy(sh.content)
|
||||
rel = [(sh, sh.name[4:], _to_carray(sh, getattr(libc, f"{ecls}_Rel"))) for sh in sections if sh.header.sh_type == libc.SHT_REL]
|
||||
rela = [(sh, sh.name[5:], _to_carray(sh, getattr(libc, f"{ecls}_Rela"))) for sh in sections if sh.header.sh_type == libc.SHT_RELA]
|
||||
symtab = next((_to_carray(sh, getattr(libc, f"{ecls}_Sym")) for sh in sections if sh.header.sh_type == libc.SHT_SYMTAB), None)
|
||||
progbits = [sh for sh in sections if sh.header.sh_type == libc.SHT_PROGBITS]
|
||||
|
||||
# Prealloc image for all fixed addresses.
|
||||
image = bytearray(max([sh.header.sh_addr + sh.header.sh_size for sh in progbits if sh.header.sh_addr != 0] + [0]))
|
||||
for sh in progbits:
|
||||
if sh.header.sh_addr != 0: image[sh.header.sh_addr:sh.header.sh_addr+sh.header.sh_size] = sh.content
|
||||
else:
|
||||
image += b'\0' * (((align:=max(sh.header.sh_addralign, force_section_align)) - len(image) % align) % align) + sh.content
|
||||
sh.header.sh_addr = len(image) - len(sh.content)
|
||||
|
||||
# Relocations
|
||||
relocs = []
|
||||
for sh, trgt_sh_name, c_rels in rel + rela:
|
||||
if trgt_sh_name == ".eh_frame": continue
|
||||
target_image_off = next(tsh for tsh in sections if tsh.name == trgt_sh_name).header.sh_addr
|
||||
rels = [(r.r_offset, unwrap(symtab)[getattr(libc, f"{ecls.upper()}_R_SYM")(r.r_info)], getattr(libc, f"{ecls.upper()}_R_TYPE")(r.r_info),
|
||||
getattr(r, "r_addend", 0)) for r in c_rels]
|
||||
relocs += [(target_image_off + roff, link_sym(_strtab(sh_strtab, sym.st_name), link_libs or []) if sym.st_shndx == 0 else
|
||||
sections[sym.st_shndx].header.sh_addr + sym.st_value, rtype, raddend) for roff, sym, rtype, raddend in rels]
|
||||
|
||||
return memoryview(image), sections, relocs
|
||||
|
||||
def jit_loader(obj: bytes, base:int=0, link_libs:list[str]|None=None) -> bytes:
|
||||
image_, _, relocs = elf_loader(obj, link_libs=link_libs)
|
||||
image = bytearray(image_)
|
||||
|
||||
def relocate(instr: int, base: int, ploc: int, tgt: int, r_type: int):
|
||||
match r_type:
|
||||
# https://refspecs.linuxfoundation.org/elf/x86_64-abi-0.95.pdf
|
||||
case libc.R_X86_64_PC32: return i2u(32, tgt-ploc)
|
||||
case libc.R_X86_64_PLT32: return i2u(32, tgt-ploc-base)
|
||||
# https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst for definitions of relocations
|
||||
# https://www.scs.stanford.edu/~zyedidia/arm64/index.html for instruction encodings
|
||||
case libc.R_AARCH64_ADR_PREL_PG_HI21:
|
||||
rel_pg = (tgt & ~0xFFF) - (ploc & ~0xFFF)
|
||||
return instr | (getbits(rel_pg, 12, 13) << 29) | (getbits(rel_pg, 14, 32) << 5)
|
||||
case libc.R_AARCH64_ADD_ABS_LO12_NC: return instr | (getbits(tgt, 0, 11) << 10)
|
||||
case libc.R_AARCH64_LDST16_ABS_LO12_NC: return instr | (getbits(tgt, 1, 11) << 10)
|
||||
case libc.R_AARCH64_LDST32_ABS_LO12_NC: return instr | (getbits(tgt, 2, 11) << 10)
|
||||
case libc.R_AARCH64_LDST64_ABS_LO12_NC: return instr | (getbits(tgt, 3, 11) << 10)
|
||||
case libc.R_AARCH64_LDST128_ABS_LO12_NC: return instr | (getbits(tgt, 4, 11) << 10)
|
||||
case libc.R_AARCH64_CALL26:
|
||||
if -(2**25) <= tgt-ploc-base and tgt-ploc-base <= (2**25 - 1) * 4: return instr | getbits(tgt-ploc-base, 2, 27)
|
||||
nonlocal image
|
||||
# create trampoline: LDR x17, 8 BR x17
|
||||
image += struct.pack("<IIQ", 0x58000051, 0xD61F0220, tgt)
|
||||
return instr | getbits(len(image)-ploc-16, 2, 27)
|
||||
raise NotImplementedError(f"Encountered unknown relocation type {r_type}")
|
||||
|
||||
# This is needed because we have an object file, not a .so that has all internal references (like loads of constants from .rodata) resolved.
|
||||
for ploc,tgt,r_type,r_addend in relocs:
|
||||
image[ploc:ploc+4] = struct.pack("<I", relocate(struct.unpack("<I", image[ploc:ploc+4])[0], base, ploc, tgt+r_addend, r_type))
|
||||
return bytes(image)
|
||||
643
tinygrad_repo/tinygrad/runtime/support/hcq.py
Normal file
643
tinygrad_repo/tinygrad/runtime/support/hcq.py
Normal file
@@ -0,0 +1,643 @@
|
||||
from __future__ import annotations
|
||||
from typing import cast, Callable, Type, TypeVar, Generic, Any
|
||||
import contextlib, decimal, statistics, time, ctypes, array, os, struct, collections, functools, itertools
|
||||
from dataclasses import replace
|
||||
try: import fcntl # windows misses that
|
||||
except ImportError: fcntl = None #type:ignore[assignment]
|
||||
from tinygrad.helpers import DEV, PROFILE, getenv, to_mv, from_mv, cpu_profile, ProfileRangeEvent, select_first_inited, select_by_name, unwrap
|
||||
from tinygrad.helpers import suppress_finalizing, pluralize, TracingKey
|
||||
from tinygrad.device import Device, BufferSpec, Compiled, LRUAllocator, ProfileDeviceEvent, ProfileProgramEvent
|
||||
from tinygrad.uop.ops import sym_infer, sint, UOp
|
||||
from tinygrad.runtime.autogen import libc
|
||||
from tinygrad.runtime.support.memory import BumpAllocator
|
||||
from tinygrad.renderer import Renderer
|
||||
|
||||
class MMIOInterface:
|
||||
def __init__(self, addr:int, nbytes:int, fmt='B'): self.mv, self.addr, self.nbytes, self.fmt = to_mv(addr, nbytes).cast(fmt), addr, nbytes, fmt
|
||||
def __len__(self): return self.nbytes // struct.calcsize(self.fmt)
|
||||
def __getitem__(self, k): return (self.mv[k] if self.fmt == 'B' else self.mv[k].tolist()) if isinstance(k, slice) else self.mv[k]
|
||||
def __setitem__(self, k, v): self.mv[k] = v
|
||||
def view(self, offset:int=0, size:int|None=None, fmt=None) -> MMIOInterface:
|
||||
return MMIOInterface(self.addr+offset, (self.nbytes - offset) if size is None else size, fmt=fmt or self.fmt)
|
||||
|
||||
class FileIOInterface:
|
||||
"""
|
||||
Hardware Abstraction Layer for HCQ devices. The class provides a unified interface for interacting with hardware devices.
|
||||
"""
|
||||
|
||||
def __init__(self, path:str="", flags:int=os.O_RDONLY, fd:int|None=None):
|
||||
self.path:str = path
|
||||
self.fd:int = fd or os.open(path, flags)
|
||||
def __del__(self):
|
||||
if hasattr(self, 'fd'): os.close(self.fd)
|
||||
def ioctl(self, request, arg): return fcntl.ioctl(self.fd, request, arg)
|
||||
def mmap(self, start, sz, prot, flags, offset): return FileIOInterface._mmap(start, sz, prot, flags, self.fd, offset)
|
||||
def read(self, size=None, binary=False, offset=None):
|
||||
if offset is not None: self.seek(offset)
|
||||
with open(self.fd, "rb" if binary else "r", closefd=False) as file: return file.read(size)
|
||||
def write(self, content, binary=False, offset=None):
|
||||
if offset is not None: self.seek(offset)
|
||||
with open(self.fd, "wb" if binary else "w", closefd=False) as file: file.write(content)
|
||||
def listdir(self): return os.listdir(self.path)
|
||||
def seek(self, offset): os.lseek(self.fd, offset, os.SEEK_SET)
|
||||
@staticmethod
|
||||
def _mmap(start, sz, prot, flags, fd, offset):
|
||||
x = libc.mmap(start, sz, prot, flags, fd, offset)
|
||||
if x == 0xffffffffffffffff: raise OSError(f"Failed to mmap {sz} bytes at {hex(start)}: {os.strerror(ctypes.get_errno())}")
|
||||
return x
|
||||
@staticmethod
|
||||
def anon_mmap(start, sz, prot, flags, offset): return FileIOInterface._mmap(start, sz, prot, flags, -1, offset)
|
||||
@staticmethod
|
||||
def munmap(buf, sz): return libc.munmap(buf, sz)
|
||||
@staticmethod
|
||||
def exists(path): return os.path.exists(path)
|
||||
@staticmethod
|
||||
def readlink(path): return os.readlink(path)
|
||||
@staticmethod
|
||||
def eventfd(initval, flags=None): return FileIOInterface(fd=os.eventfd(initval, flags)) # type: ignore[attr-defined]
|
||||
|
||||
if DEV.interface.startswith("MOCK"): from test.mockgpu.mockgpu import MockFileIOInterface as FileIOInterface # noqa: F401 # pylint: disable=unused-import
|
||||
|
||||
# **************** for HCQ Compatible Devices ****************
|
||||
|
||||
def hcq_filter_visible_devices(devs, device):
|
||||
assert (v:=getenv("HCQ_VISIBLE_DEVICES", "")) == "", f"HCQ_VISIBLE_DEVICES={v} is deprecated, use DEV={DEV.target(device, indices=v)} instead"
|
||||
if '-' in (idstr:=DEV.target(device).indices): ids = list(range(int(idstr.split('-')[0]), int(idstr.split('-')[1])+1))
|
||||
else: ids = [int(x) for x in idstr.split(',') if x.strip()]
|
||||
assert all(x < len(devs) for x in ids), f"invalid visibility filter: {ids} ({pluralize('device', len(devs))} available)"
|
||||
return [devs[x] for x in ids] if ids else devs
|
||||
|
||||
SignalType = TypeVar('SignalType', bound='HCQSignal')
|
||||
HCQDeviceType = TypeVar('HCQDeviceType', bound='HCQCompiled')
|
||||
ProgramType = TypeVar('ProgramType', bound='HCQProgram')
|
||||
ArgsStateType = TypeVar('ArgsStateType', bound='HCQArgsState')
|
||||
|
||||
class HWQueue(Generic[SignalType, HCQDeviceType, ProgramType, ArgsStateType]):
|
||||
"""
|
||||
A base class for hardware command queues in the HCQ (Hardware Command Queue) API.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._q:Any = []
|
||||
self.binded_device:HCQDeviceType|None = None
|
||||
self.q_sints:list[tuple[int, int]] = []
|
||||
self.mv_sints:list[tuple[MMIOInterface, int, int, int|None]] = []
|
||||
self.syms:list[sint] = []
|
||||
self._prev_resolved_syms:list[int|None] = []
|
||||
|
||||
def _new_sym(self, sym:sint) -> int:
|
||||
if sym not in self.syms:
|
||||
self.syms.append(sym)
|
||||
self._prev_resolved_syms.append(None)
|
||||
return self.syms.index(sym)
|
||||
|
||||
def q(self, *values):
|
||||
"""
|
||||
Enqueues values in the queue.
|
||||
|
||||
Args:
|
||||
values: The values to enqueue in the queue.
|
||||
"""
|
||||
|
||||
for v in values:
|
||||
if isinstance(v, UOp):
|
||||
self.q_sints.append((len(self._q), self._new_sym(v)))
|
||||
self._q.append(0xbadc0ded)
|
||||
else: self._q.append(v)
|
||||
|
||||
# *** common commands ***
|
||||
|
||||
def timestamp(self, signal:SignalType):
|
||||
"""
|
||||
Enqueues a timestamp command which records the current time in a signal after all previously enqueued commands are completed.
|
||||
|
||||
Args:
|
||||
signal: The signal to store the timestamp
|
||||
"""
|
||||
|
||||
def signal(self, signal:SignalType, value:sint):
|
||||
"""
|
||||
Enqueues a signal command which sets the signal to the given value, ensuring all previous operations are completed.
|
||||
|
||||
Args:
|
||||
signal: The signal to set
|
||||
value: The value to set the signal to
|
||||
"""
|
||||
|
||||
def wait(self, signal:SignalType, value:sint):
|
||||
"""
|
||||
Enqueues a wait command which halts execution until the signal is greater than or equal to a specific value.
|
||||
|
||||
Args:
|
||||
signal: The signal to wait on
|
||||
value: The value to wait for
|
||||
"""
|
||||
|
||||
# *** commands for compute queues ***
|
||||
|
||||
def memory_barrier(self):
|
||||
"""
|
||||
Enqueues a memory barrier command to ensure memory coherence between agents. Only on compute queues.
|
||||
"""
|
||||
|
||||
def exec(self, prg:ProgramType, args_state:ArgsStateType, global_size:tuple[sint, ...], local_size:tuple[sint, ...]):
|
||||
"""
|
||||
Enqueues an execution command for a kernel program. Only on compute queues.
|
||||
|
||||
Args:
|
||||
prg: The program to execute
|
||||
args_state: The args state to execute program with
|
||||
global_size: The global work size
|
||||
local_size: The local work size
|
||||
"""
|
||||
|
||||
def write(self, b:HCQBuffer, val:sint, b64:bool=False):
|
||||
"""
|
||||
Enqueues a command to write a value to a buffer address after all previously enqueued commands are completed.
|
||||
|
||||
Args:
|
||||
b: The buffer to write to
|
||||
val: The value to write
|
||||
b64: If True, write a 64-bit value; otherwise write 32-bit
|
||||
"""
|
||||
raise NotImplementedError("write not implemented")
|
||||
|
||||
def poll_bit(self, b:HCQBuffer, val:sint, mask:int):
|
||||
"""
|
||||
Enqueues a poll command which halts execution until (mem[b] & mask) == val.
|
||||
val must be 0 or mask (i.e. checks if masked bits are all clear or all set).
|
||||
|
||||
Args:
|
||||
b: The buffer to poll
|
||||
val: The expected value after masking (0 or mask)
|
||||
mask: The bit mask to test
|
||||
"""
|
||||
raise NotImplementedError("poll_bit not implemented")
|
||||
|
||||
# *** commands for copy queues ***
|
||||
|
||||
def copy(self, dest:HCQBuffer, src:HCQBuffer, copy_size:int):
|
||||
"""
|
||||
Enqueues a copy command to transfer data. Only on copy queues.
|
||||
|
||||
Args:
|
||||
dest: The destination buffer of the copy
|
||||
src: The source buffer of the copy
|
||||
copy_size: The size of data to copy
|
||||
"""
|
||||
|
||||
# *** submit and bind commands ***
|
||||
|
||||
def bind(self, dev:HCQDeviceType):
|
||||
"""
|
||||
Associates the queue with a specific device for optimized execution.
|
||||
|
||||
This optional method allows backend implementations to tailor the queue for efficient use on the given device. When implemented, it can eliminate
|
||||
the need to copy queues into the device, thereby enhancing performance.
|
||||
|
||||
Args:
|
||||
dev: The target device for queue optimization.
|
||||
|
||||
Note:
|
||||
Implementing this method is optional but recommended for performance gains.
|
||||
"""
|
||||
|
||||
def bind_args_state(self, args_state:ArgsStateType):
|
||||
for vals, mem, fmt in args_state.bind_data: self.bind_sints_to_mem(*vals, mem=mem, fmt=fmt)
|
||||
|
||||
def bind_sints(self, *vals:sint, mem:MMIOInterface, struct_t:Type[ctypes.Structure], start_field:str, fmt, mask:int|None=None):
|
||||
self.bind_sints_to_mem(*vals, mem=mem, fmt=fmt, mask=mask, offset=getattr(struct_t, start_field).offset)
|
||||
|
||||
def bind_sints_to_mem(self, *vals:sint, mem:MMIOInterface, fmt, mask:int|None=None, offset:int=0):
|
||||
mv = mem.view(offset=offset, size=len(vals)*8, fmt=fmt)
|
||||
for i, val in enumerate(vals):
|
||||
if isinstance(val, int): mv[i] = val if mask is None else ((mv[i] & ~mask) | val)
|
||||
else: self.mv_sints.append((mv, i, self._new_sym(val), mask))
|
||||
|
||||
def _apply_var_vals(self, var_vals:dict[str, int]):
|
||||
resolved_syms = [sym_infer(sym, var_vals) for sym in self.syms]
|
||||
|
||||
for off, sym_idx in self.q_sints:
|
||||
if self._prev_resolved_syms[sym_idx] == resolved_syms[sym_idx]: continue
|
||||
self._q[off] = resolved_syms[sym_idx]
|
||||
|
||||
for mv, off, sym_idx, mask in self.mv_sints:
|
||||
if self._prev_resolved_syms[sym_idx] == resolved_syms[sym_idx]: continue
|
||||
mv[off] = resolved_syms[sym_idx] if mask is None else ((mv[off] & ~mask) | resolved_syms[sym_idx])
|
||||
|
||||
self._prev_resolved_syms = cast(list[int|None], resolved_syms)
|
||||
|
||||
def submit(self, dev:HCQDeviceType, var_vals:dict[str, int]|None=None):
|
||||
"""
|
||||
Submits the command queue to a specific device for execution.
|
||||
|
||||
Args:
|
||||
dev: The device to submit the queue to
|
||||
"""
|
||||
|
||||
if var_vals is not None: self._apply_var_vals(var_vals)
|
||||
self._submit(dev)
|
||||
return self
|
||||
def _submit(self, dev:HCQDeviceType): raise NotImplementedError("need _submit")
|
||||
|
||||
class HCQSignal(Generic[HCQDeviceType]):
|
||||
def __init__(self, base_buf:HCQBuffer, value:int=0, owner:HCQDeviceType|None=None, is_timeline:bool=False, timestamp_divider=1000, virt=False):
|
||||
self.base_buf, self.owner, self.is_timeline = base_buf, owner, is_timeline
|
||||
self.should_return = isinstance(self.base_buf.va_addr, int) and self.owner is not None and not virt
|
||||
self.timestamp_divider:decimal.Decimal = decimal.Decimal(timestamp_divider)
|
||||
if isinstance(self.base_buf.va_addr, int) and not virt: self.value = value
|
||||
|
||||
def __del__(self):
|
||||
if self.should_return: HCQCompiled.signal_pool[unwrap(self.owner).peer_group].append(self.base_buf)
|
||||
|
||||
@property
|
||||
def value_addr(self) -> sint: return self.base_buf.va_addr
|
||||
|
||||
@property
|
||||
def timestamp_addr(self) -> sint: return self.base_buf.va_addr + 8
|
||||
|
||||
@property
|
||||
def value(self) -> int: return self.base_buf.cpu_view().view(0, 8, 'Q')[0]
|
||||
|
||||
@value.setter
|
||||
def value(self, new_value:int): self.base_buf.cpu_view().view(0, 8, 'Q')[0] = new_value
|
||||
|
||||
@property
|
||||
def timestamp(self) -> decimal.Decimal:
|
||||
"""
|
||||
Get the timestamp field of the signal.
|
||||
|
||||
This property provides read-only access to the signal's timestamp.
|
||||
|
||||
Returns:
|
||||
The timestamp in microseconds.
|
||||
"""
|
||||
return self.base_buf.cpu_view().view(8, 8, 'Q')[0] / self.timestamp_divider
|
||||
|
||||
def _sleep(self, time_spent_since_last_sleep_ms:int):
|
||||
"""
|
||||
Optional function which can implement sleep functionality for the signal.
|
||||
Raises RuntimeError if a fault is detected.
|
||||
"""
|
||||
|
||||
def wait(self, value:int, timeout:int|None=None):
|
||||
"""
|
||||
Waits the signal is greater than or equal to a specific value.
|
||||
|
||||
Args:
|
||||
value: The value to wait for.
|
||||
timeout: Maximum time to wait in milliseconds. Defaults to 30s.
|
||||
"""
|
||||
timeout = timeout or getenv("HCQDEV_WAIT_TIMEOUT_MS", 30000)
|
||||
start_time = int(time.perf_counter() * 1000)
|
||||
while (not_passed:=(prev_value:=self.value) < value) and (cur_time:=int(time.perf_counter() * 1000)) - start_time < timeout:
|
||||
self._sleep(cur_time - start_time)
|
||||
if self.value != prev_value: start_time = int(time.perf_counter() * 1000) # progress was made, reset timer
|
||||
if not_passed and self.value < value: raise RuntimeError(f"Wait timeout: {timeout} ms! (the signal is not set to {value}, but {self.value})")
|
||||
|
||||
@contextlib.contextmanager
|
||||
def hcq_profile(dev:HCQCompiled, enabled, desc, queue_type:Callable[[], HWQueue]|None=None, queue:HWQueue|None=None, dev_suff:str|None=None):
|
||||
st, en = (dev.new_signal(), dev.new_signal()) if enabled else (None, None)
|
||||
assert queue is not None or queue_type is not None, "Either queue or queue_type must be provided"
|
||||
|
||||
if enabled and queue is not None: queue.timestamp(st)
|
||||
elif enabled and queue_type is not None:
|
||||
queue_type().wait(dev.timeline_signal, dev.timeline_value - 1).timestamp(st).signal(dev.timeline_signal, dev.next_timeline()).submit(dev)
|
||||
|
||||
try: yield (st, en)
|
||||
finally:
|
||||
if enabled and queue is not None: queue.timestamp(en)
|
||||
elif enabled and queue_type is not None:
|
||||
queue_type().wait(dev.timeline_signal, dev.timeline_value - 1).timestamp(en).signal(dev.timeline_signal, dev.next_timeline()).submit(dev)
|
||||
|
||||
if enabled and PROFILE: dev.sig_prof_records.append((unwrap(st), unwrap(en), desc, f"{dev.device}:{dev_suff}" if dev_suff else dev.device))
|
||||
|
||||
class HCQArgsState(Generic[ProgramType]):
|
||||
def __init__(self, buf:HCQBuffer, prg:ProgramType, bufs:tuple[HCQBuffer, ...], vals:tuple[sint|None, ...]=()):
|
||||
self.buf, self.prg, self.bufs, self.vals = buf, prg, bufs, vals
|
||||
self.bind_data:list[tuple[tuple[sint, ...], MMIOInterface, str]] = []
|
||||
|
||||
def bind_sints_to_buf(self, *vals:sint, buf:HCQBuffer, fmt, offset=0): self.bind_data.append((vals, buf.cpu_view().view(offset=offset), fmt))
|
||||
|
||||
class CLikeArgsState(HCQArgsState[ProgramType]):
|
||||
def __init__(self, buf:HCQBuffer, prg:ProgramType, bufs:tuple[HCQBuffer, ...], vals:tuple[sint|None, ...]=(), prefix:list[int]|None=None):
|
||||
super().__init__(buf, prg, bufs, vals=vals)
|
||||
|
||||
if prefix is not None: self.buf.cpu_view().view(size=len(prefix) * 4, fmt='I')[:] = array.array('I', prefix)
|
||||
|
||||
self.bind_sints_to_buf(*[b.va_addr for b in bufs], buf=self.buf, fmt='Q', offset=len(prefix or []) * 4)
|
||||
assert None not in vals
|
||||
self.bind_sints_to_buf(*cast(tuple[sint, ...], vals), buf=self.buf, fmt='I', offset=len(prefix or []) * 4 + len(bufs) * 8)
|
||||
|
||||
class HCQProgram(Generic[HCQDeviceType]):
|
||||
def __init__(self, args_state_t:Type[HCQArgsState], dev:HCQDeviceType, name:str, kernargs_alloc_size:int, lib:bytes|None=None, base:int|None=None):
|
||||
self.args_state_t, self.dev, self.name, self.kernargs_alloc_size = args_state_t, dev, name, kernargs_alloc_size
|
||||
self.prof_prg_counter = next(self.dev.prof_prg_counter)
|
||||
if PROFILE: Compiled.profile_events += [ProfileProgramEvent(dev.device, name, lib, base, self.prof_prg_counter)]
|
||||
|
||||
@staticmethod
|
||||
def _fini(dev, buf, spec): dev.allocator.free(buf, buf.size, spec)
|
||||
|
||||
def fill_kernargs(self, bufs:tuple[HCQBuffer, ...], vals:tuple[int|None, ...]=(), kernargs:HCQBuffer|None=None) -> HCQArgsState:
|
||||
"""
|
||||
Fills arguments for the kernel, optionally allocating space from the device if `kernargs_ptr` is not provided.
|
||||
Args:
|
||||
bufs: Buffers to be written to kernel arguments.
|
||||
vals: Values to be written to kernel arguments.
|
||||
kernargs_ptr: Optional pointer to pre-allocated kernel arguments memory.
|
||||
Returns:
|
||||
Arguments state with the given buffers and values set for the program.
|
||||
"""
|
||||
argsbuf = kernargs or self.dev.kernargs_buf.offset(offset=self.dev.kernargs_offset_allocator.alloc(self.kernargs_alloc_size, 8),
|
||||
size=self.kernargs_alloc_size)
|
||||
return self.args_state_t(argsbuf, self, bufs, vals=vals)
|
||||
|
||||
def __call__(self, *bufs:HCQBuffer, global_size:tuple[int,int,int]=(1,1,1), local_size:tuple[int,int,int]=(1,1,1),
|
||||
vals:tuple[int|None, ...]=(), wait:bool=False, timeout:int|None=None) -> float|None:
|
||||
"""
|
||||
Enqueues the program for execution with the given arguments and dimensions.
|
||||
|
||||
Args:
|
||||
bufs: Buffer arguments to execute the kernel with.
|
||||
global_size: Specifies the global work size for kernel execution (equivalent to CUDA's grid size).
|
||||
local_size: Specifies the local work size for kernel execution (equivalent to CUDA's block size).
|
||||
vals: Value arguments to execute the kernel with.
|
||||
wait: If True, waits for the kernel to complete execution.
|
||||
|
||||
Returns:
|
||||
Execution time of the kernel if 'wait' is True, otherwise None.
|
||||
"""
|
||||
|
||||
kernargs = self.fill_kernargs(bufs, vals)
|
||||
q = unwrap(self.dev.hw_compute_queue_t)().wait(self.dev.timeline_signal, self.dev.timeline_value - 1).memory_barrier()
|
||||
|
||||
self.dev.prof_exec_counter += 1
|
||||
with hcq_profile(self.dev, queue=q, desc=self.name, enabled=wait or PROFILE) as (sig_st, sig_en):
|
||||
q.exec(self, kernargs, global_size, local_size)
|
||||
|
||||
q.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
|
||||
if wait: self.dev.synchronize(timeout=timeout)
|
||||
return (float(sig_en.timestamp - sig_st.timestamp) / 1e6) if wait else None
|
||||
|
||||
class HCQCompiled(Compiled, Generic[SignalType]):
|
||||
"""
|
||||
A base class for devices compatible with the HCQ (Hardware Command Queue) API.
|
||||
"""
|
||||
peer_groups: dict[str, list[HCQCompiled]] = collections.defaultdict(list)
|
||||
signal_pages: dict[str, list[HCQBuffer]] = collections.defaultdict(list) # per peer group
|
||||
signal_pool: dict[str, list[HCQBuffer]] = collections.defaultdict(list) # per peer group
|
||||
cpu_devices: list[HCQCompiled] = []
|
||||
|
||||
def __init__(self, device:str, allocator:HCQAllocatorBase, compilers:list[type[Renderer]], runtime, signal_t:Type[SignalType]|None=None,
|
||||
comp_queue_t:Callable[..., HWQueue]|None=None, copy_queue_t:Callable[..., HWQueue]|None=None, kernargs_size=(16 << 20),
|
||||
sigalloc_size=0x1000, can_recover:bool=False, arch=None):
|
||||
self.device_id:int = int(device.split(":")[1]) if ":" in device else 0
|
||||
|
||||
from tinygrad.runtime.graph.hcq import HCQGraph
|
||||
super().__init__(device, allocator, compilers, runtime, HCQGraph, arch=arch)
|
||||
|
||||
self.peer_group = getattr(getattr(self, 'iface', None), 'peer_group', device.split(":")[0])
|
||||
HCQCompiled.peer_groups[self.peer_group].append(self)
|
||||
|
||||
self.signal_t, self.hw_compute_queue_t, self.hw_copy_queue_t = signal_t, comp_queue_t, copy_queue_t
|
||||
|
||||
self.timeline_value:int = 1
|
||||
self.sig_prof_records:list[tuple[HCQSignal, HCQSignal, str|TracingKey, str]] = []
|
||||
self.prof_exec_counter:int = 0
|
||||
self.prof_prg_counter = itertools.count(0)
|
||||
|
||||
if signal_t is not None:
|
||||
# Map signals if any
|
||||
for sig_page in HCQCompiled.signal_pages[self.peer_group]: cast(HCQAllocator, self.allocator).map(sig_page)
|
||||
|
||||
self.sigalloc_size = sigalloc_size
|
||||
self.timeline_signal, self._shadow_timeline_signal = self.new_signal(value=0, is_timeline=True), self.new_signal(value=0, is_timeline=True)
|
||||
|
||||
if comp_queue_t is not None:
|
||||
self.kernargs_buf:HCQBuffer = self.allocator.alloc(kernargs_size, BufferSpec(cpu_access=True))
|
||||
self.kernargs_offset_allocator:BumpAllocator = BumpAllocator(self.kernargs_buf.size, wrap=True)
|
||||
|
||||
self.can_recover = can_recover # Whether the device can recover from faults or timeouts
|
||||
self.error_state:Exception|None = None # Exception if error is unrecoverable and sync will always fail
|
||||
|
||||
if self._is_cpu(): HCQCompiled.cpu_devices.append(self)
|
||||
|
||||
def count(self) -> int: return self.iface.count if hasattr(self, 'iface') else 1
|
||||
|
||||
def synchronize(self, timeout:int|None=None):
|
||||
if self.error_state is not None: raise self.error_state
|
||||
if not hasattr(self, 'timeline_signal'): return
|
||||
|
||||
# If we have any work on CPU devices, need to synchronize them. This is just an optimization to release GIL allowing to finish faster.
|
||||
if not self._is_cpu():
|
||||
for dev in HCQCompiled.cpu_devices: dev.synchronize()
|
||||
|
||||
try: self.timeline_signal.wait(self.timeline_value - 1, timeout=timeout if timeout is not None and self.can_recover else None)
|
||||
except RuntimeError as e:
|
||||
self.error_state = e
|
||||
if hasattr(self, 'on_device_hang'): self.on_device_hang()
|
||||
raise e
|
||||
|
||||
if self.timeline_value > (1 << 31): self._wrap_timeline_signal()
|
||||
if PROFILE:
|
||||
Compiled.profile_events += [ProfileRangeEvent(dev, name, st.timestamp, en.timestamp) for st,en,name,dev in self.sig_prof_records]
|
||||
self.sig_prof_records = []
|
||||
|
||||
def next_timeline(self):
|
||||
self.timeline_value += 1
|
||||
return self.timeline_value - 1
|
||||
|
||||
def new_signal(self, **kwargs) -> SignalType:
|
||||
assert self.signal_t is not None, "Device does not support signals"
|
||||
if not HCQCompiled.signal_pool[pg:=self.peer_group]:
|
||||
HCQCompiled.signal_pages[pg].append(alc:=self.allocator.alloc(self.sigalloc_size, BufferSpec(host=True, uncached=True, cpu_access=True)))
|
||||
HCQCompiled.signal_pool[pg] += [alc.offset(offset=off, size=16) for off in range(0, alc.size, 16)]
|
||||
for dev in HCQCompiled.peer_groups[pg]: cast(HCQAllocator, dev.allocator).map(alc)
|
||||
return self.signal_t(base_buf=HCQCompiled.signal_pool[pg].pop(), owner=self, **kwargs)
|
||||
|
||||
def device_props(self) -> dict[str,Any]: return {} # to be overridden if needed. dict keys are backend dependent.
|
||||
|
||||
def hw_compute_queues(self) -> list[tuple[str|None, Callable[[], HWQueue]]]:
|
||||
return [(None, self.hw_compute_queue_t)] if self.hw_compute_queue_t is not None else []
|
||||
def hw_copy_queues(self) -> list[tuple[str, Callable[[], HWQueue]]]:
|
||||
return [("SDMA:0", self.hw_copy_queue_t)] if self.hw_copy_queue_t is not None else []
|
||||
|
||||
def _at_profile_finalize(self):
|
||||
self.synchronize() # Expect device to be synchronizes
|
||||
|
||||
def _sync(d:HCQCompiled, q_t:Callable[[], HWQueue]):
|
||||
q_t().timestamp(d.timeline_signal).signal(d.timeline_signal, d.next_timeline()).submit(d)
|
||||
st = time.perf_counter_ns()
|
||||
d.timeline_signal.wait(d.timeline_value - 1) # average of the two
|
||||
et = time.perf_counter_ns()
|
||||
return (decimal.Decimal(et+st) / 2000) - d.timeline_signal.timestamp
|
||||
|
||||
for prefix, q_t in self.hw_compute_queues() + self.hw_copy_queues():
|
||||
devname = f"{self.device}:{prefix}" if prefix else self.device
|
||||
Compiled.profile_events += [ProfileDeviceEvent(devname, statistics.median([_sync(self, q_t) for _ in range(40)]), props=self.device_props())]
|
||||
|
||||
def _wrap_timeline_signal(self):
|
||||
self.timeline_signal, self._shadow_timeline_signal, self.timeline_value = self._shadow_timeline_signal, self.timeline_signal, 1
|
||||
self.timeline_signal.value = 0
|
||||
cast(HCQAllocatorBase, self.allocator).b_timeline = [0] * len(cast(HCQAllocatorBase, self.allocator).b)
|
||||
|
||||
def _realloc(self, oldbuf:HCQBuffer|None, new_size:int, options:BufferSpec|None=None, force=False) -> tuple[HCQBuffer, bool]:
|
||||
if oldbuf is not None: self.allocator.free(oldbuf, oldbuf.size, options=options)
|
||||
try: buf, realloced = self.allocator.alloc(new_size, options=options), True
|
||||
except MemoryError:
|
||||
if force: raise
|
||||
buf, realloced = self.allocator.alloc(oldbuf.size if oldbuf is not None else new_size, options=options), False
|
||||
return buf, realloced
|
||||
|
||||
def _select_iface(self):
|
||||
assert (v:=getenv(k:=f'{type(self).__name__[:-6].upper()}_IFACE', "")) == "", \
|
||||
f"{k}={v} is deprecated, use DEV={replace(DEV.target(type(self).__name__[:-6]), interface=v)} instead"
|
||||
assert hasattr(self, "ifaces"), "must have ifaces to select an iface"
|
||||
t = DEV.target(dev:=type(self).__name__[:-6])
|
||||
filtered = select_by_name(self.ifaces, lambda i: i.__name__[:-5], t.interface, f"{dev} has no interface {t.interface!r}")
|
||||
filtered = [i for i in filtered if t.interface.startswith("MOCK") or not i.__name__[:-5].startswith("MOCK")] # never fallback to mock ifaces
|
||||
return select_first_inited([functools.partial(cast(Callable, iface), self, self.device_id) for iface in filtered],
|
||||
f"No interface for {dev}:{self.device_id} is available")
|
||||
|
||||
def _is_cpu(self) -> bool: return hasattr(self, 'device') and self.device.split(":")[0] == "CPU"
|
||||
|
||||
def rdma_dev(self):
|
||||
for i in itertools.count():
|
||||
if (dev:=next((d for d in HCQCompiled.peer_groups[self.peer_group] if type(d).__name__ == 'RDMADevice'), None)): return dev
|
||||
try: Device[f'RDMA:{i}']
|
||||
except IndexError: raise RuntimeError(f"No RDMA found for peer group '{self.peer_group}'")
|
||||
|
||||
def finalize(self):
|
||||
try: self.synchronize() # Try to finalize device in any case.
|
||||
except RuntimeError as e: print(f"{self.device} synchronization failed before finalizing: {e}")
|
||||
|
||||
# If the device has an interface, call its device_fini method to clean up resources.
|
||||
if hasattr(self, 'iface') and hasattr(self.iface, 'device_fini'): self.iface.device_fini()
|
||||
|
||||
class HCQBuffer:
|
||||
def __init__(self, va_addr:sint, size:int, meta:Any=None, _base:HCQBuffer|None=None, view:MMIOInterface|None=None,
|
||||
owner:HCQCompiled|None=None):
|
||||
self.va_addr, self.size, self.meta, self._base, self.view = va_addr, size, meta, _base, view
|
||||
self._devs, self.owner = ([owner] if owner is not None else []), owner
|
||||
self._mappings:dict[HCQCompiled, HCQBuffer] = {} # mapping to the other devices
|
||||
|
||||
def offset(self, offset:int=0, size:int|None=None) -> HCQBuffer:
|
||||
return HCQBuffer(self.va_addr+offset, size or (self.size - offset), owner=self.owner, meta=self.meta,
|
||||
_base=self._base or self, view=(self.view.view(offset=offset, size=size) if self.view is not None else None))
|
||||
|
||||
def cpu_view(self) -> MMIOInterface:
|
||||
assert self.view is not None, "buffer has no cpu_view"
|
||||
return self.view
|
||||
|
||||
@property
|
||||
def base(self) -> HCQBuffer: return self._base or self
|
||||
|
||||
@property
|
||||
def mappings(self): return self._mappings if self._base is None else self._base._mappings
|
||||
|
||||
@property
|
||||
def mapped_devs(self): return self._devs if self._base is None else self._base._devs
|
||||
|
||||
class HCQAllocatorBase(LRUAllocator[HCQDeviceType], Generic[HCQDeviceType]):
|
||||
"""
|
||||
A base allocator class compatible with the HCQ (Hardware Command Queue) API.
|
||||
|
||||
This class implements basic copy operations following the HCQ API, utilizing both types of `HWQueue`.
|
||||
"""
|
||||
|
||||
def __init__(self, dev:HCQDeviceType, batch_size:int=(2 << 20), batch_cnt:int=32, copy_bufs=None, max_copyout_size:int|None=None, **kwargs):
|
||||
super().__init__(dev, **kwargs)
|
||||
self.b = copy_bufs or [self._alloc(batch_size, BufferSpec(host=True)) for _ in range(batch_cnt)]
|
||||
self.b_timeline, self.b_next, self.max_copyout_size = [0] * len(self.b), 0, max_copyout_size
|
||||
|
||||
def map(self, buf:HCQBuffer):
|
||||
if self.dev in buf.mapped_devs: return
|
||||
if buf.owner is None: raise RuntimeError(f"map failed: buffer {buf.va_addr} has no owner, it's a virtual buffer")
|
||||
if not hasattr(self, '_map'): raise NotImplementedError("map failed: no method implemented")
|
||||
|
||||
# Since it's unified memory space, any buffer mapping is valid for all devices after successful map.
|
||||
# Devices can save mappings and internal metadata as a new buffer.
|
||||
if (mb:=self._map(buf)) is not None: buf.mappings[self.dev] = mb
|
||||
buf.mapped_devs.append(self.dev)
|
||||
|
||||
@suppress_finalizing
|
||||
def _free(self, buf:HCQBuffer, options:BufferSpec|None=None):
|
||||
for dev in buf.mapped_devs: dev.synchronize()
|
||||
for d, mb in buf.mappings.items(): d.allocator._unmap(mb)
|
||||
if hasattr(self, '_do_free'): self._do_free(buf, options)
|
||||
|
||||
def _unmap(self, mb): self.dev.iface.free(mb)
|
||||
|
||||
def _offset(self, buf, size:int, offset:int) -> HCQBuffer: return buf.offset(offset=offset, size=size)
|
||||
|
||||
class HCQAllocator(HCQAllocatorBase, Generic[HCQDeviceType]):
|
||||
def _copyin(self, dest:HCQBuffer, src:memoryview):
|
||||
if self.dev.hw_copy_queue_t is None:
|
||||
self.dev.synchronize()
|
||||
with cpu_profile(f'TINY -> {self.dev.device}', f"{self.dev.device}:COPY"): ctypes.memmove(int(dest.va_addr), from_mv(src), len(src))
|
||||
return
|
||||
|
||||
with hcq_profile(self.dev, queue_type=self.dev.hw_copy_queue_t, desc=TracingKey(f"TINY -> {self.dev.device}", ret=src.nbytes), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
for i in range(0, src.nbytes, self.b[0].size):
|
||||
self.b_next = (self.b_next + 1) % len(self.b)
|
||||
self.dev.timeline_signal.wait(self.b_timeline[self.b_next])
|
||||
|
||||
lsize = min(self.b[self.b_next].size, src.nbytes - i)
|
||||
self.b[self.b_next].cpu_view().view(size=lsize, fmt='B')[:] = src.cast('B')[i:i+lsize]
|
||||
self.dev.hw_copy_queue_t().wait(self.dev.timeline_signal, self.dev.timeline_value - 1) \
|
||||
.copy(dest.offset(i), self.b[self.b_next], lsize) \
|
||||
.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
self.b_timeline[self.b_next] = self.dev.timeline_value - 1
|
||||
|
||||
def copy_from_disk(self, dest:HCQBuffer, src, size):
|
||||
def _get_temp_buf():
|
||||
# Check if the next buffer is safe to be used (its signal has passed) and reserve it.
|
||||
if self.b_timeline[(self.b_next + 1) % len(self.b)] <= self.dev.timeline_signal.value:
|
||||
self.b_timeline[(self.b_next + 1) % len(self.b)], self.b_next = (1 << 64), (self.b_next + 1) % len(self.b)
|
||||
return (self.b[self.b_next].cpu_view(), self.b_next)
|
||||
return None
|
||||
|
||||
assert self.dev.hw_copy_queue_t is not None
|
||||
with hcq_profile(self.dev, queue_type=self.dev.hw_copy_queue_t, desc=TracingKey(f"DISK -> {self.dev.device}", ret=size), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
for (batch_info, dst_off, src_off, copy_size) in src.device.allocator._copyout_sharded(src, size, _get_temp_buf, seg_len=self.b[0].size,
|
||||
use_ioring=type(self.b[0].cpu_view()) is MMIOInterface):
|
||||
self.dev.hw_copy_queue_t().wait(self.dev.timeline_signal, self.dev.timeline_value - 1) \
|
||||
.copy(dest.offset(dst_off), self.b[batch_info[1]].offset(src_off), copy_size) \
|
||||
.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
self.b_timeline[batch_info[1]] = self.dev.timeline_value - 1
|
||||
|
||||
def _copyout(self, dest:memoryview, src:HCQBuffer):
|
||||
self.dev.synchronize()
|
||||
if self.dev.hw_copy_queue_t is None:
|
||||
with cpu_profile(f'{self.dev.device} -> TINY', f"{self.dev.device}:COPY"): ctypes.memmove(from_mv(dest), int(src.va_addr), len(dest))
|
||||
return
|
||||
|
||||
with hcq_profile(self.dev, queue_type=self.dev.hw_copy_queue_t, desc=TracingKey(f"{self.dev.device} -> TINY", ret=dest.nbytes), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
for i in range(0, dest.nbytes, cp_size:=(self.max_copyout_size or self.b[0].size)):
|
||||
self.dev.hw_copy_queue_t().wait(self.dev.timeline_signal, self.dev.timeline_value - 1) \
|
||||
.copy(self.b[0], src.offset(i), lsize:=min(cp_size, dest.nbytes-i)) \
|
||||
.signal(self.dev.timeline_signal, self.dev.next_timeline()).submit(self.dev)
|
||||
self.dev.timeline_signal.wait(self.dev.timeline_value - 1)
|
||||
dest.cast('B')[i:i+lsize] = self.b[0].cpu_view().view(size=lsize, fmt='B')[:]
|
||||
|
||||
def _transfer(self, dest:HCQBuffer, src:HCQBuffer, sz:int, src_dev:HCQDeviceType, dest_dev:HCQDeviceType):
|
||||
if src_dev.peer_group != dest_dev.peer_group: return src_dev.rdma_dev().allocator._transfer(dest, src, sz, src_dev, dest_dev)
|
||||
|
||||
cast(HCQAllocator, src_dev.allocator).map(dest)
|
||||
|
||||
assert src_dev.hw_copy_queue_t is not None
|
||||
with hcq_profile(src_dev, queue_type=src_dev.hw_copy_queue_t, desc=TracingKey(f"{src_dev.device} -> {dest_dev.device}", ret=sz), enabled=PROFILE,
|
||||
dev_suff="SDMA:0"):
|
||||
src_dev.hw_copy_queue_t().wait(src_dev.timeline_signal, src_dev.timeline_value - 1) \
|
||||
.wait(dest_dev.timeline_signal, dest_dev.timeline_value - 1) \
|
||||
.copy(dest, src, sz) \
|
||||
.signal(src_dev.timeline_signal, src_dev.next_timeline()).submit(src_dev)
|
||||
|
||||
if src_dev != dest_dev:
|
||||
unwrap(dest_dev.hw_compute_queue_t)().wait(src_dev.timeline_signal, src_dev.timeline_value - 1) \
|
||||
.wait(dest_dev.timeline_signal, dest_dev.timeline_value - 1) \
|
||||
.signal(dest_dev.timeline_signal, dest_dev.next_timeline()).submit(dest_dev)
|
||||
281
tinygrad_repo/tinygrad/runtime/support/memory.py
Normal file
281
tinygrad_repo/tinygrad/runtime/support/memory.py
Normal file
@@ -0,0 +1,281 @@
|
||||
import collections, functools, dataclasses, enum
|
||||
from typing import Any, ClassVar
|
||||
from tinygrad.helpers import round_up, getenv
|
||||
|
||||
class BumpAllocator:
|
||||
def __init__(self, size:int, base:int=0, wrap:bool=True): self.size, self.ptr, self.base, self.wrap = size, 0, base, wrap
|
||||
def alloc(self, size:int, alignment:int=1) -> int:
|
||||
if round_up(self.ptr, alignment) + size > self.size:
|
||||
if not self.wrap: raise RuntimeError("Out of memory")
|
||||
self.ptr = 0
|
||||
self.ptr = (res:=round_up(self.ptr, alignment)) + size
|
||||
return res + self.base
|
||||
|
||||
class TLSFAllocator:
|
||||
"""
|
||||
The allocator is based on the Two-Level Segregated Fit (TLSF) algorithm. The allocator maintains 2 level of buckets:
|
||||
* 1st level is determined by the most significant bit of the size.
|
||||
* 2nd level splits the covered memory of 1st level into @lv2_cnt entries.
|
||||
|
||||
For each allocation request, the allocator searches for the smallest block that can fit the requested size.
|
||||
For each deallocation request, the allocator merges the block with its neighbors if they are free.
|
||||
"""
|
||||
|
||||
def __init__(self, size:int, base:int=0, block_size:int=16, lv2_cnt:int=16):
|
||||
self.size, self.base, self.block_size, self.l2_cnt = size, base, block_size, lv2_cnt.bit_length()
|
||||
self.storage:list = [collections.defaultdict(list) for _ in range(size.bit_length() + 1)]
|
||||
self.lv1_entries:list[int] = [0] * len(self.storage)
|
||||
|
||||
# self.blocks is more like a linked list, where each entry is a contiguous block.
|
||||
self.blocks:dict[int, tuple[int, int|None, int|None, bool]] = {0: (size, None, None, True)} # size, next, prev, is_free
|
||||
if size > 0: self._insert_block(0, size)
|
||||
|
||||
@functools.cache # pylint: disable=method-cache-max-size-none
|
||||
def lv1(self, size): return size.bit_length()
|
||||
|
||||
@functools.cache # pylint: disable=method-cache-max-size-none
|
||||
def lv2(self, size): return (size - (1 << (size.bit_length() - 1))) // (1 << max(0, size.bit_length() - self.l2_cnt))
|
||||
|
||||
def _insert_block(self, start:int, size:int, prev:int|None=None):
|
||||
if prev is None: prev = self.blocks[start][2]
|
||||
self.storage[self.lv1(size)][self.lv2(size)].append(start)
|
||||
self.lv1_entries[self.lv1(size)] += 1
|
||||
self.blocks[start] = (size, start + size, prev, True)
|
||||
return self
|
||||
|
||||
def _remove_block(self, start:int, size:int, prev:int|None=None):
|
||||
if prev is None: prev = self.blocks[start][2]
|
||||
self.storage[self.lv1(size)][self.lv2(size)].remove(start)
|
||||
self.lv1_entries[self.lv1(size)] -= 1
|
||||
self.blocks[start] = (size, start + size, prev, False)
|
||||
return self
|
||||
|
||||
def _split_block(self, start:int, size:int, new_size:int):
|
||||
nxt = self.blocks[start][1]
|
||||
assert self.blocks[start][3], "block must be free"
|
||||
self._remove_block(start, size)._insert_block(start, new_size)._insert_block(start + new_size, size - new_size, prev=start)
|
||||
if nxt in self.blocks: self.blocks[nxt] = (self.blocks[nxt][0], self.blocks[nxt][1], start + new_size, self.blocks[nxt][3])
|
||||
return self
|
||||
|
||||
def _merge_right(self, start:int):
|
||||
size, nxt, _, is_free = self.blocks[start]
|
||||
assert is_free, "block must be free"
|
||||
|
||||
while is_free and nxt in self.blocks:
|
||||
if (blk:=self.blocks[nxt])[3] is False: break
|
||||
self._remove_block(start, size)._remove_block(nxt, blk[0])._insert_block(start, size:=size + blk[0])
|
||||
assert self.blocks[start][1] == blk[1]
|
||||
_, nxt, _, _ = self.blocks.pop(nxt)
|
||||
|
||||
if nxt in self.blocks: self.blocks[nxt] = (self.blocks[nxt][0], self.blocks[nxt][1], start, self.blocks[nxt][3])
|
||||
|
||||
def _merge_block(self, start:int):
|
||||
# Go left while blocks are free. Then merge all them right.
|
||||
while (x:=self.blocks[start][2]) is not None and self.blocks[x][3] is True: start = x
|
||||
self._merge_right(start)
|
||||
|
||||
def alloc(self, req_size:int, align:int=1) -> int:
|
||||
req_size = max(self.block_size, req_size) # at least block size.
|
||||
size = max(self.block_size, req_size + align - 1)
|
||||
|
||||
# Round up the allocation size to the next bucket, so any entry there can fit the requested size.
|
||||
size = round_up(size, (1 << size.bit_length() - self.l2_cnt))
|
||||
|
||||
# Search for the smallest block that can fit the requested size. Start with its bucket and go up until any block is found.
|
||||
for l1 in range(self.lv1(size), len(self.storage)):
|
||||
if self.lv1_entries[l1] == 0: continue
|
||||
for l2 in range(self.lv2(size) if l1 == size.bit_length() else 0, (1 << self.l2_cnt)):
|
||||
if len(self.storage[l1][l2]) > 0:
|
||||
# Block start address.
|
||||
start = self.storage[l1][l2][0]
|
||||
nsize = self.blocks[start][0]
|
||||
assert nsize >= size, "block must be larger"
|
||||
|
||||
# If request contains alignment, split the block into two parts.
|
||||
if (new_start:=round_up(start, align)) != start:
|
||||
self._split_block(start, nsize, new_start - start)
|
||||
start, nsize = new_start, self.blocks[new_start][0]
|
||||
|
||||
# If the block is larger than the requested size, split it into two parts.
|
||||
if nsize > req_size: self._split_block(start, nsize, req_size)
|
||||
self._remove_block(start, req_size) # Mark the block as allocated.
|
||||
return start + self.base
|
||||
raise MemoryError(f"Can't allocate {req_size} bytes")
|
||||
|
||||
def free(self, start:int):
|
||||
self._insert_block(start - self.base, self.blocks[start - self.base][0])._merge_block(start - self.base)
|
||||
|
||||
# Memory Management
|
||||
|
||||
class AddrSpace(enum.Enum): PHYS = enum.auto(); SYS = enum.auto(); PEER = enum.auto() # noqa: E702
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class VirtMapping: va_addr:int; size:int; paddrs:list[tuple[int, int]]; aspace:AddrSpace; uncached:bool=False; snooped:bool=False # noqa: E702
|
||||
|
||||
class PageTableTraverseContext:
|
||||
def __init__(self, dev, pt, vaddr, create_pts=False, free_pts=False, inspect=False, boot=False):
|
||||
self.dev, self.vaddr, self.create_pts, self.free_pts, self.inspect, self.boot = dev, vaddr - dev.mm.va_base, create_pts, free_pts, inspect, boot
|
||||
self.pt_stack:list[tuple[Any, int, int]] = [(pt, self._pt_pte_idx(pt, self.vaddr), self._pt_pte_size(pt))]
|
||||
|
||||
def _pt_pte_cnt(self, lv): return self.dev.mm.pte_cnt[lv]
|
||||
def _pt_pte_size(self, pt): return self.dev.mm.pte_covers[pt.lv]
|
||||
def _pt_pte_idx(self, pt, va): return (va // self._pt_pte_size(pt)) % self._pt_pte_cnt(pt.lv)
|
||||
|
||||
def level_down(self):
|
||||
pt, pte_idx, _ = self.pt_stack[-1]
|
||||
|
||||
if not pt.valid(pte_idx):
|
||||
assert self.create_pts, "Not allowed to create new page table"
|
||||
pt.set_entry(pte_idx, self.dev.mm.palloc(0x1000, zero=True, boot=self.boot, ptable=True), table=True, valid=True)
|
||||
|
||||
assert not pt.is_page(pte_idx), f"Must be table pt={pt.paddr:#x}, {pt.lv=} {pte_idx=} {pt.entry(pte_idx)=:#x}"
|
||||
child_page_table = self.dev.mm.pt_t(self.dev, pt.address(pte_idx), lv=pt.lv+1)
|
||||
|
||||
self.pt_stack.append((child_page_table, self._pt_pte_idx(child_page_table, self.vaddr), self._pt_pte_size(child_page_table)))
|
||||
return self.pt_stack[-1]
|
||||
|
||||
def _try_free_pt(self) -> bool:
|
||||
pt, _, _ = self.pt_stack[-1]
|
||||
if self.free_pts and pt != self.dev.mm.root_page_table and all(not pt.valid(i) for i in range(self._pt_pte_cnt(self.pt_stack[-1][0].lv))):
|
||||
self.dev.mm.pfree(pt.paddr, ptable=True)
|
||||
parent_pt, parent_pte_idx, _ = self.pt_stack[-2]
|
||||
parent_pt.set_entry(parent_pte_idx, 0x0, valid=False)
|
||||
return True
|
||||
return False
|
||||
|
||||
def level_up(self):
|
||||
while self._try_free_pt() or self.pt_stack[-1][1] == self._pt_pte_cnt(self.pt_stack[-1][0].lv):
|
||||
pt, pt_cnt, _ = self.pt_stack.pop()
|
||||
if pt_cnt == self._pt_pte_cnt(pt.lv): self.pt_stack[-1] = (self.pt_stack[-1][0], self.pt_stack[-1][1] + 1, self.pt_stack[-1][2])
|
||||
|
||||
def next(self, size:int, paddr:int|None=None, off:int=0):
|
||||
while size > 0:
|
||||
pt, pte_idx, pte_covers = self.pt_stack[-1]
|
||||
|
||||
# create_pts goes down until the page covers the request.
|
||||
# free_pts goes down to the table, it assumses all entries are valid on the range (and validates that)
|
||||
# inspect just visits any valid ranges and yields them.
|
||||
if self.create_pts:
|
||||
assert paddr is not None, "paddr must be provided when allocating new page tables"
|
||||
while pte_covers > size or not pt.supports_huge_page(paddr+off) or self.vaddr&(pte_covers-1) != 0: pt, pte_idx, pte_covers = self.level_down()
|
||||
else:
|
||||
while not pt.is_page(pte_idx) and (self.free_pts or pt.valid(pte_idx)): pt, pte_idx, pte_covers = self.level_down()
|
||||
|
||||
entries = max(min(size // pte_covers, self._pt_pte_cnt(pt.lv) - pte_idx), 1 if self.inspect else 0)
|
||||
assert entries > 0, f"Invalid entries {size=:#x}, {pte_covers=:#x}"
|
||||
yield off, pt, pte_idx, entries, pte_covers
|
||||
|
||||
size, off, self.vaddr = size - entries * pte_covers, off + entries * pte_covers, self.vaddr + entries * pte_covers
|
||||
self.pt_stack[-1] = (pt, pte_idx + entries, pte_covers)
|
||||
self.level_up()
|
||||
|
||||
class MemoryManager:
|
||||
va_allocator: ClassVar[TLSFAllocator|None] = None
|
||||
|
||||
def __init__(self, dev, vram_size:int, boot_size:int, pt_t, va_bits:int, va_shifts:list[int], va_base:int,
|
||||
palloc_ranges:list[tuple[int, int]], first_lv:int=0, reserve_ptable=False):
|
||||
self.dev, self.vram_size, self.va_shifts, self.va_base, lvl_msb = dev, vram_size, va_shifts, va_base, va_shifts + [va_bits + 1]
|
||||
self.pte_covers, self.pte_cnt = [1 << x for x in va_shifts][::-1], [1 << (lvl_msb[i+1] - lvl_msb[i]) for i in range(len(lvl_msb) - 1)][::-1]
|
||||
self.pt_t, self.palloc_ranges, self.level_cnt, self.va_bits, self.reserve_ptable = pt_t, palloc_ranges, len(va_shifts), va_bits, reserve_ptable
|
||||
|
||||
self.boot_allocator = TLSFAllocator(boot_size, base=0)
|
||||
self.ptable_allocator = TLSFAllocator(round_up(vram_size // 512, 1 << 20) if self.reserve_ptable else 0, base=self.boot_allocator.size)
|
||||
self.pa_allocator = TLSFAllocator(vram_size - (off_sz:=self.boot_allocator.size + self.ptable_allocator.size), base=off_sz)
|
||||
self.root_page_table = pt_t(self.dev, self.palloc(0x1000, zero=not self.dev.smi_dev, boot=True), lv=first_lv)
|
||||
|
||||
def _frag_size(self, va, sz, must_cover=True):
|
||||
"""
|
||||
Calculate the tlb fragment size for a given virtual address and size.
|
||||
If must_cover is True, the fragment size must cover the size, otherwise the biggest fragment size that fits the size is returned.
|
||||
Fragment 0 is 4KB, 1 is 8KB and so on.
|
||||
"""
|
||||
va_pwr2_div, sz_pwr2_div, sz_pwr2_max = va & -(va) if va > 0 else (1 << 63), sz & -(sz), (1 << (sz.bit_length() - 1))
|
||||
return (min(va_pwr2_div, sz_pwr2_div) if must_cover else min(va_pwr2_div, sz_pwr2_max)).bit_length() - 1 - 12
|
||||
|
||||
def page_tables(self, vaddr:int, size:int):
|
||||
ctx = PageTableTraverseContext(self.dev, self.root_page_table, vaddr, create_pts=True)
|
||||
for _ in ctx.next(size, paddr=0): return [pt for pt, _, _ in ctx.pt_stack]
|
||||
|
||||
def map_range(self, vaddr:int, size:int, paddrs:list[tuple[int, int]], aspace:AddrSpace, uncached=False, snooped=False, boot=False) -> VirtMapping:
|
||||
if getenv("MM_DEBUG", 0): print(f"mm {self.dev.devfmt}: mapping {vaddr=:#x} ({size=:#x})")
|
||||
|
||||
assert size == sum(p[1] for p in paddrs), f"Size mismatch {size=} {sum(p[1] for p in paddrs)=}"
|
||||
|
||||
ctx = PageTableTraverseContext(self.dev, self.root_page_table, vaddr, boot=boot, inspect=True)
|
||||
for _, pt, pte_idx, pte_cnt, _ in ctx.next(size):
|
||||
for pte_off in range(pte_cnt): assert not pt.valid(pte_idx + pte_off), f"PTE already mapped: {pt.entry(pte_idx + pte_off):#x}"
|
||||
|
||||
ctx = PageTableTraverseContext(self.dev, self.root_page_table, vaddr, create_pts=True, boot=boot)
|
||||
for paddr, psize in paddrs:
|
||||
for off, pt, pte_idx, pte_cnt, pte_covers in ctx.next(psize, paddr=paddr):
|
||||
for pte_off in range(pte_cnt):
|
||||
pt.set_entry(pte_idx + pte_off, paddr + off + pte_off * pte_covers, uncached=uncached, aspace=aspace, snooped=snooped,
|
||||
frag=self._frag_size(ctx.vaddr+off, pte_cnt * pte_covers), valid=True)
|
||||
|
||||
self.on_range_mapped()
|
||||
return VirtMapping(vaddr, size, paddrs, aspace=aspace, uncached=uncached, snooped=snooped)
|
||||
|
||||
def unmap_range(self, vaddr:int, size:int):
|
||||
if getenv("MM_DEBUG", 0): print(f"mm {self.dev.devfmt}: unmapping {vaddr=:#x} ({size=:#x})")
|
||||
|
||||
ctx = PageTableTraverseContext(self.dev, self.root_page_table, vaddr, free_pts=True)
|
||||
for _, pt, pte_idx, pte_cnt, _ in ctx.next(size):
|
||||
for pte_id in range(pte_idx, pte_idx + pte_cnt):
|
||||
assert pt.valid(pte_id), f"PTE not mapped: {pt.entry(pte_id):#x}"
|
||||
pt.set_entry(pte_id, paddr=0x0, valid=False)
|
||||
|
||||
def on_range_mapped(self): pass
|
||||
|
||||
@classmethod
|
||||
def alloc_vaddr(cls, size:int, align=0x1000) -> int:
|
||||
assert cls.va_allocator is not None, "must be set"
|
||||
return cls.va_allocator.alloc(size, max((1 << (size.bit_length() - 1)), align))
|
||||
|
||||
@functools.cache # pylint: disable=method-cache-max-size-none
|
||||
def identity_va(self, uncached:bool) -> int:
|
||||
self.map_range(va:=self.alloc_vaddr(self.vram_size, self.vram_size), self.vram_size, [(0, self.vram_size)], AddrSpace.PHYS, uncached=uncached)
|
||||
return va
|
||||
|
||||
def valloc(self, size:int, align=0x1000, uncached=False, contiguous=False) -> VirtMapping:
|
||||
if not getenv("GMMU", 1):
|
||||
paddr = self.palloc(size:=round_up(size, 0x1000), align, zero=False)
|
||||
return VirtMapping(self.identity_va(uncached) + paddr, size, [(paddr, size)], aspace=AddrSpace.PHYS, uncached=uncached)
|
||||
|
||||
# Alloc physical memory and map it to the virtual address
|
||||
va = self.alloc_vaddr(size:=round_up(size, 0x1000), align)
|
||||
|
||||
if contiguous: paddrs = [(self.palloc(size, zero=True), size)]
|
||||
else:
|
||||
# Traverse the PT to find the largest contiguous sizes we need to allocate. Try to allocate the longest segment to reduce TLB pressure.
|
||||
nxt_range, rem_size, paddrs = 0, size, []
|
||||
while rem_size > 0:
|
||||
while self.palloc_ranges[nxt_range][0] > rem_size: nxt_range += 1
|
||||
|
||||
try: paddrs += [(self.palloc(try_sz:=self.palloc_ranges[nxt_range][0], self.palloc_ranges[nxt_range][1], zero=False), try_sz)]
|
||||
except MemoryError:
|
||||
# Move to a smaller size and try again.
|
||||
nxt_range += 1
|
||||
if nxt_range == len(self.palloc_ranges):
|
||||
for paddr, _ in paddrs: self.pfree(paddr)
|
||||
raise MemoryError(f"Failed to allocate memory (OOM). Request size={size:#x} ({self.palloc_ranges[nxt_range-1]})")
|
||||
continue
|
||||
rem_size -= self.palloc_ranges[nxt_range][0]
|
||||
|
||||
return self.map_range(va, size, paddrs, aspace=AddrSpace.PHYS, uncached=uncached)
|
||||
|
||||
def vfree(self, vm:VirtMapping):
|
||||
if not getenv("GMMU", 1): return self.pfree(vm.paddrs[0][0])
|
||||
|
||||
assert self.va_allocator is not None, "must be set"
|
||||
self.unmap_range(vm.va_addr, vm.size)
|
||||
self.va_allocator.free(vm.va_addr)
|
||||
for paddr, _ in vm.paddrs: self.pfree(paddr)
|
||||
|
||||
def palloc(self, size:int, align:int=0x1000, zero=True, boot=False, ptable=False) -> int:
|
||||
assert self.dev.is_booting == boot, "During booting, only boot memory can be allocated"
|
||||
allocator = self.boot_allocator if boot else (self.ptable_allocator if self.reserve_ptable and ptable else self.pa_allocator)
|
||||
paddr = allocator.alloc(round_up(size, 0x1000), align)
|
||||
if zero: self.dev.vram[paddr:paddr+size] = bytes(size)
|
||||
return paddr
|
||||
|
||||
def pfree(self, paddr:int, ptable=False): (self.ptable_allocator if self.reserve_ptable and ptable else self.pa_allocator).free(paddr)
|
||||
248
tinygrad_repo/tinygrad/runtime/support/mlx/mlxdev.py
Normal file
248
tinygrad_repo/tinygrad/runtime/support/mlx/mlxdev.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from __future__ import annotations
|
||||
import struct, random, socket, ctypes, functools, itertools
|
||||
from tinygrad.helpers import getenv, wait_cond, round_up, next_power2, ceildiv, DEBUG, hi32, lo32, to_be32, to_be64
|
||||
from tinygrad.runtime.support.memory import BumpAllocator
|
||||
from tinygrad.runtime.support.system import PCIDevice
|
||||
from tinygrad.runtime.autogen import mlx5, pci
|
||||
|
||||
MLX_DEBUG = getenv("MLX_DEBUG", 0)
|
||||
|
||||
MLX5_CMD_STRUCTS = {v: (getattr(mlx5, f"struct_mlx5_ifc_{n[12:].lower()}_in_bits", None),
|
||||
getattr(mlx5, f"struct_mlx5_ifc_{n[12:].lower()}_out_bits", None)) for n, v in mlx5.__dict__.items() if n.startswith("MLX5_CMD_OP_")}
|
||||
MLX5_CMD_STRUCTS[mlx5.MLX5_CMD_OP_ACCESS_REG] = (mlx5.struct_mlx5_ifc_access_register_in_bits, mlx5.struct_mlx5_ifc_access_register_out_bits)
|
||||
|
||||
def ipv4_to_gid(ip): return bytes(10) + b'\xff\xff' + socket.inet_aton(ip)
|
||||
|
||||
def udp_sport(lqpn, rqpn):
|
||||
v = (lqpn * rqpn ^ ((lqpn * rqpn) >> 20) ^ ((lqpn * rqpn) >> 40)) & 0xFFFFF
|
||||
return ((v & 0x3FFF) ^ ((v & 0xFC000) >> 14)) | 0xC000
|
||||
|
||||
def ifc_get(buf, bit_off, width):
|
||||
byte_off, bit_in, n = bit_off // 8, bit_off % 8, (bit_off % 8 + width + 7) // 8
|
||||
return (int.from_bytes(buf[byte_off:byte_off + n], 'big') >> (n * 8 - bit_in - width)) & ((1 << width) - 1)
|
||||
|
||||
def ifc_set(buf, bit_off, width, value):
|
||||
byte_off, bit_in, n = bit_off // 8, bit_off % 8, (bit_off % 8 + width + 7) // 8
|
||||
shift, val = n * 8 - bit_in - width, int.from_bytes(buf[byte_off:byte_off + n], 'big')
|
||||
buf[byte_off:byte_off + n] = ((val & ~(((1 << width) - 1) << shift)) | ((value & ((1 << width) - 1)) << shift)).to_bytes(n, 'big')
|
||||
|
||||
@functools.cache
|
||||
def ifc_fields(ifc_struct): return {name: (off, ctypes.sizeof(typ)) for name, typ, off in ifc_struct._real_fields_ if not name.startswith('reserved')}
|
||||
|
||||
def ifc_subfield(ifc_struct, field_name):
|
||||
for name, typ, off in ifc_struct._real_fields_:
|
||||
if name == field_name: return typ, ifc_fields(ifc_struct)[field_name][0]
|
||||
raise KeyError(f"no field '{field_name}' in {ifc_struct}")
|
||||
|
||||
def fill_ifc(buf, ifc_struct, base=0, **kw):
|
||||
fields = ifc_fields(ifc_struct)
|
||||
for name, val in kw.items():
|
||||
if isinstance(val, dict):
|
||||
sub_struct, sub_off = ifc_subfield(ifc_struct, name)
|
||||
fill_ifc(buf, sub_struct, base=base + sub_off, **val)
|
||||
else: ifc_set(buf, base + fields[name][0], fields[name][1], val)
|
||||
|
||||
def ifc_decode(buf, ifc_struct, base=0):return {name: ifc_get(buf, base + off, width) for name, (off, width) in ifc_fields(ifc_struct).items()}
|
||||
|
||||
class MLXCmdQueue:
|
||||
def __init__(self, dev):
|
||||
self.dev, self._tok = dev, itertools.count(1)
|
||||
|
||||
cmd_l = dev.iseg_r('cmdq_addr_l_sz') & 0xFF
|
||||
self.log_stride, self.max_reg_cmds = cmd_l & 0xF, (1 << ((cmd_l >> 4) & 0xF)) - 1
|
||||
|
||||
stride = next_power2(ctypes.sizeof(mlx5.struct_mlx5_cmd_prot_block))
|
||||
self.queue, self.queue_paddrs = dev.pci_dev.alloc_sysmem(0x1000 + 1024 * stride)
|
||||
self.mboxes = [(off:=0x1000 + i * stride, self.queue_paddrs[1 + (i * stride) // 0x1000] + (off % 0x1000)) for i in range(1024)]
|
||||
|
||||
dev.iseg_w('cmdq_addr_h', hi32(self.queue_paddrs[0]))
|
||||
dev.iseg_w('cmdq_addr_l_sz', lo32(self.queue_paddrs[0]) | cmd_l)
|
||||
|
||||
def create_mbox_chain(self, base, tok, data):
|
||||
n = ceildiv(len(data), chunk_sz:=mlx5.MLX5_CMD_DATA_BLOCK_SIZE)
|
||||
for i in range(n):
|
||||
off, _ = self.mboxes[base + i]
|
||||
blk = mlx5.struct_mlx5_cmd_prot_block(data=(ctypes.c_ubyte*chunk_sz).from_buffer_copy(data[i*chunk_sz:(i+1)*chunk_sz].ljust(chunk_sz, b'\x00')),
|
||||
next=to_be64(self.mboxes[base+i+1][1]) if i < n-1 else 0, block_num=to_be32(i), token=tok)
|
||||
self.queue[off:off + ctypes.sizeof(mlx5.struct_mlx5_cmd_prot_block)] = bytes(blk)
|
||||
return (self.mboxes[base][0], self.mboxes[base][1], n)
|
||||
|
||||
def exec(self, opcode, op_mod=0, payload=b'', raw=False, **kw):
|
||||
in_struct, out_struct = MLX5_CMD_STRUCTS[opcode]
|
||||
out_sz = max(0, ctypes.sizeof(out_struct) - 16) if out_struct else 0
|
||||
tok, slot = (next(self._tok) % 255) + 1, self.max_reg_cmds if opcode == mlx5.MLX5_CMD_OP_MANAGE_PAGES else 0
|
||||
|
||||
# serialize input
|
||||
inp_sz = max(16, ceildiv(max((off + w for off, w in ifc_fields(in_struct).values()), default=0), 8))
|
||||
fill_ifc(inp:=bytearray(inp_sz + len(payload)), in_struct, opcode=opcode, op_mod=op_mod, **kw)
|
||||
if payload: inp[inp_sz:] = payload
|
||||
|
||||
# prepare mailboxes and build command layout
|
||||
_, in_ptr, n_in = self.create_mbox_chain(0, tok, inp[16:])
|
||||
_, out_ptr, n_out = self.create_mbox_chain(n_in, tok, bytes(out_sz))
|
||||
cmd = mlx5.struct_mlx5_cmd_layout(type=mlx5.MLX5_PCI_CMD_XPORT, inlen=to_be32(len(inp)), in_ptr=to_be64(in_ptr),
|
||||
_in=(ctypes.c_uint32*4)(*(int.from_bytes(inp[i:i+4], 'little') for i in range(0, 16, 4))),
|
||||
out_ptr=to_be64(out_ptr), outlen=to_be32(16 + out_sz), token=tok, status_own=mlx5.CMD_OWNER_HW)
|
||||
cmd_bytes = bytearray(bytes(cmd))
|
||||
cmd_bytes[mlx5.struct_mlx5_cmd_layout.sig.offset] = (~functools.reduce(lambda a, b: a ^ b, cmd_bytes)) & 0xFF # type: ignore[attr-defined]
|
||||
|
||||
# submit and wait for completion
|
||||
slot_view = self.queue.view(slot << self.log_stride, len(cmd_bytes))
|
||||
slot_view[:] = cmd_bytes
|
||||
self.dev.iseg_w('cmd_dbell', 1 << slot)
|
||||
wait_cond(lambda: slot_view[mlx5.struct_mlx5_cmd_layout.status_own.offset] & mlx5.CMD_OWNER_HW, value=0, # type: ignore[attr-defined]
|
||||
msg=f"cmd 0x{opcode:04x}")
|
||||
|
||||
# check status and read output
|
||||
assert slot_view[mlx5.struct_mlx5_cmd_layout.status_own.offset] >> 1 == 0, f"cmd 0x{opcode:04x} delivery error" # type: ignore[attr-defined]
|
||||
|
||||
out_view = slot_view.view(mlx5.struct_mlx5_cmd_layout.out.offset, 16 + out_sz) # type: ignore[attr-defined]
|
||||
status, syndrome = struct.unpack('>I', out_view[0:4])[0] >> 24, struct.unpack('>I', out_view[4:8])[0]
|
||||
assert status == 0, f"cmd 0x{opcode:04x} failed status=0x{status:x} syn=0x{syndrome:08x}"
|
||||
|
||||
ret = bytearray(out_view[:16])
|
||||
ret += b''.join(bytes(self.queue[va:va + mlx5.MLX5_CMD_DATA_BLOCK_SIZE]) for va, _ in self.mboxes[n_in:n_in+n_out])[:out_sz]
|
||||
return ret if raw else (ifc_decode(ret, out_struct) if out_struct else ret)
|
||||
|
||||
class MLXDev:
|
||||
def __init__(self, pci_dev:PCIDevice, ip:str=getenv("MLX_IP", "10.0.0.1")):
|
||||
self.pci_dev, self.devfmt, self.bar = pci_dev, pci_dev.pcibus, pci_dev.map_bar(0, fmt='I')
|
||||
|
||||
fw_rev, cmdif_sub = self.iseg_r('fw_rev'), self.iseg_r('cmdif_rev_fw_sub')
|
||||
if DEBUG >= 2: print(f"mlx5 {self.devfmt}: firmware {fw_rev >> 16}.{fw_rev & 0xFFFF}.{cmdif_sub & 0xFFFF}")
|
||||
assert (cmdif_sub >> 16) == 5, f"unsupported mlx version: {cmdif_sub >> 16}"
|
||||
|
||||
self.init_hw(ip)
|
||||
|
||||
def rreg(self, off): return to_be32(self.bar[off // 4])
|
||||
def wreg(self, off, val): self.bar[off // 4] = to_be32(val)
|
||||
def iseg_r(self, field): return self.rreg(getattr(mlx5.struct_mlx5_init_seg, field).offset)
|
||||
def iseg_w(self, field, val): self.wreg(getattr(mlx5.struct_mlx5_init_seg, field).offset, val)
|
||||
|
||||
def init_hw(self, ip):
|
||||
wait_cond(lambda: self.iseg_r('initializing') & 0x80000000, value=0, msg="FW init timeout")
|
||||
self.pci_dev.write_config(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
self.cmd = MLXCmdQueue(self)
|
||||
wait_cond(lambda: self.iseg_r('initializing') & 0x80000000, value=0, msg="FW init timeout")
|
||||
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_ENABLE_HCA)
|
||||
if self.cmd.exec(mlx5.MLX5_CMD_OP_QUERY_ISSI)['supported_issi_dw0'] & 2:
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_SET_ISSI, current_issi=1)
|
||||
|
||||
self.provide_pages(mlx5.MLX5_BOOT_PAGES)
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_ACCESS_REG, register_id=mlx5.MLX5_REG_HOST_ENDIANNESS, payload=bytearray(16))
|
||||
|
||||
self.init_hca()
|
||||
|
||||
self.uar = self.cmd.exec(mlx5.MLX5_CMD_OP_ALLOC_UAR)['uar']
|
||||
self.uar_view = self.pci_dev.map_bar(0, off=self.uar * 0x1000, size=0x1000, fmt='Q')
|
||||
|
||||
vport = self.cmd.exec(mlx5.MLX5_CMD_OP_QUERY_NIC_VPORT_CONTEXT, raw=True)
|
||||
nvc_struct, nvc_off = ifc_subfield(mlx5.struct_mlx5_ifc_query_nic_vport_context_out_bits, 'nic_vport_context')
|
||||
mac_struct, mac_off = ifc_subfield(nvc_struct, 'permanent_address')
|
||||
self.mac = ifc_get(vport, nvc_off + mac_off + 16, 48)
|
||||
|
||||
# enable roce
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_MODIFY_NIC_VPORT_CONTEXT, field_select=dict(roce_en=1), nic_vport_context=dict(roce_en=1))
|
||||
|
||||
dbr_mem, self.dbr_paddrs = self.pci_dev.alloc_sysmem(0x1000)
|
||||
self.dbr = dbr_mem.view(fmt='I')
|
||||
self.dbr_alloc = BumpAllocator(0x1000, wrap=False)
|
||||
|
||||
self.pd = self.cmd.exec(mlx5.MLX5_CMD_OP_ALLOC_PD)['pd']
|
||||
res = self.cmd.exec(mlx5.MLX5_CMD_OP_CREATE_MKEY, memory_key_mkey_entry=dict(access_mode_1_0=0, rw=1, rr=1, lw=1, lr=1, qpn=0xFFFFFF,
|
||||
mkey_7_0=(key_lo:=0x22), length64=1, pd=self.pd))
|
||||
self.mkey = (res['mkey_index'] << 8) | key_lo
|
||||
|
||||
self.local_gid = ipv4_to_gid(ip)
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_SET_ROCE_ADDRESS, roce_address=dict(roce_version=2, source_l3_address=int.from_bytes(self.local_gid, 'big'),
|
||||
roce_l3_type=0, source_mac_47_32=hi32(self.mac), source_mac_31_0=lo32(self.mac)), roce_address_index=0, vhca_port_num=1)
|
||||
|
||||
if DEBUG >= 2: print(f"mlx5 {self.devfmt}: booted mac={self.mac.to_bytes(6,'big').hex(':')} mkey=0x{self.mkey:x}")
|
||||
|
||||
def register_mem(self, paddrs:list[int], size:int, log_page_size:int=12) -> int:
|
||||
n = len(paddrs)
|
||||
mtt = struct.pack(f'>{round_up(n, 2)}Q', *paddrs, *([0] * (round_up(n, 2) - n)))
|
||||
if MLX_DEBUG >= 1: print(f"mlx5 {self.devfmt}: register_mem pages={n} page_sz={1 << log_page_size} mtt_bytes={len(mtt)}")
|
||||
self.provide_pages(mlx5.MLX5_INIT_PAGES)
|
||||
res = self.cmd.exec(mlx5.MLX5_CMD_OP_CREATE_MKEY, translations_octword_actual_size=ceildiv(n, 2), payload=mtt,
|
||||
memory_key_mkey_entry=dict(access_mode_1_0=1, lr=1, lw=1, rr=1, rw=1, pd=self.pd, qpn=0xFFFFFF, mkey_7_0=(key_lo:=0x33),
|
||||
start_addr=paddrs[0], len=size, log_page_size=log_page_size, translations_octword_size=ceildiv(n, 2)))
|
||||
return (res['mkey_index'] << 8) | key_lo
|
||||
|
||||
def unregister_mem(self, mkey:int): self.cmd.exec(mlx5.MLX5_CMD_OP_DESTROY_MKEY, mkey_index=mkey >> 8)
|
||||
|
||||
def provide_pages(self, mode):
|
||||
if (npages:=self.cmd.exec(mlx5.MLX5_CMD_OP_QUERY_PAGES, op_mod=mode)['num_pages']) <= 0: return
|
||||
if MLX_DEBUG >= 1: print(f"mlx5 {self.devfmt}: provide_pages mode={mode}, {npages} pages")
|
||||
mem, paddrs = self.pci_dev.alloc_sysmem(npages * 0x1000)
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_MANAGE_PAGES, op_mod=mlx5.MLX5_PAGES_GIVE, input_num_entries=npages, payload=struct.pack(f'>{npages}Q', *paddrs))
|
||||
|
||||
def hca_query_cap(self, cap_type, cap_struct, mode):
|
||||
raw = bytearray(self.cmd.exec(mlx5.MLX5_CMD_OP_QUERY_HCA_CAP, op_mod=(cap_type << 1) | mode, raw=True)[16:16+4096])
|
||||
return raw, ifc_decode(raw, cap_struct)
|
||||
|
||||
def hca_set_cap(self, cap_type, cap_struct, raw, **kwargs):
|
||||
fill_ifc(cap:=bytearray(raw), cap_struct, **kwargs)
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_SET_HCA_CAP, op_mod=cap_type << 1, capability=int.from_bytes(cap[:4096].ljust(4096, b'\x00'), 'big'))
|
||||
|
||||
def init_hca(self):
|
||||
gen_caps, gen_cur = self.hca_query_cap(mlx5.MLX5_CAP_GENERAL, mlx5.struct_mlx5_ifc_cmd_hca_cap_bits, mode=1)
|
||||
self.hca_set_cap(mlx5.MLX5_CAP_GENERAL, mlx5.struct_mlx5_ifc_cmd_hca_cap_bits, gen_caps,
|
||||
pkey_table_size=0, cmdif_checksum=0, log_uar_page_sz=0, log_max_qp=18, roce=1)
|
||||
|
||||
roce_cur_raw, roce_cur = self.hca_query_cap(mlx5.MLX5_CAP_ROCE, mlx5.struct_mlx5_ifc_roce_cap_bits, mode=1)
|
||||
self.hca_set_cap(mlx5.MLX5_CAP_ROCE, mlx5.struct_mlx5_ifc_roce_cap_bits, roce_cur_raw, sw_r_roce_src_udp_port=1)
|
||||
|
||||
self.provide_pages(mlx5.MLX5_INIT_PAGES)
|
||||
self.cmd.exec(mlx5.MLX5_CMD_OP_INIT_HCA, sw_owner_id=random.getrandbits(128))
|
||||
|
||||
_, self.caps = self.hca_query_cap(mlx5.MLX5_CAP_GENERAL, mlx5.struct_mlx5_ifc_cmd_hca_cap_bits, 1)
|
||||
|
||||
if MLX_DEBUG >= 4: print(f"mlx5 {self.devfmt}: HCA initialized with gen_caps={gen_cur} roce_caps={roce_cur}")
|
||||
|
||||
class MLXQP:
|
||||
def __init__(self, dev:MLXDev, log_sq_size=4, log_rq_size=4, log_eq_size=7, log_cq_size=7):
|
||||
self.dev, self.cq_size, self.log_sq_size, self.log_rq_size, self.head = dev, 1 << log_cq_size, log_sq_size, log_rq_size, 0
|
||||
|
||||
self.cq_dbr, self.qp_dbr = dev.dbr_alloc.alloc(8, alignment=8), dev.dbr_alloc.alloc(8, alignment=8)
|
||||
|
||||
# create EQ, CQ
|
||||
self.eq_mem, self.eq_paddrs, self.eq_info = self.create_queue(mlx5.MLX5_CMD_OP_CREATE_EQ, log_eq_size, entry_sz=64, owner_off=31,
|
||||
eq_context_entry=dict(log_eq_size=log_eq_size, uar_page=dev.uar, log_page_size=0))
|
||||
|
||||
self.cq_mem, self.cq_paddrs, self.cq_info = self.create_queue(mlx5.MLX5_CMD_OP_CREATE_CQ, log_cq_size, entry_sz=64, owner_off=63,
|
||||
cq_context=dict(log_cq_size=log_cq_size, uar_page=dev.uar, c_eqn_or_apu_element=self.eq_info['eq_number'],
|
||||
dbr_addr=dev.dbr_paddrs[0] + self.cq_dbr, log_page_size=0))
|
||||
|
||||
# create QP, buffer is RQ (16B stride) + SQ (64B stride)
|
||||
self.sq_offset = (1 << log_rq_size) << 4
|
||||
self.qp_buf, self.qp_paddrs, self.qp_info = self.create_queue(mlx5.MLX5_CMD_OP_CREATE_QP, log_sq_size, entry_sz=64,
|
||||
owner_off=0, extra_sz=self.sq_offset,
|
||||
qpc=dict(st=0, pm_state=3, pd=dev.pd, cqn_snd=self.cq_info['cqn'], cqn_rcv=self.cq_info['cqn'], log_msg_max=30, log_rq_size=log_rq_size,
|
||||
log_rq_stride=0, log_sq_size=log_sq_size, rlky=1, uar_page=dev.uar, log_page_size=0, dbr_addr=dev.dbr_paddrs[0] + self.qp_dbr))
|
||||
|
||||
# transition to INIT
|
||||
self.qp_op(mlx5.MLX5_CMD_OP_RST2INIT_QP, qpc_args=dict(log_ack_req_freq=8), addr_args=dict(pkey_index=0, vhca_port_num=1))
|
||||
|
||||
for i in range(self.cq_size): self.cq_mem[i * 64 + 63] = 0x01 # init owner bits so poll_cq waits for real CQEs
|
||||
if MLX_DEBUG >= 1: print(f"mlx5: QP 0x{self.qp_info['qpn']:x} (EQ={self.eq_info['eq_number']} CQ=0x{self.cq_info['cqn']:x})")
|
||||
|
||||
def create_queue(self, opcode, log_size, entry_sz, owner_off, extra_sz=0, **ctx_kw):
|
||||
mem, paddrs = self.dev.pci_dev.alloc_sysmem((n := ceildiv((1 << log_size) * entry_sz + extra_sz, 0x1000)) * 0x1000)
|
||||
return mem, paddrs, self.dev.cmd.exec(opcode, payload=struct.pack(f'>{n}Q', *paddrs), **ctx_kw)
|
||||
|
||||
def qp_op(self, opcode, qpc_args=None, addr_args=None, **kwargs):
|
||||
qpc_args = dict(st=0, pm_state=3, pd=self.dev.pd, cqn_snd=self.cq_info['cqn'], cqn_rcv=self.cq_info['cqn'], **(qpc_args or {}))
|
||||
self.dev.cmd.exec(opcode, qpn=self.qp_info['qpn'], qpc=(qpc_args or {}) | {'primary_address_path': addr_args or {}}, **kwargs)
|
||||
|
||||
def connect(self, remote:MLXQP):
|
||||
self.qp_op(mlx5.MLX5_CMD_OP_INIT2RTR_QP, opt_param_mask=0x1A,
|
||||
qpc_args=dict(mtu=5, log_msg_max=self.dev.caps['log_max_msg'], remote_qpn=remote.qp_info['qpn'], log_ack_req_freq=8,
|
||||
log_rra_max=3, rre=1, rwe=1, min_rnr_nak=1, next_rcv_psn=0),
|
||||
addr_args=dict(pkey_index=0, src_addr_index=0, hop_limit=64, udp_sport=udp_sport(self.qp_info['qpn'], remote.qp_info['qpn']), vhca_port_num=1,
|
||||
rmac_47_32=hi32(remote.dev.mac), rmac_31_0=lo32(remote.dev.mac), rgid_rip=int.from_bytes(remote.dev.local_gid, 'big')))
|
||||
self.qp_op(mlx5.MLX5_CMD_OP_RTR2RTS_QP, qpc_args=dict(log_ack_req_freq=8, next_send_psn=0, log_sra_max=3, retry_count=7, rnr_retry=7),
|
||||
addr_args=dict(ack_timeout=14, vhca_port_num=1))
|
||||
|
||||
if MLX_DEBUG >= 1: print(f"mlx5: QP 0x{self.qp_info['qpn']:x} connected (remote=0x{remote.qp_info['qpn']:x})")
|
||||
649
tinygrad_repo/tinygrad/runtime/support/nv/ip.py
Normal file
649
tinygrad_repo/tinygrad/runtime/support/nv/ip.py
Normal file
@@ -0,0 +1,649 @@
|
||||
from __future__ import annotations
|
||||
import ctypes, time, array, struct, itertools, dataclasses
|
||||
from typing import cast, Any
|
||||
from tinygrad.runtime.autogen import nv, nv_570 as nv_gpu, pci
|
||||
from tinygrad.helpers import lo32, hi32, DEBUG, round_up, round_down, fetch_fw, wait_cond, ceildiv
|
||||
from tinygrad.runtime.support.system import System, MMIOInterface
|
||||
from tinygrad.runtime.support.elf import elf_loader
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class GRBufDesc: size:int; virt:bool; phys:bool; local:bool=False # noqa: E702
|
||||
|
||||
class NV_IP:
|
||||
def __init__(self, nvdev): self.nvdev = nvdev
|
||||
def init_sw(self): pass # Prepare sw/allocations for this IP
|
||||
def init_hw(self): pass # Initialize hw for this IP
|
||||
def fini_hw(self): pass # Finalize hw for this IP
|
||||
|
||||
class NVRpcQueue:
|
||||
def __init__(self, gsp:NV_GSP, view:MMIOInterface, completion_q_view:MMIOInterface|None=None):
|
||||
self.tx_view = view.view(fmt='I')
|
||||
wait_cond(lambda: self.tx_view[getattr(nv.msgqTxHeader, 'entryOff').offset // 4], value=0x1000, msg="RPC queue not initialized")
|
||||
self.tx = nv.msgqTxHeader.from_buffer_copy(bytes(view[:ctypes.sizeof(nv.msgqTxHeader)]))
|
||||
|
||||
if completion_q_view is not None:
|
||||
comp_tx = nv.msgqTxHeader.from_buffer_copy(bytes(completion_q_view[:ctypes.sizeof(nv.msgqTxHeader)]))
|
||||
self.rx_view = completion_q_view.view(comp_tx.rxHdrOff, fmt='I')
|
||||
|
||||
self.gsp, self.view, self.seq = gsp, view, 0
|
||||
self.queue_mv = view.view(self.tx.entryOff, self.tx.msgSize * self.tx.msgCount)
|
||||
|
||||
def _checksum(self, data:bytes):
|
||||
if (pad_len:=(-len(data)) % 8): data += b'\x00' * pad_len
|
||||
checksum = 0
|
||||
for offset in range(0, len(data), 8): checksum ^= struct.unpack_from('Q', data, offset)[0]
|
||||
return hi32(checksum) ^ lo32(checksum)
|
||||
|
||||
def _send_rpc_record(self, func:int, msg:bytes):
|
||||
header = nv.rpc_message_header_v(signature=nv.NV_VGPU_MSG_SIGNATURE_VALID, rpc_result=nv.NV_VGPU_MSG_RESULT_RPC_PENDING,
|
||||
rpc_result_private=nv.NV_VGPU_MSG_RESULT_RPC_PENDING, header_version=(3<<24), function=func, length=len(msg) + 0x20)
|
||||
|
||||
msg = bytes(header) + msg
|
||||
phdr = nv.GSP_MSG_QUEUE_ELEMENT(elemCount=ceildiv(len(msg) + ctypes.sizeof(nv.GSP_MSG_QUEUE_ELEMENT), self.tx.msgSize), seqNum=self.seq)
|
||||
phdr.checkSum = self._checksum(bytes(phdr) + msg)
|
||||
msg = (bytes(phdr) + msg).ljust(phdr.elemCount * self.tx.msgSize, b'\x00')
|
||||
|
||||
wp = self.tx_view[getattr(nv.msgqTxHeader, 'writePtr').offset // 4]
|
||||
off, first = wp * self.tx.msgSize, min(len(msg), len(self.queue_mv) - wp * self.tx.msgSize)
|
||||
self.queue_mv[off:off+first] = msg[:first]
|
||||
if first < len(msg): self.queue_mv[:len(msg)-first] = msg[first:]
|
||||
self.tx_view[getattr(nv.msgqTxHeader, 'writePtr').offset // 4] = (wp + phdr.elemCount) % self.tx.msgCount
|
||||
System.memory_barrier()
|
||||
|
||||
self.seq += 1
|
||||
self.gsp.nvdev.NV_PGSP_QUEUE_HEAD[0].write(0x0)
|
||||
|
||||
def send_rpc(self, func:int, msg:bytes):
|
||||
max_payload = self.tx.msgSize * 16 - ctypes.sizeof(nv.GSP_MSG_QUEUE_ELEMENT) - ctypes.sizeof(nv.rpc_message_header_v)
|
||||
self._send_rpc_record(func, msg[:max_payload])
|
||||
for off in range(max_payload, len(msg), max_payload): self._send_rpc_record(nv.NV_VGPU_MSG_FUNCTION_CONTINUATION_RECORD, msg[off:off+max_payload])
|
||||
|
||||
def read_resp(self):
|
||||
System.memory_barrier()
|
||||
while self.rx_view[0] != self.tx_view[getattr(nv.msgqTxHeader, 'writePtr').offset // 4]:
|
||||
off = self.rx_view[0] * self.tx.msgSize
|
||||
hdr = nv.rpc_message_header_v.from_buffer_copy(bytes(self.queue_mv[off + 0x30 : off + 0x30 + ctypes.sizeof(nv.rpc_message_header_v)]))
|
||||
msg = bytes(self.queue_mv[off + 0x50 : off + 0x50 + hdr.length])
|
||||
|
||||
# Handling special functions
|
||||
if hdr.function == nv.NV_VGPU_MSG_EVENT_GSP_RUN_CPU_SEQUENCER: self.gsp.run_cpu_seq(msg)
|
||||
elif hdr.function == nv.NV_VGPU_MSG_EVENT_OS_ERROR_LOG:
|
||||
print(f"nv {self.gsp.nvdev.devfmt}: GSP LOG: {msg[12:].rstrip(bytes([0])).decode('utf-8')}")
|
||||
|
||||
self.gsp.nvdev.is_err_state |= hdr.function in {nv.NV_VGPU_MSG_EVENT_OS_ERROR_LOG, nv.NV_VGPU_MSG_EVENT_MMU_FAULT_QUEUED}
|
||||
|
||||
# Update the read pointer
|
||||
self.rx_view[0] = (self.rx_view[0] + round_up(hdr.length, self.tx.msgSize) // self.tx.msgSize) % self.tx.msgCount
|
||||
System.memory_barrier()
|
||||
|
||||
if DEBUG >= 3:
|
||||
nm = nv.rpc_fns.get(hdr.function, nv.rpc_events.get(hdr.function, f'ev:{hdr.function:x}'))
|
||||
print(f"nv {self.gsp.nvdev.devfmt}: in RPC: {nm}, res:{hdr.rpc_result:#x}")
|
||||
|
||||
if hdr.rpc_result != 0: raise RuntimeError(f"RPC call {hdr.function} failed with result {hdr.rpc_result}")
|
||||
yield hdr.function, msg
|
||||
|
||||
def wait_resp(self, cmd:int, timeout:int=10000) -> bytes:
|
||||
start_time = int(time.perf_counter() * 1000)
|
||||
while (int(time.perf_counter() * 1000) - start_time) < timeout:
|
||||
if (msg:=next((message for func, message in self.read_resp() if func == cmd), None)) is not None: return msg
|
||||
raise RuntimeError(f"Timeout waiting for RPC response for command {cmd}")
|
||||
|
||||
class NV_FLCN(NV_IP):
|
||||
def wait_for_reset(self):
|
||||
wait_cond(lambda _: self.nvdev.NV_PGC6_AON_SECURE_SCRATCH_GROUP_05_PRIV_LEVEL_MASK.read_bitfields()['read_protection_level0'] == 1 and
|
||||
self.nvdev.NV_PGC6_AON_SECURE_SCRATCH_GROUP_05[0].read() & 0xff == 0xff, "waiting for reset")
|
||||
|
||||
def init_sw(self):
|
||||
self.nvdev.include("dev_gsp", "ga102")
|
||||
self.nvdev.include("dev_falcon_v4", "ga102")
|
||||
self.nvdev.include("dev_riscv_pri", "ga102")
|
||||
self.nvdev.include("dev_fbif_v4", "ga102")
|
||||
self.nvdev.include("dev_falcon_second_pri", "ga102")
|
||||
self.nvdev.include("dev_sec_pri", "ga102")
|
||||
self.nvdev.include("dev_bus", "tu102")
|
||||
|
||||
self.prep_ucode()
|
||||
self.prep_booter()
|
||||
|
||||
def prep_ucode(self):
|
||||
vbios_bytes, vbios_off = memoryview(bytes(array.array('I', self.nvdev.mmio[0x00300000//4:(0x00300000+0x100000)//4]))), 0
|
||||
while True:
|
||||
pci_blck = vbios_bytes[vbios_off + nv.OFFSETOF_PCI_EXP_ROM_PCI_DATA_STRUCT_PTR:].cast('H')[0]
|
||||
imglen = vbios_bytes[vbios_off + pci_blck + nv.OFFSETOF_PCI_DATA_STRUCT_IMAGE_LEN:].cast('H')[0] * nv.PCI_ROM_IMAGE_BLOCK_SIZE
|
||||
match vbios_bytes[vbios_off + pci_blck + nv.OFFSETOF_PCI_DATA_STRUCT_CODE_TYPE]:
|
||||
case nv.NV_BCRT_HASH_INFO_BASE_CODE_TYPE_VBIOS_BASE: block_size = imglen
|
||||
case nv.NV_BCRT_HASH_INFO_BASE_CODE_TYPE_VBIOS_EXT:
|
||||
expansion_rom_off = vbios_off - block_size
|
||||
break
|
||||
vbios_off += imglen
|
||||
|
||||
bit_header = nv.BIT_HEADER_V1_00.from_buffer_copy(vbios_bytes[(bit_addr:=0x1b0):bit_addr + ctypes.sizeof(nv.BIT_HEADER_V1_00)])
|
||||
assert bit_header.Signature == 0x00544942, f"Invalid BIT header signature {hex(bit_header.Signature)}"
|
||||
|
||||
for i in range(bit_header.TokenEntries):
|
||||
bit = nv.BIT_TOKEN_V1_00.from_buffer_copy(vbios_bytes[bit_addr + bit_header.HeaderSize + i * bit_header.TokenSize:])
|
||||
if bit.TokenId != nv.BIT_TOKEN_FALCON_DATA or bit.DataVersion != 2 or bit.DataSize < nv.BIT_DATA_FALCON_DATA_V2_SIZE_4: continue
|
||||
|
||||
falcon_data = nv.BIT_DATA_FALCON_DATA_V2.from_buffer_copy(vbios_bytes[bit.DataPtr & 0xffff:])
|
||||
ucode_hdr = nv.FALCON_UCODE_TABLE_HDR_V1.from_buffer_copy(vbios_bytes[(table_ptr:=expansion_rom_off + falcon_data.FalconUcodeTablePtr):])
|
||||
for j in range(ucode_hdr.EntryCount):
|
||||
ucode_entry = nv.FALCON_UCODE_TABLE_ENTRY_V1.from_buffer_copy(vbios_bytes[table_ptr + ucode_hdr.HeaderSize + j * ucode_hdr.EntrySize:])
|
||||
if ucode_entry.ApplicationID != nv.FALCON_UCODE_ENTRY_APPID_FWSEC_PROD: continue
|
||||
|
||||
ucode_desc_hdr = nv.FALCON_UCODE_DESC_HEADER.from_buffer_copy(vbios_bytes[expansion_rom_off + ucode_entry.DescPtr:])
|
||||
ucode_desc_off = expansion_rom_off + ucode_entry.DescPtr
|
||||
ucode_desc_size = ucode_desc_hdr.vDesc >> 16
|
||||
|
||||
self.desc_v3 = nv.FALCON_UCODE_DESC_V3.from_buffer_copy(vbios_bytes[ucode_desc_off:ucode_desc_off + ucode_desc_size])
|
||||
|
||||
sig_total_size = ucode_desc_size - nv.FALCON_UCODE_DESC_V3_SIZE_44
|
||||
signature = vbios_bytes[ucode_desc_off + nv.FALCON_UCODE_DESC_V3_SIZE_44:][:sig_total_size]
|
||||
image = vbios_bytes[ucode_desc_off + ucode_desc_size:][:round_up(self.desc_v3.StoredSize, 256)]
|
||||
|
||||
self.frts_offset = self.nvdev.vram_size - 0x100000 - 0x100000
|
||||
read_vbios_desc = nv.FWSECLIC_READ_VBIOS_DESC(version=0x1, size=ctypes.sizeof(nv.FWSECLIC_READ_VBIOS_DESC), flags=2)
|
||||
frst_reg_desc = nv.FWSECLIC_FRTS_REGION_DESC(version=0x1, size=ctypes.sizeof(nv.FWSECLIC_FRTS_REGION_DESC),
|
||||
frtsRegionOffset4K=self.frts_offset >> 12, frtsRegionSize=0x100, frtsRegionMediaType=2)
|
||||
frts_cmd = nv.FWSECLIC_FRTS_CMD(readVbiosDesc=read_vbios_desc, frtsRegionDesc=frst_reg_desc)
|
||||
|
||||
def __patch(cmd_id, cmd):
|
||||
patched_image = bytearray(image)
|
||||
|
||||
dmem_offset = 0
|
||||
hdr = nv.FALCON_APPLICATION_INTERFACE_HEADER_V1.from_buffer_copy(image[(app_hdr_off:=self.desc_v3.IMEMLoadSize+self.desc_v3.InterfaceOffset):])
|
||||
ents = (nv.FALCON_APPLICATION_INTERFACE_ENTRY_V1 * hdr.entryCount).from_buffer_copy(image[app_hdr_off + ctypes.sizeof(hdr):])
|
||||
for i in range(hdr.entryCount):
|
||||
if ents[i].id == nv.FALCON_APPLICATION_INTERFACE_ENTRY_ID_DMEMMAPPER: dmem_offset = ents[i].dmemOffset
|
||||
|
||||
# Patch image
|
||||
dmem = nv.FALCON_APPLICATION_INTERFACE_DMEM_MAPPER_V3.from_buffer_copy(image[(dmem_mapper_offset:=self.desc_v3.IMEMLoadSize+dmem_offset):])
|
||||
dmem.init_cmd = cmd_id
|
||||
patched_image[dmem_mapper_offset : dmem_mapper_offset+len(bytes(dmem))] = bytes(dmem)
|
||||
patched_image[(cmd_off:=self.desc_v3.IMEMLoadSize+dmem.cmd_in_buffer_offset) : cmd_off+len(cmd)] = cmd
|
||||
patched_image[(sig_off:=self.desc_v3.IMEMLoadSize+self.desc_v3.PKCDataOffset) : sig_off+0x180] = signature[-0x180:]
|
||||
|
||||
return self.nvdev._alloc_boot_mem(len(patched_image), data=patched_image, sysmem=False)
|
||||
|
||||
_, self.frts_image_paddr, _ = __patch(0x15, bytes(frts_cmd))
|
||||
|
||||
def prep_booter(self):
|
||||
sha = {"ga102":"4497e3eff7e95c774b8a569d17b27c08c9650158d10b229d2be81cdcad9a085b",
|
||||
"ad102":"8b293e19b637c5e22c87a2428d1c71bb13e0904e8a88ac6b3c6c1f2679c6e37a"}[self.nvdev.fw_name]
|
||||
h = nv.struct_nvfw_bin_hdr.from_buffer_copy(b:=fetch_fw(f"nvidia/{self.nvdev.fw_name}/gsp", "booter_load-570.144.bin", sha))
|
||||
lh = nv.struct_nvfw_hs_load_header_v2.from_buffer_copy(b, (hs:=nv.struct_nvfw_hs_header_v2.from_buffer_copy(b, h.header_offset)).header_offset)
|
||||
app = nv.struct_nvfw_hs_load_header_v2_app.from_buffer_copy(b, hs.header_offset + ctypes.sizeof(nv.struct_nvfw_hs_load_header_v2))
|
||||
|
||||
patch_loc, patch_sig = struct.unpack_from("<I", b, hs.patch_loc)[0], struct.unpack_from("<I", b, hs.patch_sig)[0]
|
||||
sig = b[(sig_off:=hs.sig_prod_offset + patch_sig):sig_off + (sig_len:=hs.sig_prod_size // struct.unpack_from("<I", b, hs.num_sig)[0])]
|
||||
|
||||
(patched_image:=bytearray(b[h.data_offset:h.data_offset + h.data_size]))[patch_loc:patch_loc+sig_len] = sig
|
||||
|
||||
_, self.booter_image_paddr, _ = self.nvdev._alloc_boot_mem(len(patched_image), data=patched_image, sysmem=False)
|
||||
self.booter_data_off, self.booter_data_sz, self.booter_code_off, self.booter_code_sz = lh.os_data_offset, lh.os_data_size, app.offset, app.size
|
||||
|
||||
def init_hw(self):
|
||||
self.falcon, self.sec2 = 0x00110000, 0x00840000
|
||||
|
||||
self.reset(self.falcon)
|
||||
self.execute_hs(self.falcon, self.frts_image_paddr, code_off=0x0, data_off=self.desc_v3.IMEMLoadSize,
|
||||
imemPa=self.desc_v3.IMEMPhysBase, imemVa=self.desc_v3.IMEMVirtBase, imemSz=self.desc_v3.IMEMLoadSize,
|
||||
dmemPa=self.desc_v3.DMEMPhysBase, dmemVa=0x0, dmemSz=self.desc_v3.DMEMLoadSize,
|
||||
pkc_off=self.desc_v3.PKCDataOffset, engid=self.desc_v3.EngineIdMask, ucodeid=self.desc_v3.UcodeId)
|
||||
assert self.nvdev.NV_PFB_PRI_MMU_WPR2_ADDR_HI.read() != 0, "WPR2 is not initialized"
|
||||
|
||||
self.reset(self.falcon, riscv=True)
|
||||
|
||||
# set up the mailbox
|
||||
self.nvdev.NV_PGSP_FALCON_MAILBOX0.write(lo32(self.nvdev.gsp.libos_args_sysmem))
|
||||
self.nvdev.NV_PGSP_FALCON_MAILBOX1.write(hi32(self.nvdev.gsp.libos_args_sysmem))
|
||||
|
||||
# booter
|
||||
self.reset(self.sec2)
|
||||
mbx = self.execute_hs(self.sec2, self.booter_image_paddr, code_off=self.booter_code_off, data_off=self.booter_data_off,
|
||||
imemPa=0x0, imemVa=self.booter_code_off, imemSz=self.booter_code_sz, dmemPa=0x0, dmemVa=0x0, dmemSz=self.booter_data_sz,
|
||||
pkc_off=0x10, engid=1, ucodeid=3, mailbox=self.nvdev.gsp.wpr_meta_sysmem)
|
||||
assert mbx[0] == 0x0, f"Booter failed to execute, mailbox is {mbx[0]:08x}, {mbx[1]:08x}"
|
||||
|
||||
self.nvdev.NV_PFALCON_FALCON_OS.with_base(self.falcon).write(0x0)
|
||||
assert self.nvdev.NV_PRISCV_RISCV_CPUCTL.with_base(self.falcon).read_bitfields()['active_stat'] == 1, "GSP Core is not active"
|
||||
|
||||
def execute_dma(self, base:int, cmd:int, dest:int, mem_off:int, src:int, size:int):
|
||||
wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).read_bitfields()['full'], value=0, msg="DMA does not progress")
|
||||
|
||||
self.nvdev.NV_PFALCON_FALCON_DMATRFBASE.with_base(base).write(lo32(src >> 8))
|
||||
self.nvdev.NV_PFALCON_FALCON_DMATRFBASE1.with_base(base).write(hi32(src >> 8) & 0x1ff)
|
||||
|
||||
xfered = 0
|
||||
while xfered < size:
|
||||
wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).read_bitfields()['full'], value=0, msg="DMA does not progress")
|
||||
|
||||
self.nvdev.NV_PFALCON_FALCON_DMATRFMOFFS.with_base(base).write(dest + xfered)
|
||||
self.nvdev.NV_PFALCON_FALCON_DMATRFFBOFFS.with_base(base).write(mem_off + xfered)
|
||||
self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).write(cmd)
|
||||
xfered += 256
|
||||
|
||||
wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).read_bitfields()['idle'], msg="DMA does not complete")
|
||||
|
||||
def start_cpu(self, base:int):
|
||||
if self.nvdev.NV_PFALCON_FALCON_CPUCTL.with_base(base).read_bitfields()['alias_en'] == 1:
|
||||
self.nvdev.wreg(base + self.nvdev.NV_PFALCON_FALCON_CPUCTL_ALIAS, 0x2)
|
||||
else: self.nvdev.NV_PFALCON_FALCON_CPUCTL.with_base(base).write(startcpu=1)
|
||||
|
||||
def wait_cpu_halted(self, base): wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_CPUCTL.with_base(base).read_bitfields()['halted'], msg="not halted")
|
||||
|
||||
def execute_hs(self, base, img_paddr, code_off, data_off, imemPa, imemVa, imemSz, dmemPa, dmemVa, dmemSz, pkc_off, engid, ucodeid, mailbox=None):
|
||||
self.disable_ctx_req(base)
|
||||
|
||||
# target=0 is FB (not in published headers)
|
||||
self.nvdev.NV_PFALCON_FBIF_TRANSCFG.with_base(base)[ctx_dma:=0].update(target=0, mem_type=self.nvdev.NV_PFALCON_FBIF_TRANSCFG_MEM_TYPE_PHYSICAL)
|
||||
|
||||
cmd = self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).encode(write=0, size=self.nvdev.NV_PFALCON_FALCON_DMATRFCMD_SIZE_256B,
|
||||
ctxdma=ctx_dma, imem=1, sec=1)
|
||||
self.execute_dma(base, cmd, dest=imemPa, mem_off=imemVa, src=img_paddr+code_off-imemVa, size=imemSz)
|
||||
|
||||
cmd = self.nvdev.NV_PFALCON_FALCON_DMATRFCMD.with_base(base).encode(write=0, size=self.nvdev.NV_PFALCON_FALCON_DMATRFCMD_SIZE_256B,
|
||||
ctxdma=ctx_dma, imem=0, sec=0)
|
||||
self.execute_dma(base, cmd, dest=dmemPa, mem_off=dmemVa, src=img_paddr+data_off-dmemVa, size=dmemSz)
|
||||
|
||||
self.nvdev.NV_PFALCON2_FALCON_BROM_PARAADDR.with_base(base)[0].write(pkc_off)
|
||||
self.nvdev.NV_PFALCON2_FALCON_BROM_ENGIDMASK.with_base(base).write(engid)
|
||||
self.nvdev.NV_PFALCON2_FALCON_BROM_CURR_UCODE_ID.with_base(base).write(val=ucodeid)
|
||||
self.nvdev.NV_PFALCON2_FALCON_MOD_SEL.with_base(base).write(algo=self.nvdev.NV_PFALCON2_FALCON_MOD_SEL_ALGO_RSA3K)
|
||||
|
||||
self.nvdev.NV_PFALCON_FALCON_BOOTVEC.with_base(base).write(imemVa)
|
||||
|
||||
if mailbox is not None:
|
||||
self.nvdev.NV_PFALCON_FALCON_MAILBOX0.with_base(base).write(lo32(mailbox))
|
||||
self.nvdev.NV_PFALCON_FALCON_MAILBOX1.with_base(base).write(hi32(mailbox))
|
||||
|
||||
self.start_cpu(base)
|
||||
self.wait_cpu_halted(base)
|
||||
|
||||
if mailbox is not None:
|
||||
return self.nvdev.NV_PFALCON_FALCON_MAILBOX0.with_base(base).read(), self.nvdev.NV_PFALCON_FALCON_MAILBOX1.with_base(base).read()
|
||||
|
||||
def disable_ctx_req(self, base:int):
|
||||
self.nvdev.NV_PFALCON_FBIF_CTL.with_base(base).update(allow_phys_no_ctx=1)
|
||||
self.nvdev.NV_PFALCON_FALCON_DMACTL.with_base(base).write(0x0)
|
||||
|
||||
def reset(self, base:int, riscv=False):
|
||||
engine_reg = self.nvdev.NV_PGSP_FALCON_ENGINE if base == self.falcon else self.nvdev.NV_PSEC_FALCON_ENGINE
|
||||
engine_reg.write(reset=1)
|
||||
time.sleep(0.1)
|
||||
engine_reg.write(reset=0)
|
||||
|
||||
wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_HWCFG2.with_base(base).read_bitfields()['mem_scrubbing'], value=0, msg="Scrubbing not completed")
|
||||
|
||||
if riscv: self.nvdev.NV_PRISCV_RISCV_BCR_CTRL.with_base(base).write(core_select=1, valid=0, brfetch=1)
|
||||
elif self.nvdev.NV_PFALCON_FALCON_HWCFG2.with_base(base).read_bitfields()['riscv'] == 1:
|
||||
self.nvdev.NV_PRISCV_RISCV_BCR_CTRL.with_base(base).write(core_select=0)
|
||||
wait_cond(lambda: self.nvdev.NV_PRISCV_RISCV_BCR_CTRL.with_base(base).read_bitfields()['valid'], msg="RISCV core not booted")
|
||||
self.nvdev.NV_PFALCON_FALCON_RM.with_base(base).write(self.nvdev.chip_id)
|
||||
|
||||
class NV_FLCN_COT(NV_IP):
|
||||
def wait_for_reset(self):
|
||||
self.nvdev.include("dev_therm", "gb202")
|
||||
wait_cond(lambda _: self.nvdev.NV_THERM_I2CS_SCRATCH.read() == 0xff, "waiting for reset")
|
||||
|
||||
def init_sw(self):
|
||||
self.nvdev.include("dev_gsp", "ga102")
|
||||
self.nvdev.include("dev_falcon_v4", "gh100")
|
||||
self.nvdev.include("dev_vm", "gh100")
|
||||
self.nvdev.include("dev_fsp_pri", "gh100")
|
||||
self.nvdev.include("dev_bus", "tu102")
|
||||
|
||||
self.fmc_boot_args_view, _, fmc_boot_addrs = self.nvdev._alloc_boot_mem(ctypes.sizeof(nv.GSP_FMC_BOOT_PARAMS),
|
||||
data=bytes(nv.GSP_FMC_BOOT_PARAMS()))
|
||||
self.fmc_boot_args_sysmem = fmc_boot_addrs[0]
|
||||
self.init_fmc_image()
|
||||
|
||||
def init_fmc_image(self):
|
||||
_, sections, _ = elf_loader(fetch_fw(f"nvidia/{self.nvdev.fw_name}/gsp", "fmc-570.144.bin",
|
||||
"cb59a35c1d4bd1274d7267fd10243c29f843ff41c851b9cbd59f5af2ddd7fece"))
|
||||
def _section(s): return next((sh.content for sh in sections if sh.name == s))
|
||||
self.fmc_booter_image, self.fmc_booter_hash = _section("image"), memoryview(_section("hash")).cast('I')
|
||||
self.fmc_booter_sig, self.fmc_booter_pkey = memoryview(_section("signature")).cast('I'), memoryview(_section("publickey") + b"\x00" * 3).cast('I')
|
||||
_, _, fmc_booter_addrs = self.nvdev._alloc_boot_mem(len(self.fmc_booter_image), data=self.fmc_booter_image)
|
||||
self.fmc_booter_bar1 = fmc_booter_addrs[0]
|
||||
|
||||
def init_hw(self):
|
||||
self.falcon = 0x00110000
|
||||
|
||||
boot_args = nv.GSP_ACR_BOOT_GSP_RM_PARAMS(gspRmDescOffset=self.nvdev.gsp.wpr_meta_sysmem,
|
||||
gspRmDescSize=ctypes.sizeof(nv.GspFwWprMeta), target=nv.GSP_DMA_TARGET_COHERENT_SYSTEM, bIsGspRmBoot=True)
|
||||
rm_args = nv.GSP_RM_PARAMS(bootArgsOffset=self.nvdev.gsp.libos_args_sysmem, target=nv.GSP_DMA_TARGET_COHERENT_SYSTEM)
|
||||
self.fmc_boot_args_view[:ctypes.sizeof(nv.GSP_FMC_BOOT_PARAMS)] = bytes(nv.GSP_FMC_BOOT_PARAMS(bootGspRmParams=boot_args, gspRmParams=rm_args))
|
||||
|
||||
cot_payload = nv.NVDM_PAYLOAD_COT(version=0x2, size=ctypes.sizeof(nv.NVDM_PAYLOAD_COT), frtsVidmemOffset=0x1c00000, frtsVidmemSize=0x100000,
|
||||
gspBootArgsSysmemOffset=self.fmc_boot_args_sysmem, gspFmcSysmemOffset=self.fmc_booter_bar1)
|
||||
for i,x in enumerate(self.fmc_booter_hash): cot_payload.hash384[i] = x
|
||||
for i,x in enumerate(self.fmc_booter_sig): cot_payload.signature[i] = x
|
||||
for i,x in enumerate(self.fmc_booter_pkey): cot_payload.publicKey[i] = x
|
||||
|
||||
self.kfsp_send_msg(nv.NVDM_TYPE_COT, bytes(cot_payload))
|
||||
wait_cond(lambda: self.nvdev.NV_PFALCON_FALCON_HWCFG2.with_base(self.falcon).read_bitfields()['riscv_br_priv_lockdown'], value=0)
|
||||
|
||||
def kfsp_send_msg(self, nvmd:int, buf:bytes):
|
||||
# All single-packets go to seid 0
|
||||
headers = int.to_bytes((1 << 31) | (1 << 30), 4, 'little') + int.to_bytes((0x7e << 0) | (0x10de << 8) | (nvmd << 24), 4, 'little')
|
||||
buf = headers + buf + (4 - (len(buf) % 4)) * b'\x00'
|
||||
assert len(buf) < 0x400, f"FSP message too long: {len(buf)} bytes, max 1024 bytes"
|
||||
|
||||
self.nvdev.NV_PFSP_EMEMC[0].write(offs=0, blk=0, aincw=1, aincr=0)
|
||||
for i in range(0, len(buf), 4): self.nvdev.NV_PFSP_EMEMD[0].write(int.from_bytes(buf[i:i+4], 'little'))
|
||||
|
||||
self.nvdev.NV_PFSP_QUEUE_TAIL[0].write(len(buf) - 4)
|
||||
self.nvdev.NV_PFSP_QUEUE_HEAD[0].write(0)
|
||||
|
||||
# Waiting for a response
|
||||
wait_cond(lambda: self.nvdev.NV_PFSP_MSGQ_HEAD[0].read() != self.nvdev.NV_PFSP_MSGQ_TAIL[0].read(), msg="FSP didn't respond to message")
|
||||
|
||||
self.nvdev.NV_PFSP_EMEMC[0].write(offs=0, blk=0, aincw=0, aincr=1)
|
||||
self.nvdev.NV_PFSP_MSGQ_TAIL[0].write(self.nvdev.NV_PFSP_MSGQ_HEAD[0].read())
|
||||
|
||||
class NV_GSP(NV_IP):
|
||||
def init_sw(self):
|
||||
self.handle_gen = itertools.count(0xcf000000)
|
||||
self.init_rm_args()
|
||||
self.init_libos_args()
|
||||
self.init_wpr_meta()
|
||||
|
||||
# Prefill cmd queue with info for gsp to start.
|
||||
self.rpc_set_gsp_system_info()
|
||||
self.rpc_set_registry_table()
|
||||
|
||||
self.gpfifo_class, self.compute_class, self.dma_class = nv_gpu.AMPERE_CHANNEL_GPFIFO_A, nv_gpu.AMPERE_COMPUTE_B, nv_gpu.AMPERE_DMA_COPY_B
|
||||
match self.nvdev.chip_name[:2]:
|
||||
case "AD": self.compute_class = nv_gpu.ADA_COMPUTE_A
|
||||
case "GB":
|
||||
self.gpfifo_class,self.compute_class,self.dma_class=nv_gpu.BLACKWELL_CHANNEL_GPFIFO_A,nv_gpu.BLACKWELL_COMPUTE_B,nv_gpu.BLACKWELL_DMA_COPY_B
|
||||
|
||||
def init_rm_args(self, queue_size=0x40000):
|
||||
# Alloc queues
|
||||
pte_cnt = ((queue_pte_cnt:=(queue_size * 2) // 0x1000)) + round_up(queue_pte_cnt * 8, 0x1000) // 0x1000
|
||||
pt_size = round_up(pte_cnt * 8, 0x1000)
|
||||
queues_view, _, queues_sysmem = self.nvdev._alloc_boot_mem(pt_size + queue_size * 2, sysmem=True)
|
||||
|
||||
# Fill up ptes
|
||||
for i, sysmem in enumerate(queues_sysmem): queues_view.view(i * 0x8, 0x8, fmt='Q')[0] = sysmem
|
||||
|
||||
# Fill up arguments
|
||||
queue_args = nv.MESSAGE_QUEUE_INIT_ARGUMENTS(sharedMemPhysAddr=queues_sysmem[0], pageTableEntryCount=pte_cnt, cmdQueueOffset=pt_size,
|
||||
statQueueOffset=pt_size + queue_size)
|
||||
_, _, rm_args_addrs = self.nvdev._alloc_boot_mem(ctypes.sizeof(nv.GSP_ARGUMENTS_CACHED),
|
||||
data=bytes(nv.GSP_ARGUMENTS_CACHED(bDmemStack=True, messageQueueInitArguments=queue_args)))
|
||||
self.rm_args_sysmem = rm_args_addrs[0]
|
||||
|
||||
# Build command queue header
|
||||
# self.cmd_q_va, self.stat_q_va = queues_view.addr + pt_size, queues_view.addr + pt_size + queue_size
|
||||
self.cmd_q_view, self.stat_q_view = queues_view.view(pt_size), queues_view.view(pt_size + queue_size)
|
||||
|
||||
self.cmd_q_view[:ctypes.sizeof(nv.msgqTxHeader)] = bytes(nv.msgqTxHeader(version=0, size=queue_size, entryOff=0x1000, msgSize=0x1000,
|
||||
msgCount=(queue_size - 0x1000) // 0x1000, writePtr=0, flags=1, rxHdrOff=ctypes.sizeof(nv.msgqTxHeader)))
|
||||
|
||||
self.cmd_q = NVRpcQueue(self, self.cmd_q_view, None)
|
||||
|
||||
def init_libos_args(self):
|
||||
_, _, logbuf_addrs = self.nvdev._alloc_boot_mem(2 << 20)
|
||||
libos_args_view, _, libos_addrs = self.nvdev._alloc_boot_mem(0x1000)
|
||||
self.libos_args_sysmem = libos_addrs[0]
|
||||
|
||||
libos_structs = [nv.LibosMemoryRegionInitArgument(kind=nv.LIBOS_MEMORY_REGION_CONTIGUOUS, loc=nv.LIBOS_MEMORY_REGION_LOC_SYSMEM, size=0x10000,
|
||||
id8=int.from_bytes(bytes(f"LOG{name}", 'utf-8'), 'big'), pa=logbuf_addrs[0] + 0x10000 * i)
|
||||
for i, name in enumerate(["INIT", "INTR", "RM", "MNOC", "KRNL"])]
|
||||
libos_structs.append(nv.LibosMemoryRegionInitArgument(kind=nv.LIBOS_MEMORY_REGION_CONTIGUOUS, loc=nv.LIBOS_MEMORY_REGION_LOC_SYSMEM, size=0x1000,
|
||||
id8=int.from_bytes(bytes("RMARGS", 'utf-8'), 'big'), pa=self.rm_args_sysmem))
|
||||
libos_args_view[:sum(ctypes.sizeof(s) for s in libos_structs)] = b''.join(bytes(s) for s in libos_structs)
|
||||
|
||||
def init_gsp_image(self):
|
||||
_, sections, _ = elf_loader(fetch_fw("nvidia/ga102/gsp", "gsp-570.144.bin", "a8c3ebeed280323aedb51c061f321e73379cce7a9ae643a33dd03915df027f7f"))
|
||||
self.gsp_image = next((sh.content for sh in sections if sh.name == ".fwimage"))
|
||||
signature = next((sh.content for sh in sections if sh.name == (f".fwsignature_{self.nvdev.chip_name[:4].lower()}x")))
|
||||
|
||||
# Build radix3
|
||||
npages = [0, 0, 0, round_up(len(self.gsp_image), 0x1000) // 0x1000]
|
||||
for i in range(3, 0, -1): npages[i-1] = ((npages[i] - 1) >> (nv.LIBOS_MEMORY_REGION_RADIX_PAGE_LOG2 - 3)) + 1
|
||||
|
||||
offsets = [sum(npages[:i]) * 0x1000 for i in range(4)]
|
||||
radix_view, _, self.gsp_radix3_addrs = self.nvdev._alloc_boot_mem(offsets[-1] + len(self.gsp_image))
|
||||
|
||||
# Copy image
|
||||
radix_view.view(offsets[-1], len(self.gsp_image))[:] = self.gsp_image
|
||||
|
||||
# Copy level and image pages.
|
||||
for i in range(0, 3):
|
||||
cur_offset = sum(npages[:i+1])
|
||||
radix_view.view(offsets[i], npages[i+1] * 8, fmt='Q')[:] = array.array('Q', self.gsp_radix3_addrs[cur_offset:cur_offset+npages[i+1]])
|
||||
|
||||
# Copy signature
|
||||
_, _, gsp_sig_addrs = self.nvdev._alloc_boot_mem(len(signature), data=signature)
|
||||
self.gsp_signature_bar1 = gsp_sig_addrs[0]
|
||||
|
||||
def init_boot_binary_image(self):
|
||||
sha = {"ga102":"82428f532240727e95bb3083fbaaba9b2cc7b937314323f2d546ce7245f27fad",
|
||||
"ad102":"65ab2e6b6e0fca95365c4deac79a34582abcfeb15b6ae234138f22e7183118a8",
|
||||
"gb202":"d40b48e431d1707dc77af3605db358ed7a32ebfc2830eb74de2eddb4d3025071"}[self.nvdev.fw_name]
|
||||
h = nv.struct_nvfw_bin_hdr.from_buffer_copy(b:=fetch_fw(f"nvidia/{self.nvdev.fw_name}/gsp", "bootloader-570.144.bin", sha))
|
||||
self.booter_image, self.booter_desc = b[h.data_offset:h.data_offset+h.data_size], nv.RM_RISCV_UCODE_DESC.from_buffer_copy(b, h.header_offset)
|
||||
_, _, booter_addrs = self.nvdev._alloc_boot_mem(len(self.booter_image), data=self.booter_image)
|
||||
self.booter_bar1 = booter_addrs[0]
|
||||
|
||||
def init_wpr_meta(self):
|
||||
self.init_gsp_image()
|
||||
self.init_boot_binary_image()
|
||||
|
||||
common = {'sizeOfBootloader':(boot_sz:=len(self.booter_image)), 'sysmemAddrOfBootloader':self.booter_bar1,
|
||||
'sizeOfRadix3Elf':(radix3_sz:=len(self.gsp_image)), 'sysmemAddrOfRadix3Elf': self.gsp_radix3_addrs[0],
|
||||
'sizeOfSignature': 0x1000, 'sysmemAddrOfSignature': self.gsp_signature_bar1,
|
||||
'bootloaderCodeOffset': self.booter_desc.monitorCodeOffset, 'bootloaderDataOffset': self.booter_desc.monitorDataOffset,
|
||||
'bootloaderManifestOffset': self.booter_desc.manifestOffset, 'revision':nv.GSP_FW_WPR_META_REVISION, 'magic':nv.GSP_FW_WPR_META_MAGIC}
|
||||
|
||||
if self.nvdev.fmc_boot:
|
||||
m = nv.GspFwWprMeta(**common, vgaWorkspaceSize=0x20000, pmuReservedSize=0x1820000, nonWprHeapSize=0x220000, gspFwHeapSize=0x8700000,
|
||||
frtsSize=0x100000)
|
||||
else:
|
||||
m = nv.GspFwWprMeta(**common, vgaWorkspaceSize=(vga_sz:=0x100000), vgaWorkspaceOffset=(vga_off:=self.nvdev.vram_size-vga_sz),
|
||||
gspFwWprEnd=vga_off, frtsSize=(frts_sz:=0x100000), frtsOffset=(frts_off:=vga_off-frts_sz), bootBinOffset=(boot_off:=frts_off-boot_sz),
|
||||
gspFwOffset=(gsp_off:=round_down(boot_off-radix3_sz, 0x10000)), gspFwHeapSize=(gsp_heap_sz:=0x8100000), fbSize=self.nvdev.vram_size,
|
||||
gspFwHeapOffset=(gsp_heap_off:=round_down(gsp_off-gsp_heap_sz, 0x100000)), gspFwWprStart=(wpr_st:=round_down(gsp_heap_off-0x1000, 0x100000)),
|
||||
nonWprHeapSize=(non_wpr_sz:=0x100000), nonWprHeapOffset=(non_wpr_off:=round_down(wpr_st-non_wpr_sz, 0x100000)), gspFwRsvdStart=non_wpr_off)
|
||||
assert self.nvdev.flcn.frts_offset == m.frtsOffset, f"FRTS mismatch: {self.nvdev.flcn.frts_offset} != {m.frtsOffset}"
|
||||
self.wpr_meta, _, wpr_meta_addrs = self.nvdev._alloc_boot_mem(ctypes.sizeof(type(m)), data=bytes(m))
|
||||
self.wpr_meta_sysmem = wpr_meta_addrs[0]
|
||||
|
||||
def promote_ctx(self, client:int, subdevice:int, obj:int, ctxbufs:dict[int, GRBufDesc], bufs=None, virt=None, phys=None):
|
||||
res, prom = {}, nv_gpu.NV2080_CTRL_GPU_PROMOTE_CTX_PARAMS(entryCount=len(ctxbufs), engineType=0x1, hChanClient=client, hObject=obj)
|
||||
for i,(buf,desc) in enumerate(ctxbufs.items()):
|
||||
use_v, use_p = (desc.virt if virt is None else virt), (desc.phys if phys is None else phys)
|
||||
x = (bufs or {}).get(buf, self.nvdev.mm.valloc(desc.size, contiguous=True)) # allocate buffers
|
||||
prom.promoteEntry[i] = nv_gpu.NV2080_CTRL_GPU_PROMOTE_CTX_BUFFER_ENTRY(bufferId=buf, gpuVirtAddr=x.va_addr if use_v else 0, bInitialize=use_p,
|
||||
gpuPhysAddr=x.paddrs[0][0] if use_p else 0, size=desc.size if use_p else 0, physAttr=0x4 if use_p else 0, bNonmapped=(use_p and not use_v))
|
||||
res[buf] = x
|
||||
self.rpc_rm_control(hObject=subdevice, cmd=nv_gpu.NV2080_CTRL_CMD_GPU_PROMOTE_CTX, params=prom, client=client)
|
||||
return res
|
||||
|
||||
def init_golden_image(self):
|
||||
self.rpc_rm_alloc(hParent=0x0, hClass=0x0, params=nv_gpu.NV0000_ALLOC_PARAMETERS())
|
||||
dev = self.rpc_rm_alloc(hParent=self.priv_root, hClass=nv_gpu.NV01_DEVICE_0, params=nv_gpu.NV0080_ALLOC_PARAMETERS(hClientShare=self.priv_root))
|
||||
subdev = self.rpc_rm_alloc(hParent=dev, hClass=nv_gpu.NV20_SUBDEVICE_0, params=nv_gpu.NV2080_ALLOC_PARAMETERS())
|
||||
vaspace = self.rpc_rm_alloc(hParent=dev, hClass=nv_gpu.FERMI_VASPACE_A, params=nv_gpu.NV_VASPACE_ALLOCATION_PARAMETERS())
|
||||
|
||||
# reserve 512MB for the reserved PDES
|
||||
res_va = self.nvdev.mm.alloc_vaddr(res_sz:=(512 << 20))
|
||||
|
||||
bufs_p = nv_gpu.struct_NV90F1_CTRL_VASPACE_COPY_SERVER_RESERVED_PDES_PARAMS(pageSize=res_sz, numLevelsToCopy=3,
|
||||
virtAddrLo=res_va, virtAddrHi=res_va + res_sz - 1)
|
||||
for i,pt in enumerate(self.nvdev.mm.page_tables(res_va, size=res_sz)):
|
||||
bufs_p.levels[i] = nv_gpu.struct_NV90F1_CTRL_VASPACE_COPY_SERVER_RESERVED_PDES_PARAMS_level(physAddress=pt.paddr,
|
||||
size=self.nvdev.mm.pte_cnt[0] * 8 if i == 0 else 0x1000, pageShift=self.nvdev.mm.pte_covers[i].bit_length() - 1, aperture=1)
|
||||
self.rpc_rm_control(hObject=vaspace, cmd=nv_gpu.NV90F1_CTRL_CMD_VASPACE_COPY_SERVER_RESERVED_PDES, params=bufs_p)
|
||||
|
||||
gpfifo_area = self.nvdev.mm.valloc(4 << 10, contiguous=True)
|
||||
userd = nv_gpu.NV_MEMORY_DESC_PARAMS(base=gpfifo_area.paddrs[0][0] + 0x20 * 8, size=0x20, addressSpace=2, cacheAttrib=0)
|
||||
gg_params = nv_gpu.NV_CHANNELGPFIFO_ALLOCATION_PARAMETERS(gpFifoOffset=gpfifo_area.va_addr, gpFifoEntries=32, engineType=0x1, cid=3,
|
||||
hVASpace=vaspace, userdOffset=(ctypes.c_uint64*8)(0x20 * 8), userdMem=userd, internalFlags=0x1a, flags=0x200320)
|
||||
ch_gpfifo = self.rpc_rm_alloc(hParent=dev, hClass=self.gpfifo_class, params=gg_params)
|
||||
|
||||
gr_ctx_bufs_info = self.rpc_rm_control(hObject=subdev, cmd=nv_gpu.NV2080_CTRL_CMD_INTERNAL_STATIC_KGR_GET_CONTEXT_BUFFERS_INFO,
|
||||
params=nv_gpu.NV2080_CTRL_INTERNAL_STATIC_KGR_GET_CONTEXT_BUFFERS_INFO_PARAMS()).engineContextBuffersInfo[0]
|
||||
def _ctx_info(idx, add=0, align=None): return round_up(gr_ctx_bufs_info.engine[idx].size + add, align or gr_ctx_bufs_info.engine[idx].alignment)
|
||||
|
||||
# Setup graphics context
|
||||
gr_size = _ctx_info(nv_gpu.NV0080_CTRL_FIFO_GET_ENGINE_CONTEXT_PROPERTIES_ENGINE_ID_GRAPHICS, add=0x40000)
|
||||
patch_size = _ctx_info(nv_gpu.NV0080_CTRL_FIFO_GET_ENGINE_CONTEXT_PROPERTIES_ENGINE_ID_GRAPHICS_PATCH)
|
||||
cfgs_sizes = {x: _ctx_info(x + 14, align=(2 << 20) if x == 5 else None) for x in range(3, 11)} # indices 3–10 are mapped to 17–24
|
||||
self.grctx_bufs = {0: GRBufDesc(gr_size, phys=True, virt=True), 1: GRBufDesc(patch_size, phys=True, virt=True, local=True),
|
||||
2: GRBufDesc(patch_size, phys=True, virt=True), **{x: GRBufDesc(cfgs_sizes[x], phys=False, virt=True) for x in range(3, 7)},
|
||||
9: GRBufDesc(cfgs_sizes[9], phys=True, virt=True), 10: GRBufDesc(cfgs_sizes[10], phys=True, virt=False),
|
||||
11: GRBufDesc(cfgs_sizes[10], phys=True, virt=True)} # NOTE: 11 reuses cfgs_sizes[10]
|
||||
self.promote_ctx(self.priv_root, subdev, ch_gpfifo, {k:v for k, v in self.grctx_bufs.items() if not v.local})
|
||||
|
||||
self.rpc_rm_alloc(hParent=ch_gpfifo, hClass=self.compute_class, params=None)
|
||||
self.rpc_rm_alloc(hParent=ch_gpfifo, hClass=self.dma_class, params=None)
|
||||
|
||||
def init_hw(self):
|
||||
self.stat_q = NVRpcQueue(self, self.stat_q_view, self.cmd_q_view)
|
||||
self.cmd_q.rx_view = self.stat_q_view.view(self.stat_q.tx.rxHdrOff, fmt='I')
|
||||
|
||||
self.stat_q.wait_resp(nv.NV_VGPU_MSG_EVENT_GSP_INIT_DONE)
|
||||
|
||||
self.nvdev.NV_PBUS_BAR1_BLOCK.write(mode=0, target=0, ptr=0)
|
||||
if self.nvdev.fmc_boot: self.nvdev.NV_VIRTUAL_FUNCTION_PRIV_FUNC_BAR1_BLOCK_LOW_ADDR.write(mode=0, target=0, ptr=0)
|
||||
|
||||
self.priv_root = 0xc1e00004
|
||||
self.init_golden_image()
|
||||
|
||||
def fini_hw(self): self.rpc_unloading_guest_driver()
|
||||
|
||||
### RPCs
|
||||
|
||||
def rpc_alloc_memory(self, hDevice:int, hClass:int, paddrs:list[tuple[int,int]], length:int, flags:int, client:int|None=None) -> int:
|
||||
assert all(sz == 0x1000 for _, sz in paddrs), f"all pages must be 4KB, got {[(hex(p), hex(sz)) for p, sz in paddrs]}"
|
||||
|
||||
rpc = nv.rpc_alloc_memory_v(hClient=(client:=client or self.priv_root), hDevice=hDevice, hMemory=(handle:=next(self.handle_gen)),
|
||||
hClass=hClass, flags=flags, pteAdjust=0, format=6, length=length, pageCount=len(paddrs))
|
||||
rpc.pteDesc.idr, rpc.pteDesc.length = nv.NV_VGPU_PTEDESC_IDR_NONE, (len(paddrs) & 0xffff)
|
||||
|
||||
payload = bytes(rpc) + b''.join(bytes(nv.struct_pte_desc_pte_pde(pte=(paddr >> 12))) for paddr, _ in paddrs)
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_ALLOC_MEMORY, bytes(payload))
|
||||
self.stat_q.wait_resp(nv.NV_VGPU_MSG_FUNCTION_ALLOC_MEMORY)
|
||||
return handle
|
||||
|
||||
def rpc_rm_alloc(self, hParent:int, hClass:int, params:Any, client=None) -> int:
|
||||
if hClass == self.gpfifo_class:
|
||||
ramfc_alloc = self.nvdev.mm.valloc(0x1000, contiguous=True)
|
||||
params.ramfcMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=ramfc_alloc.paddrs[0][0], size=0x200, addressSpace=2, cacheAttrib=0)
|
||||
params.instanceMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=ramfc_alloc.paddrs[0][0], size=0x1000, addressSpace=2, cacheAttrib=0)
|
||||
|
||||
_, method_paddr, _ = self.nvdev._alloc_boot_mem(0x5000, sysmem=False)
|
||||
params.mthdbufMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=method_paddr, size=0x5000, addressSpace=2, cacheAttrib=0)
|
||||
|
||||
if client is not None and client != self.priv_root and params.hObjectError != 0:
|
||||
params.errorNotifierMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=0, size=0xecc, addressSpace=0, cacheAttrib=0)
|
||||
params.userdMem = nv_gpu.NV_MEMORY_DESC_PARAMS(base=params.hUserdMemory[0] + params.userdOffset[0], size=0x400, addressSpace=2, cacheAttrib=0)
|
||||
|
||||
alloc_args = nv.rpc_gsp_rm_alloc_v(hClient=(client:=client or self.priv_root), hParent=hParent, hObject=(obj:=next(self.handle_gen)),
|
||||
hClass=hClass, flags=0x0, paramsSize=ctypes.sizeof(params) if params is not None else 0x0)
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_GSP_RM_ALLOC, bytes(alloc_args) + (bytes(params) if params is not None else b''))
|
||||
self.stat_q.wait_resp(nv.NV_VGPU_MSG_FUNCTION_GSP_RM_ALLOC)
|
||||
|
||||
if hClass == nv_gpu.FERMI_VASPACE_A and client != self.priv_root:
|
||||
self.rpc_set_page_directory(device=hParent, hVASpace=obj, pdir_paddr=self.nvdev.mm.root_page_table.paddr, client=client)
|
||||
if hClass == nv_gpu.NV01_DEVICE_0 and client != self.priv_root: self.device = obj # save user device handle
|
||||
if hClass == nv_gpu.NV20_SUBDEVICE_0: self.subdevice = obj # save subdevice handle
|
||||
if hClass == self.compute_class and client != self.priv_root:
|
||||
phys_gr_ctx = self.promote_ctx(client, self.subdevice, hParent, {k:v for k,v in self.grctx_bufs.items() if k in [0, 1, 2]}, virt=False)
|
||||
self.promote_ctx(client, self.subdevice, hParent, {k:v for k,v in self.grctx_bufs.items() if k in [0, 1, 2]}, phys_gr_ctx, phys=False)
|
||||
return obj if hClass != nv_gpu.NV1_ROOT else client
|
||||
|
||||
def rpc_rm_control(self, hObject:int, cmd:int, params:Any, client=None, extra=None):
|
||||
if cmd == nv_gpu.NVB0CC_CTRL_CMD_POWER_REQUEST_FEATURES:
|
||||
self.rpc_rm_control(hObject, nv_gpu.NVB0CC_CTRL_CMD_INTERNAL_PERMISSIONS_INIT, nv_gpu.NVB0CC_CTRL_INTERNAL_PERMISSIONS_INIT_PARAMS(
|
||||
bAdminProfilingPermitted=1, bDevProfilingPermitted=1, bCtxProfilingPermitted=1, bVideoMemoryProfilingPermitted=1,
|
||||
bSysMemoryProfilingPermitted=1), client=client)
|
||||
elif cmd == nv_gpu.NVB0CC_CTRL_CMD_ALLOC_PMA_STREAM:
|
||||
params.hMemPmaBuffer = self.rpc_alloc_memory(self.device, nv_gpu.NV01_MEMORY_LIST_SYSTEM, extra[0].meta.mapping.paddrs, extra[0].size,
|
||||
pma_flags:=(nv_gpu.NVOS02_FLAGS_PHYSICALITY_NONCONTIGUOUS << 4 | nv_gpu.NVOS02_FLAGS_MAPPING_NO_MAP << 30), client=client)
|
||||
params.hMemPmaBytesAvailable = self.rpc_alloc_memory(self.device, nv_gpu.NV01_MEMORY_LIST_SYSTEM, extra[1].meta.mapping.paddrs, extra[1].size,
|
||||
pma_flags | nv_gpu.NVOS02_FLAGS_ALLOC_USER_READ_ONLY_YES << 21, client=client)
|
||||
|
||||
control_args = nv.rpc_gsp_rm_control_v(hClient=(client:=client or self.priv_root), hObject=hObject, cmd=cmd, flags=0x0,
|
||||
paramsSize=ctypes.sizeof(params) if params is not None else 0x0)
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_GSP_RM_CONTROL, bytes(control_args) + (bytes(params) if params is not None else b''))
|
||||
res = self.stat_q.wait_resp(nv.NV_VGPU_MSG_FUNCTION_GSP_RM_CONTROL)
|
||||
st = type(params).from_buffer_copy(res[len(bytes(control_args)):]) if params is not None else None
|
||||
|
||||
# NOTE: gb20x requires the enable bit for token submission. Patch workSubmitToken here to maintain userspace compatibility.
|
||||
if self.nvdev.chip_name.startswith("GB2") and cmd == nv_gpu.NVC36F_CTRL_CMD_GPFIFO_GET_WORK_SUBMIT_TOKEN:
|
||||
cast(nv_gpu.NVC36F_CTRL_CMD_GPFIFO_GET_WORK_SUBMIT_TOKEN_PARAMS, st).workSubmitToken |= (1 << 30)
|
||||
return st
|
||||
|
||||
def rpc_set_page_directory(self, device:int, hVASpace:int, pdir_paddr:int, client=None, pasid=0xffffffff):
|
||||
params = nv.struct_NV0080_CTRL_DMA_SET_PAGE_DIRECTORY_PARAMS_v1E_05(physAddress=pdir_paddr,
|
||||
numEntries=self.nvdev.mm.pte_cnt[0], flags=0x8, hVASpace=hVASpace, pasid=pasid, subDeviceId=1, chId=0) # flags field is all channels.
|
||||
alloc_args = nv.rpc_set_page_directory_v(hClient=client or self.priv_root, hDevice=device, pasid=pasid, params=params)
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_SET_PAGE_DIRECTORY, bytes(alloc_args))
|
||||
self.stat_q.wait_resp(nv.NV_VGPU_MSG_FUNCTION_SET_PAGE_DIRECTORY)
|
||||
|
||||
def rpc_set_gsp_system_info(self):
|
||||
def bdf_as_int(s): return 0x000 if s.startswith("usb") or s.startswith("remote") else (int(s[5:7],16)<<8) | (int(s[8:10],16)<<3) | int(s[-1],16)
|
||||
|
||||
pcidev = self.nvdev.pci_dev
|
||||
data = nv.GspSystemInfo(gpuPhysAddr=pcidev.bar_info(0)[0], gpuPhysFbAddr=pcidev.bar_info(1)[0], gpuPhysInstAddr=pcidev.bar_info(3)[0],
|
||||
pciConfigMirrorBase=[0x88000, 0x92000][self.nvdev.fmc_boot], pciConfigMirrorSize=0x1000, nvDomainBusDeviceFunc=bdf_as_int(self.nvdev.devfmt),
|
||||
bIsPassthru=1, PCIDeviceID=pcidev.read_config(pci.PCI_VENDOR_ID, 4), PCISubDeviceID=pcidev.read_config(pci.PCI_SUBSYSTEM_VENDOR_ID, 4),
|
||||
PCIRevisionID=pcidev.read_config(pci.PCI_REVISION_ID, 1), maxUserVa=0x7ffffffff000)
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_GSP_SET_SYSTEM_INFO, bytes(data))
|
||||
|
||||
def rpc_unloading_guest_driver(self):
|
||||
data = nv.rpc_unloading_guest_driver_v(bInPMTransition=0, bGc6Entering=0, newLevel=(__GPU_STATE_FLAGS_FAST_UNLOAD:=1 << 6))
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_UNLOADING_GUEST_DRIVER, bytes(data))
|
||||
self.stat_q.wait_resp(nv.NV_VGPU_MSG_FUNCTION_UNLOADING_GUEST_DRIVER)
|
||||
|
||||
def rpc_set_registry_table(self):
|
||||
table = {'RMForcePcieConfigSave': 0x1, 'RMSecBusResetEnable': 0x1}
|
||||
entries_bytes, data_bytes = bytes(), bytes()
|
||||
hdr_size, entries_size = ctypes.sizeof(nv.PACKED_REGISTRY_TABLE), ctypes.sizeof(nv.PACKED_REGISTRY_ENTRY) * len(table)
|
||||
|
||||
for k,v in table.items():
|
||||
entries_bytes += bytes(nv.PACKED_REGISTRY_ENTRY(nameOffset=hdr_size + entries_size + len(data_bytes),
|
||||
type=nv.REGISTRY_TABLE_ENTRY_TYPE_DWORD, data=v, length=4))
|
||||
data_bytes += k.encode('utf-8') + b'\x00'
|
||||
|
||||
header = nv.PACKED_REGISTRY_TABLE(size=hdr_size + len(entries_bytes) + len(data_bytes), numEntries=len(table))
|
||||
self.cmd_q.send_rpc(nv.NV_VGPU_MSG_FUNCTION_SET_REGISTRY, bytes(header) + entries_bytes + data_bytes)
|
||||
|
||||
def run_cpu_seq(self, seq_buf:bytes):
|
||||
hdr = nv.rpc_run_cpu_sequencer_v17_00.from_buffer_copy(seq_buf[:(hdr_sz:=ctypes.sizeof(nv.rpc_run_cpu_sequencer_v17_00))])
|
||||
cmd_iter = iter(memoryview(seq_buf[hdr_sz:]).cast('I')[:hdr.cmdIndex])
|
||||
|
||||
for op in cmd_iter:
|
||||
if op == 0x0: self.nvdev.wreg(next(cmd_iter), next(cmd_iter)) # reg write
|
||||
elif op == 0x1: # reg modify
|
||||
addr, val, mask = next(cmd_iter), next(cmd_iter), next(cmd_iter)
|
||||
self.nvdev.wreg(addr, (self.nvdev.rreg(addr) & ~mask) | (val & mask))
|
||||
elif op == 0x2: # reg poll
|
||||
addr, mask, val, _, _ = next(cmd_iter), next(cmd_iter), next(cmd_iter), next(cmd_iter), next(cmd_iter)
|
||||
wait_cond(lambda a, m: (self.nvdev.rreg(a) & m), addr, mask, value=val, msg=f"Register {addr:#x} not equal to {val:#x} after polling")
|
||||
elif op == 0x3: time.sleep(next(cmd_iter) / 1e6) # delay us
|
||||
elif op == 0x4: # save reg
|
||||
addr, index = next(cmd_iter), next(cmd_iter)
|
||||
hdr.regSaveArea[index] = self.nvdev.rreg(addr)
|
||||
elif op == 0x5: # core reset
|
||||
self.nvdev.flcn.reset(self.nvdev.flcn.falcon)
|
||||
self.nvdev.flcn.disable_ctx_req(self.nvdev.flcn.falcon)
|
||||
elif op == 0x6: self.nvdev.flcn.start_cpu(self.nvdev.flcn.falcon)
|
||||
elif op == 0x7: self.nvdev.flcn.wait_cpu_halted(self.nvdev.flcn.falcon)
|
||||
elif op == 0x8: # core resume
|
||||
self.nvdev.flcn.reset(self.nvdev.flcn.falcon, riscv=True)
|
||||
|
||||
self.nvdev.NV_PGSP_FALCON_MAILBOX0.write(lo32(self.libos_args_sysmem))
|
||||
self.nvdev.NV_PGSP_FALCON_MAILBOX1.write(hi32(self.libos_args_sysmem))
|
||||
|
||||
self.nvdev.flcn.start_cpu(self.nvdev.flcn.sec2)
|
||||
wait_cond(lambda: self.nvdev.NV_PGC6_BSI_SECURE_SCRATCH_14.read_bitfields()['boot_stage_3_handoff'], msg="SEC2 didn't hand off")
|
||||
|
||||
mailbox = self.nvdev.NV_PFALCON_FALCON_MAILBOX0.with_base(self.nvdev.flcn.sec2).read()
|
||||
assert mailbox == 0x0, f"Falcon SEC2 failed to execute, mailbox is {mailbox:08x}"
|
||||
else: raise ValueError(f"Unknown op code {op} in run_cpu_seq")
|
||||
162
tinygrad_repo/tinygrad/runtime/support/nv/nvdev.py
Normal file
162
tinygrad_repo/tinygrad/runtime/support/nv/nvdev.py
Normal file
@@ -0,0 +1,162 @@
|
||||
from __future__ import annotations
|
||||
import time, functools, tinygrad.runtime.autogen.nv_regs
|
||||
from tinygrad.helpers import getenv, DEBUG, getbits, round_up
|
||||
from tinygrad.runtime.autogen import pci
|
||||
from tinygrad.runtime.support.memory import TLSFAllocator, MemoryManager, AddrSpace
|
||||
from tinygrad.runtime.support.nv.ip import NV_FLCN, NV_FLCN_COT, NV_GSP
|
||||
from tinygrad.runtime.support.system import PCIDevice, MMIOInterface
|
||||
|
||||
NV_DEBUG = getenv("NV_DEBUG", 0)
|
||||
|
||||
class NVReg:
|
||||
def __init__(self, nvdev, base, off, fields=None): self.nvdev, self.base, self.off, self.fields = nvdev, base, off, fields
|
||||
|
||||
def __getitem__(self, idx:int): return NVReg(self.nvdev, self.base, self.off(idx), fields=self.fields)
|
||||
|
||||
def add_field(self, name:str, start:int, end:int): self.fields[name] = (start, end)
|
||||
def with_base(self, base:int): return NVReg(self.nvdev, base + self.base, self.off, self.fields)
|
||||
|
||||
def read(self): return self.nvdev.rreg(self.base + self.off)
|
||||
def read_bitfields(self) -> dict[str, int]: return self.decode(self.read())
|
||||
|
||||
def write(self, _ini_val:int=0, **kwargs): self.nvdev.wreg(self.base + self.off, _ini_val | self.encode(**kwargs))
|
||||
|
||||
def update(self, **kwargs): self.write(self.read() & ~self.mask(*kwargs.keys()), **kwargs)
|
||||
|
||||
def mask(self, *names):
|
||||
return functools.reduce(int.__or__, ((((1 << (self.fields[nm][1]-self.fields[nm][0] + 1)) - 1) << self.fields[nm][0]) for nm in names), 0)
|
||||
|
||||
def encode(self, **kwargs) -> int: return functools.reduce(int.__or__, (value << self.fields[name][0] for name,value in kwargs.items()), 0)
|
||||
def decode(self, val: int) -> dict: return {name:getbits(val, start, end) for name,(start,end) in self.fields.items()}
|
||||
|
||||
class NVPageTableEntry:
|
||||
def __init__(self, nvdev, paddr, lv): self.nvdev, self.paddr, self.lv, self.entries = nvdev, paddr, lv, nvdev.vram.view(paddr, 0x1000, fmt='Q')
|
||||
|
||||
def _is_dual_pde(self) -> bool: return self.lv == self.nvdev.mm.level_cnt - 2
|
||||
|
||||
def set_entry(self, entry_id:int, paddr:int, table=False, uncached=False, aspace=AddrSpace.PHYS, snooped=False, frag=0, valid=True):
|
||||
if not table:
|
||||
x = self.nvdev.pte_t.encode(valid=valid, address_sys=paddr >> 12, aperture=2 if aspace is AddrSpace.SYS else 0, kind=6,
|
||||
**({'pcf': int(uncached)} if self.nvdev.mmu_ver == 3 else {'vol': uncached}))
|
||||
else:
|
||||
pde = self.nvdev.dual_pde_t if self._is_dual_pde() else self.nvdev.pde_t
|
||||
small, sys = ("_small" if self._is_dual_pde() else ""), "" if self.nvdev.mmu_ver == 3 else "_sys"
|
||||
x = pde.encode(is_pte=False, **{f'aperture{small}': 1 if valid else 0, f'address{small}{sys}': paddr >> 12},
|
||||
**({f'pcf{small}': 0b10} if self.nvdev.mmu_ver == 3 else {'no_ats': 1}))
|
||||
|
||||
if self._is_dual_pde(): self.entries[2*entry_id], self.entries[2*entry_id+1] = x & 0xffffffffffffffff, x >> 64
|
||||
else: self.entries[entry_id] = x
|
||||
|
||||
def entry(self, entry_id:int) -> int:
|
||||
return (self.entries[2*entry_id+1]<<64) | self.entries[2*entry_id] if self._is_dual_pde() else self.entries[entry_id]
|
||||
|
||||
def read_fields(self, entry_id:int) -> dict:
|
||||
if self.is_page(entry_id): return self.nvdev.pte_t.decode(self.entry(entry_id))
|
||||
return (self.nvdev.dual_pde_t if self._is_dual_pde() else self.nvdev.pde_t).decode(self.entry(entry_id))
|
||||
|
||||
def is_page(self, entry_id) -> bool: return (self.entry(entry_id) & 1 == 1) if self.lv < self.nvdev.mm.level_cnt - 1 else True
|
||||
def supports_huge_page(self, paddr:int): return self.lv >= self.nvdev.mm.level_cnt - 3 and paddr % self.nvdev.mm.pte_covers[self.lv] == 0
|
||||
|
||||
def valid(self, entry_id):
|
||||
if self.is_page(entry_id): return self.read_fields(entry_id)['valid']
|
||||
return self.read_fields(entry_id)['aperture_small' if self._is_dual_pde() else 'aperture'] != 0
|
||||
|
||||
def address(self, entry_id:int) -> int:
|
||||
small, sys = ("_small" if self._is_dual_pde() else ""), "_sys" if self.nvdev.mmu_ver == 2 or self.lv == self.nvdev.mm.level_cnt - 1 else ""
|
||||
return self.read_fields(entry_id)[f'address{small}{sys}'] << 12
|
||||
|
||||
class NVMemoryManager(MemoryManager):
|
||||
va_allocator = TLSFAllocator((1 << 44), base=0x1000000000) # global for all devices.
|
||||
|
||||
def on_range_mapped(self): self.dev.NV_VIRTUAL_FUNCTION_PRIV_MMU_INVALIDATE.write((1 << 0) | (1 << 1) | (1 << 6) | (1 << 31))
|
||||
|
||||
class NVDev:
|
||||
def __init__(self, pci_dev:PCIDevice):
|
||||
self.pci_dev, self.devfmt, self.mmio = pci_dev, pci_dev.pcibus, pci_dev.map_bar(0, fmt='I')
|
||||
|
||||
self.smi_dev, self.is_booting, self.is_err_state = False, True, False
|
||||
self._early_ip_init()
|
||||
self._early_mmu_init()
|
||||
|
||||
# No booting state, gsp client is reinited every run.
|
||||
self.is_booting = False
|
||||
|
||||
for ip in [self.flcn, self.gsp]: ip.init_sw()
|
||||
for ip in [self.flcn, self.gsp]: ip.init_hw()
|
||||
|
||||
def fini(self):
|
||||
for ip in [self.gsp, self.flcn]: ip.fini_hw()
|
||||
|
||||
def reg(self, reg:str) -> NVReg: return self.__dict__[reg]
|
||||
def wreg(self, addr:int, value:int):
|
||||
self.mmio[addr // 4] = value
|
||||
if NV_DEBUG >= 4: print(f"wreg: {hex(addr)} = {hex(value)}")
|
||||
def rreg(self, addr:int) -> int: return self.mmio[addr // 4]
|
||||
|
||||
def _early_ip_init(self):
|
||||
self.reg_names:set[str] = set()
|
||||
self.reg_offsets:dict[str, tuple[int, int]] = {}
|
||||
|
||||
self.include("nv_ref", "")
|
||||
self.include("dev_fb", "tu102")
|
||||
self.include("dev_gc6_island", "ga102")
|
||||
|
||||
if (needs_reset:=self.reg("NV_PFB_PRI_MMU_WPR2_ADDR_HI").read() != 0):
|
||||
self.pci_dev.write_config_flush(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) & ~pci.PCI_COMMAND_MASTER, 2)
|
||||
if DEBUG >= 2: print(f"nv {self.devfmt}: WPR2 is up. Issuing a full reset.", flush=True)
|
||||
self.pci_dev.reset()
|
||||
time.sleep(0.1) # wait until device can respond again
|
||||
|
||||
self.pci_dev.write_config_flush(pci.PCI_COMMAND, self.pci_dev.read_config(pci.PCI_COMMAND, 2) | pci.PCI_COMMAND_MASTER, 2)
|
||||
self.chip_id = self.reg("NV_PMC_BOOT_0").read()
|
||||
self.chip_details = self.reg("NV_PMC_BOOT_42").read_bitfields()
|
||||
self.chip_name = {0x17: "GA1", 0x19: "AD1", 0x1b: "GB2"}[self.chip_details['architecture']] + f"{self.chip_details['implementation']:02d}"
|
||||
self.fw_name = {"GB2": "gb202", "AD1": "ad102", "GA1": "ga102"}[self.chip_name[:3]]
|
||||
self.mmu_ver, self.fmc_boot = (3, True) if self.chip_details['architecture'] >= 0x1a else (2, False)
|
||||
|
||||
self.flcn:NV_FLCN|NV_FLCN_COT = NV_FLCN_COT(self) if self.fmc_boot else NV_FLCN(self)
|
||||
self.gsp:NV_GSP = NV_GSP(self)
|
||||
|
||||
if needs_reset: self.flcn.wait_for_reset()
|
||||
|
||||
def _early_mmu_init(self):
|
||||
self.include("dev_vm", "tu102")
|
||||
|
||||
# MMU Init
|
||||
self.include("dev_mmu", "gh100" if self.mmu_ver == 3 else "tu102")
|
||||
self.pte_t, self.pde_t, self.dual_pde_t = [self.__dict__[name] for name in [f'NV_MMU_VER{self.mmu_ver}_PTE', f'NV_MMU_VER{self.mmu_ver}_PDE',
|
||||
f'NV_MMU_VER{self.mmu_ver}_DUAL_PDE']]
|
||||
|
||||
self.vram_size = self.reg("NV_PGC6_AON_SECURE_SCRATCH_GROUP_42").read() << 20
|
||||
|
||||
self.vram, self.mmio = self.pci_dev.map_bar(1), self.pci_dev.map_bar(0, fmt='I')
|
||||
self.large_bar = self.vram.nbytes >= self.vram_size
|
||||
|
||||
# UVM depth HW level VA bits
|
||||
# 0 PDE4 56:56 (hopper+)
|
||||
# 1 PDE3 55:47
|
||||
# 2 PDE2 46:38
|
||||
# 3 PDE1 (or 512M PTE) 37:29
|
||||
# 4 PDE0 (dual 64k/4k PDE, or 2M PTE) 28:21
|
||||
# 5 PTE_64K / PTE_4K 20:16 / 20:12
|
||||
bits, shifts = (56, [12, 21, 29, 38, 47, 56]) if self.mmu_ver == 3 else (48, [12, 21, 29, 38, 47])
|
||||
|
||||
# tail vram reserved for falcon structs
|
||||
self.mm = NVMemoryManager(self, self.vram_size - (64 << 20), boot_size=(2 << 20), pt_t=NVPageTableEntry, va_bits=bits, va_shifts=shifts,
|
||||
va_base=0, palloc_ranges=[(x, x) for x in [512 << 20, 2 << 20, 4 << 10]], reserve_ptable=not self.large_bar)
|
||||
|
||||
def _alloc_boot_mem(self, size:int, data:bytes|None=None, contiguous:bool=False, sysmem:bool|None=None) -> tuple[MMIOInterface,int|None,list[int]]:
|
||||
sz = round_up(size, 0x1000)
|
||||
if sysmem is True or (sysmem is None and not self.large_bar):
|
||||
view, sysaddr = self.pci_dev.alloc_sysmem(size, 0, contiguous=contiguous)
|
||||
paddr = None
|
||||
else:
|
||||
paddr = self.mm.palloc(sz, boot=False)
|
||||
view = self.vram.view(paddr, sz)
|
||||
sysaddr = [self.pci_dev.bar_info(1)[0] + paddr + i * 0x1000 for i in range(sz // 0x1000)]
|
||||
if data is not None: view[:size] = data
|
||||
return view, paddr, sysaddr
|
||||
|
||||
def include(self, name:str, arch:str):
|
||||
for k,v in getattr(getattr(tinygrad.runtime.autogen.nv_regs, name), arch or 'regs').items():
|
||||
self.__dict__[k] = NVReg(self, *v) if isinstance(v, tuple) else v
|
||||
73
tinygrad_repo/tinygrad/runtime/support/objc.py
Normal file
73
tinygrad_repo/tinygrad/runtime/support/objc.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import ctypes, ctypes.util, functools, sys
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING: id_ = ctypes.c_void_p
|
||||
else:
|
||||
class id_(ctypes.c_void_p):
|
||||
_is_finalizing = sys.is_finalizing # FIXME: why is this needed
|
||||
|
||||
retain: bool = False
|
||||
# This prevents ctypes from converting response to plain int, and dict.fromkeys() can use it to dedup
|
||||
def __hash__(self): return hash(self.value)
|
||||
def __eq__(self, other): return self.value == other.value
|
||||
def __del__(self):
|
||||
if self.retain and not self._is_finalizing(): self.release()
|
||||
def release(self): msg("release")(self)
|
||||
def retained(self):
|
||||
setattr(self, 'retain', True)
|
||||
return self
|
||||
|
||||
def returns_retained(f): return functools.wraps(f)(lambda *args, **kwargs: f(*args, **kwargs).retained())
|
||||
|
||||
lib = ctypes.CDLL(ctypes.util.find_library('objc'))
|
||||
lib.sel_registerName.restype = id_
|
||||
getsel = functools.cache(lib.sel_registerName)
|
||||
lib.objc_getClass.restype = id_
|
||||
dispatch_data_create = ctypes.CDLL("/usr/lib/libSystem.dylib").dispatch_data_create
|
||||
dispatch_data_create.restype = id_
|
||||
dispatch_data_create = returns_retained(dispatch_data_create)
|
||||
|
||||
def msg(sel:str, restype=id_, argtypes=[], retain=False, clsmeth=False):
|
||||
# Using attribute access returns a new reference so setting restype is safe
|
||||
(sender:=lib["objc_msgSend"]).restype, sender.argtypes = restype, [id_, id_]+list(argtypes) if argtypes else []
|
||||
def f(ptr, *args): return sender(ptr._objc_class_ if clsmeth else ptr, getsel(sel.encode()), *args)
|
||||
return returns_retained(f) if retain else f
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import _ctypes
|
||||
class MetaSpec(_ctypes._PyCSimpleType):
|
||||
_objc_class_: id_
|
||||
def __getattr__(cls, nm:str) -> Any: ...
|
||||
def __setattr__(cls, nm:str, v:Any): ...
|
||||
else:
|
||||
class MetaSpec(type(id_)):
|
||||
def __new__(mcs, name, bases, dct):
|
||||
cls = super().__new__(mcs, name, bases, {'_objc_class_': lib.objc_getClass(name.encode()), '_children_': set(), **dct})
|
||||
cls._methods_, cls._classmethods_ = dct.get('_methods_', []), dct.get('_classmethods_', [])
|
||||
return cls
|
||||
|
||||
def __setattr__(cls, k, v):
|
||||
super().__setattr__(k, v)
|
||||
if k in ("_methods_", "_classmethods_"):
|
||||
for m in v: cls._addmeth(m, clsmeth=(k=="_classmethods_"))
|
||||
for c in cls._children_: c._inherit(cls)
|
||||
if k == "_bases_":
|
||||
for b in v:
|
||||
b._children_.add(cls)
|
||||
cls._inherit(b)
|
||||
|
||||
def _inherit(cls, b):
|
||||
for _b in getattr(b, "_bases_", []): cls._inherit(_b)
|
||||
for m in getattr(b, "_methods_", []): cls._addmeth(m)
|
||||
for m in getattr(b, "_classmethods_", []): cls._addmeth(m, True)
|
||||
for c in cls._children_: c._inherit(cls)
|
||||
|
||||
def _addmeth(cls, m, clsmeth=False):
|
||||
nm = m[0].strip(':').replace(':', '_')
|
||||
if clsmeth: setattr(cls, nm, classmethod(msg(m[0], cls if m[1] == 'instancetype' else m[1],
|
||||
[cls if a == 'instancetype' else a for a in m[2]], *m[3:], clsmeth=True))) # type: ignore[misc]
|
||||
else: setattr(cls, nm, msg(m[0], cls if m[1] == 'instancetype' else m[1], [cls if a == 'instancetype' else a for a in m[2]], *m[3:]))
|
||||
|
||||
class Spec(id_, metaclass=MetaSpec):
|
||||
if TYPE_CHECKING:
|
||||
def __getattr__(self, nm:str) -> Any: ...
|
||||
438
tinygrad_repo/tinygrad/runtime/support/system.py
Normal file
438
tinygrad_repo/tinygrad/runtime/support/system.py
Normal file
@@ -0,0 +1,438 @@
|
||||
from __future__ import annotations
|
||||
import os, mmap, array, functools, ctypes, select, contextlib, dataclasses, sys, itertools, struct, socket, subprocess, time, enum, atexit
|
||||
from tinygrad.helpers import round_up, getenv, OSX, temp, ceildiv, unwrap, fetch, system, _ensure_downloads_dir, DEBUG, flatten, pluralize
|
||||
from tinygrad.runtime.autogen import libc, pci, vfio, iokit, corefoundation
|
||||
from tinygrad.runtime.support.hcq import FileIOInterface, MMIOInterface, HCQBuffer, hcq_filter_visible_devices
|
||||
from tinygrad.runtime.support.memory import VirtMapping, AddrSpace, BumpAllocator
|
||||
from tinygrad.runtime.support.usb import USB3, CustomASM24Controller, ASM24Controller, USBMMIOInterface
|
||||
|
||||
MAP_FIXED, MAP_FIXED_NOREPLACE = 0x10, 0x100000
|
||||
MAP_LOCKED, MAP_POPULATE, MAP_NORESERVE = 0 if OSX else 0x2000, getattr(mmap, "MAP_POPULATE", 0 if OSX else 0x008000), 0x400
|
||||
|
||||
class _System:
|
||||
def write_sysfs(self, path:str, value:str, msg:str, expected:str|None=None):
|
||||
if FileIOInterface(path, os.O_RDONLY).read().splitlines()[0] != (expected or value):
|
||||
os.system(cmd:=f"sudo sh -c 'echo {value} > {path}'")
|
||||
if FileIOInterface(path, os.O_RDONLY).read().splitlines()[0] != (expected or value): raise RuntimeError(f"{msg}. Please run {cmd} manually.")
|
||||
|
||||
@functools.cached_property
|
||||
def atomic_lib(self): return ctypes.CDLL(ctypes.util.find_library('atomic')) if sys.platform == "linux" else None
|
||||
|
||||
@functools.cached_property
|
||||
def libsys(self): return ctypes.CDLL(ctypes.util.find_library("System"))
|
||||
|
||||
@functools.cached_property
|
||||
def pagemap(self) -> FileIOInterface:
|
||||
self.write_sysfs("/proc/sys/vm/compact_unevictable_allowed", "0", "Failed to disable migration of locked pages")
|
||||
return FileIOInterface("/proc/self/pagemap", os.O_RDONLY)
|
||||
|
||||
@functools.cached_property
|
||||
def vfio(self) -> FileIOInterface|None:
|
||||
try:
|
||||
if not FileIOInterface.exists("/sys/module/vfio"): os.system("sudo modprobe vfio-pci disable_idle_d3=1")
|
||||
|
||||
FileIOInterface("/sys/module/vfio/parameters/enable_unsafe_noiommu_mode", os.O_RDWR).write("1")
|
||||
vfio_fd = FileIOInterface("/dev/vfio/vfio", os.O_RDWR)
|
||||
vfio.VFIO_CHECK_EXTENSION(vfio_fd, vfio.VFIO_NOIOMMU_IOMMU)
|
||||
|
||||
return vfio_fd
|
||||
except OSError: return None
|
||||
|
||||
@functools.cache
|
||||
def reserve_va(self, va_start, va_size):
|
||||
# cached, runs only once per range. used to not collide with other mappings.
|
||||
FileIOInterface.anon_mmap(va_start, va_size, 0, mmap.MAP_PRIVATE | mmap.MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, 0)
|
||||
|
||||
def memory_barrier(self): lib.atomic_thread_fence(__ATOMIC_SEQ_CST:=5) if (lib:=self.libsys if OSX else self.atomic_lib) is not None else None
|
||||
|
||||
def lock_memory(self, addr:int, size:int):
|
||||
if libc.mlock(ctypes.c_void_p(addr), size): raise RuntimeError(f"Failed to lock memory at {addr:#x} with size {size:#x}")
|
||||
|
||||
def system_paddrs(self, vaddr:int, size:int) -> list[int]:
|
||||
self.pagemap.seek(vaddr // mmap.PAGESIZE * 8)
|
||||
return [(x & ((1<<55) - 1)) * mmap.PAGESIZE for x in array.array('Q', self.pagemap.read(size//mmap.PAGESIZE*8, binary=True))]
|
||||
|
||||
def pci_scan_bus(self, vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None=None) -> list[str]:
|
||||
all_devs = []
|
||||
if OSX:
|
||||
def read_prop(svc, key) -> int:
|
||||
cfkey = corefoundation.CFStringCreateWithCString(None, key.encode(), corefoundation.kCFStringEncodingUTF8)
|
||||
cfdata = ctypes.cast(iokit.IORegistryEntryCreateCFProperty(svc, ctypes.cast(cfkey, iokit.CFStringRef), None, 0), corefoundation.CFDataRef)
|
||||
corefoundation.CFDataGetBytes(cfdata, corefoundation.CFRange(0, corefoundation.CFDataGetLength(cfdata)), buf:=(ctypes.c_uint8*8)())
|
||||
return int.from_bytes(bytes(buf), "little")
|
||||
|
||||
iokit.IOServiceGetMatchingServices(0, iokit.IOServiceMatching(b"IOPCIDevice"), ctypes.byref(iterator:=ctypes.c_uint()))
|
||||
while svc:=iokit.IOIteratorNext(iterator):
|
||||
if base_class is not None and read_prop(svc, "class-code") >> 16 != base_class: continue
|
||||
all_devs.append((v:=read_prop(svc, "vendor-id"), d:=read_prop(svc, "device-id"), f"{v:x}:{d:x}"))
|
||||
else:
|
||||
try: devs = FileIOInterface("/sys/bus/pci/devices")
|
||||
except FileNotFoundError: raise RuntimeError("no pcie")
|
||||
for pcibus in devs.listdir():
|
||||
if base_class is not None and int(FileIOInterface(f"/sys/bus/pci/devices/{pcibus}/class").read(), 16) >> 16 != base_class: continue
|
||||
all_devs.append((int(FileIOInterface(f"/sys/bus/pci/devices/{pcibus}/vendor").read(), 16),
|
||||
int(FileIOInterface(f"/sys/bus/pci/devices/{pcibus}/device").read(), 16), pcibus))
|
||||
|
||||
return sorted([val for vndr, device, val in all_devs if vndr == vendor and any((device & mask) in devlist for mask, devlist in devices)])
|
||||
|
||||
@functools.cache
|
||||
def list_devices(self, vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None=None):
|
||||
if getenv("REMOTE", ""): return [(functools.partial(RemotePCIDevice,sock=s), x) for s,x in RemotePCIDevice.remote_list(vendor,devices,base_class)]
|
||||
return [(APLRemotePCIDevice if OSX else PCIDevice, x) for x in System.pci_scan_bus(vendor, devices, base_class)]
|
||||
|
||||
def pci_probe_device(self, device:str, dev_id:int, vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None=None):
|
||||
try: cl, pcibus = (ds:=hcq_filter_visible_devices(self.list_devices(vendor, devices, base_class), device))[dev_id]
|
||||
except IndexError: raise RuntimeError(f"{device}:{dev_id} does not exist ({pluralize('device', len(ds))} available)")
|
||||
return cl(device[:2], pcibus)
|
||||
|
||||
def pci_setup_usb_bars(self, usb:CustomASM24Controller|ASM24Controller, gpu_bus:int, mem_base:int, pref_mem_base:int) -> dict[int, tuple[int, int]]:
|
||||
for bus in range(gpu_bus):
|
||||
# All 3 values must be written at the same time.
|
||||
buses = (0 << 0) | ((bus+1) << 8) | ((gpu_bus) << 16)
|
||||
usb.pcie_cfg_req(pci.PCI_PRIMARY_BUS, bus=bus, dev=0, fn=0, value=buses, size=4)
|
||||
|
||||
usb.pcie_cfg_req(pci.PCI_MEMORY_BASE, bus=bus, dev=0, fn=0, value=(mem_base>>16) & 0xffff, size=2)
|
||||
usb.pcie_cfg_req(pci.PCI_MEMORY_LIMIT, bus=bus, dev=0, fn=0, value=0xffff, size=2)
|
||||
usb.pcie_cfg_req(pci.PCI_PREF_MEMORY_BASE, bus=bus, dev=0, fn=0, value=(pref_mem_base>>16) & 0xffff, size=2)
|
||||
usb.pcie_cfg_req(pci.PCI_PREF_MEMORY_LIMIT, bus=bus, dev=0, fn=0, value=0xffff, size=2)
|
||||
usb.pcie_cfg_req(pci.PCI_PREF_BASE_UPPER32, bus=bus, dev=0, fn=0, value=pref_mem_base >> 32, size=4)
|
||||
usb.pcie_cfg_req(pci.PCI_PREF_LIMIT_UPPER32, bus=bus, dev=0, fn=0, value=0xffffffff, size=4)
|
||||
|
||||
usb.pcie_cfg_req(pci.PCI_COMMAND, bus=bus, dev=0, fn=0, value=pci.PCI_COMMAND_IO | pci.PCI_COMMAND_MEMORY | pci.PCI_COMMAND_MASTER, size=1)
|
||||
|
||||
# resize bar 0
|
||||
cap_ptr = 0x100
|
||||
while cap_ptr:
|
||||
if pci.PCI_EXT_CAP_ID(hdr:=usb.pcie_cfg_req(cap_ptr, bus=gpu_bus, dev=0, fn=0, size=4)) == pci.PCI_EXT_CAP_ID_REBAR:
|
||||
cap = usb.pcie_cfg_req(cap_ptr + 0x04, bus=gpu_bus, dev=0, fn=0, size=4)
|
||||
new_ctrl = (usb.pcie_cfg_req(cap_ptr + 0x08, bus=gpu_bus, dev=0, fn=0, size=4) & ~0x1F00) | ((int(cap >> 4).bit_length() - 1) << 8)
|
||||
usb.pcie_cfg_req(cap_ptr + 0x08, bus=gpu_bus, dev=0, fn=0, value=new_ctrl, size=4)
|
||||
|
||||
cap_ptr = pci.PCI_EXT_CAP_NEXT(hdr)
|
||||
|
||||
mem_space_addr, bar_off, bars = [mem_base, pref_mem_base], 0, {}
|
||||
while bar_off < 24:
|
||||
cfg = usb.pcie_cfg_req(pci.PCI_BASE_ADDRESS_0 + bar_off, bus=gpu_bus, dev=0, fn=0, size=4)
|
||||
bar_mem, bar_64 = bool(cfg & pci.PCI_BASE_ADDRESS_MEM_PREFETCH), cfg & pci.PCI_BASE_ADDRESS_MEM_TYPE_64
|
||||
|
||||
if (cfg & pci.PCI_BASE_ADDRESS_SPACE) == pci.PCI_BASE_ADDRESS_SPACE_MEMORY:
|
||||
usb.pcie_cfg_req(pci.PCI_BASE_ADDRESS_0 + bar_off, bus=gpu_bus, dev=0, fn=0, value=0xffffffff, size=4)
|
||||
lo = (usb.pcie_cfg_req(pci.PCI_BASE_ADDRESS_0 + bar_off, bus=gpu_bus, dev=0, fn=0, size=4) & 0xfffffff0)
|
||||
|
||||
if bar_64: usb.pcie_cfg_req(pci.PCI_BASE_ADDRESS_0 + bar_off + 4, bus=gpu_bus, dev=0, fn=0, value=0xffffffff, size=4)
|
||||
hi = (usb.pcie_cfg_req(pci.PCI_BASE_ADDRESS_0 + bar_off + 4, bus=gpu_bus, dev=0, fn=0, size=4) if bar_64 else 0)
|
||||
|
||||
bar_size = ((~(((hi << 32) | lo) & ~0xf)) + 1) & (0xffffffffffffffff if bar_64 else 0xffffffff)
|
||||
|
||||
usb.pcie_cfg_req(pci.PCI_BASE_ADDRESS_0 + bar_off, bus=gpu_bus, dev=0, fn=0, value=mem_space_addr[bar_mem] & 0xffffffff, size=4)
|
||||
if bar_64: usb.pcie_cfg_req(pci.PCI_BASE_ADDRESS_0 + bar_off + 4, bus=gpu_bus, dev=0, fn=0, value=mem_space_addr[bar_mem] >> 32, size=4)
|
||||
|
||||
bars[bar_off // 4] = (mem_space_addr[bar_mem], bar_size)
|
||||
mem_space_addr[bar_mem] += round_up(bar_size, 2 << 20)
|
||||
|
||||
bar_off += 8 if bar_64 else 4
|
||||
|
||||
usb.pcie_cfg_req(pci.PCI_COMMAND, bus=gpu_bus, dev=0, fn=0, value=pci.PCI_COMMAND_IO | pci.PCI_COMMAND_MEMORY | pci.PCI_COMMAND_MASTER, size=1)
|
||||
return bars
|
||||
|
||||
def flock_acquire(self, name:str) -> int:
|
||||
import fcntl # to support windows
|
||||
|
||||
os.umask(0) # Set umask to 0 to allow creating files with 0666 permissions
|
||||
|
||||
# Avoid O_CREAT because we don’t want to re-create/replace an existing file (triggers extra perms checks) when opening as non-owner.
|
||||
if os.path.exists(lock_name:=temp(name)): self.lock_fd = os.open(lock_name, os.O_RDWR)
|
||||
else: self.lock_fd = os.open(lock_name, os.O_RDWR | os.O_CREAT | os.O_CLOEXEC, 0o666)
|
||||
|
||||
try: fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError: raise RuntimeError(f"Failed to acquire lock file {name}. `sudo lsof {lock_name}` may help identify the process holding the lock.")
|
||||
|
||||
return self.lock_fd
|
||||
|
||||
System = _System()
|
||||
|
||||
# *** PCI Devices
|
||||
|
||||
class PCIDevice:
|
||||
def __init__(self, devpref:str, pcibus:str):
|
||||
self.lock_fd = System.flock_acquire(f"{devpref.lower()}_{pcibus.lower()}.lock")
|
||||
self.pcibus, self.irq_poller = pcibus, None
|
||||
|
||||
try: FileIOInterface(f"/sys/bus/pci/devices/{self.pcibus}/enable", os.O_RDWR)
|
||||
except PermissionError: raise PermissionError(f"Cannot access PCI device {pcibus}: run `extra/amdpci/setup_python_cap.sh` or use sudo")
|
||||
|
||||
if FileIOInterface.exists(f"/sys/bus/pci/devices/{self.pcibus}/driver"):
|
||||
FileIOInterface(f"/sys/bus/pci/devices/{self.pcibus}/driver/unbind", os.O_WRONLY).write(self.pcibus)
|
||||
|
||||
if getenv("VFIO", 0) and (vfio_fd:=System.vfio) is not None:
|
||||
FileIOInterface(f"/sys/bus/pci/devices/{self.pcibus}/driver_override", os.O_WRONLY).write("vfio-pci")
|
||||
FileIOInterface("/sys/bus/pci/drivers_probe", os.O_WRONLY).write(self.pcibus)
|
||||
iommu_group = FileIOInterface.readlink(f"/sys/bus/pci/devices/{self.pcibus}/iommu_group").split('/')[-1]
|
||||
|
||||
self.vfio_group = FileIOInterface(f"/dev/vfio/noiommu-{iommu_group}", os.O_RDWR)
|
||||
vfio.VFIO_GROUP_SET_CONTAINER(self.vfio_group, ctypes.c_int(vfio_fd.fd))
|
||||
|
||||
with contextlib.suppress(OSError): vfio.VFIO_SET_IOMMU(vfio_fd, vfio.VFIO_NOIOMMU_IOMMU) # set iommu works only once for the fd.
|
||||
self.vfio_dev = FileIOInterface(fd=vfio.VFIO_GROUP_GET_DEVICE_FD(self.vfio_group, ctypes.create_string_buffer(self.pcibus.encode())))
|
||||
|
||||
self.irq_fd = FileIOInterface.eventfd(0, 0)
|
||||
self.irq_poller = select.poll()
|
||||
self.irq_poller.register(self.irq_fd.fd, select.POLLIN)
|
||||
|
||||
irqs = vfio.struct_vfio_irq_set(index=vfio.VFIO_PCI_MSI_IRQ_INDEX, flags=vfio.VFIO_IRQ_SET_DATA_EVENTFD|vfio.VFIO_IRQ_SET_ACTION_TRIGGER,
|
||||
argsz=ctypes.sizeof(vfio.struct_vfio_irq_set) + ctypes.sizeof(ctypes.c_int), count=1)
|
||||
vfio.VFIO_DEVICE_SET_IRQS(self.vfio_dev, (ctypes.c_byte * irqs.argsz).from_buffer(bytearray(bytes(irqs)) + struct.pack('i', self.irq_fd.fd)))
|
||||
else: FileIOInterface(f"/sys/bus/pci/devices/{self.pcibus}/enable", os.O_RDWR).write("1")
|
||||
|
||||
self.cfg_fd = FileIOInterface(f"/sys/bus/pci/devices/{self.pcibus}/config", os.O_RDWR | os.O_SYNC | os.O_CLOEXEC)
|
||||
|
||||
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
|
||||
assert not contiguous or size <= (2 << 20), "Contiguous allocation is only supported for sizes up to 2MB"
|
||||
flags = (libc.MAP_HUGETLB if contiguous and (size:=round_up(size, mmap.PAGESIZE)) > mmap.PAGESIZE else 0) | (MAP_FIXED if vaddr else 0)
|
||||
va = FileIOInterface.anon_mmap(vaddr, size, mmap.PROT_READ|mmap.PROT_WRITE, mmap.MAP_SHARED|mmap.MAP_ANONYMOUS|MAP_POPULATE|MAP_LOCKED|flags, 0)
|
||||
sysmem_view, paddrs = MMIOInterface(va, size), [(x, mmap.PAGESIZE) for x in System.system_paddrs(va, size)]
|
||||
return sysmem_view, [p + i for p, sz in paddrs for i in range(0, sz, 0x1000)][:ceildiv(size, 0x1000)]
|
||||
|
||||
def reset(self): os.system(f"sudo sh -c 'echo 1 > /sys/bus/pci/devices/{self.pcibus}/reset'")
|
||||
def read_config(self, offset:int, size:int): return int.from_bytes(self.cfg_fd.read(size, binary=True, offset=offset), byteorder='little')
|
||||
def write_config(self, offset:int, value:int, size:int): self.cfg_fd.write(value.to_bytes(size, byteorder='little'), binary=True, offset=offset)
|
||||
def write_config_flush(self, offset:int, value:int, size:int):
|
||||
self.write_config(offset, value, size)
|
||||
self.read_config(offset, size)
|
||||
|
||||
@functools.cache
|
||||
def bar_fd(self, bar_idx:int) -> FileIOInterface:
|
||||
return FileIOInterface(f"/sys/bus/pci/devices/{self.pcibus}/resource{bar_idx}", os.O_RDWR | os.O_SYNC | os.O_CLOEXEC)
|
||||
@functools.cache
|
||||
def bar_info(self, bar_idx:int) -> tuple[int, int]:
|
||||
s, e, _ = FileIOInterface(f"/sys/bus/pci/devices/{self.pcibus}/resource", os.O_RDONLY).read().splitlines()[bar_idx].split()
|
||||
return (int(s, 16), int(e, 16) - int(s, 16) + 1)
|
||||
def map_bar(self, bar:int, off:int=0, addr:int=0, size:int|None=None, fmt='B') -> MMIOInterface:
|
||||
fd, sz = self.bar_fd(bar), size or (self.bar_info(bar)[1] - off)
|
||||
libc.madvise(loc:=fd.mmap(addr, sz, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED | (MAP_FIXED if addr else 0), off), sz, libc.MADV_DONTFORK)
|
||||
return MMIOInterface(loc, sz, fmt=fmt)
|
||||
def resize_bar(self, bar_idx:int):
|
||||
rpath = f"/sys/bus/pci/devices/{self.pcibus}/resource{bar_idx}_resize"
|
||||
try: FileIOInterface(rpath, os.O_RDWR).write(str(int(FileIOInterface(rpath, os.O_RDONLY).read(), 16).bit_length() - 1))
|
||||
except OSError as e: raise RuntimeError(f"Cannot resize BAR {bar_idx}: {e}. Ensure the resizable BAR option is enabled.") from e
|
||||
|
||||
class USBPCIDevice(PCIDevice):
|
||||
def __init__(self, devpref:str, dev, pcibus):
|
||||
self.pcibus, self.peer_group = pcibus, f"USBPCIDevice_{pcibus}"
|
||||
self.lock_fd = System.flock_acquire(f"{devpref.lower()}_{pcibus.lower()}.lock")
|
||||
usb = USB3(dev, 0x81, 0x83, 0x02, 0x04)
|
||||
if DEBUG >= 1: print(f"am {self.pcibus}: product string: {usb.product!r}")
|
||||
self.usb: CustomASM24Controller | ASM24Controller = CustomASM24Controller(usb) if usb.is_custom else ASM24Controller(usb)
|
||||
self._bar_info = System.pci_setup_usb_bars(self.usb, gpu_bus=4, mem_base=0x10000000, pref_mem_base=(32 << 30))
|
||||
self.sram = BumpAllocator(size=0x80000, wrap=False) # asm24 controller sram
|
||||
|
||||
def dma_view(self, ctrl_addr, size): return USBMMIOInterface(self.usb, ctrl_addr, size, fmt='B', pcimem=False)
|
||||
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
|
||||
return self.dma_view(0xf000 + (off:=self.sram.alloc(size)), size), [0x200000 + off]
|
||||
|
||||
def read_config(self, offset:int, size:int): return self.usb.pcie_cfg_req(offset, bus=4, dev=0, fn=0, size=size)
|
||||
def write_config(self, offset:int, value:int, size:int): self.usb.pcie_cfg_req(offset, bus=4, dev=0, fn=0, value=value, size=size)
|
||||
|
||||
def bar_info(self, bar_idx:int) -> tuple[int, int]: return self._bar_info[bar_idx] # type: ignore[override]
|
||||
def map_bar(self, bar, off=0, addr=0, size=None, fmt='B'):
|
||||
return USBMMIOInterface(self.usb, self.bar_info(bar)[0] + off, size or self.bar_info(bar)[1], fmt)
|
||||
def resize_bar(self, bar_idx:int): pass # already resized
|
||||
|
||||
@dataclasses.dataclass
|
||||
class PCIAllocationMeta: mapping:VirtMapping; has_cpu_mapping:bool; hMemory:int=0 # noqa: E702
|
||||
|
||||
class PCIIfaceBase:
|
||||
@property
|
||||
def peer_group(self) -> str: return getattr(self.pci_dev, 'peer_group', type(self.pci_dev).__name__)
|
||||
def is_local(self) -> bool: return not isinstance(self.pci_dev, RemotePCIDevice)
|
||||
def is_bar_small(self) -> bool: return self.pci_dev.bar_info(self.vram_bar)[1] == (256 << 20)
|
||||
|
||||
def __init__(self, dev, dev_id, vendor, devices:tuple[tuple[int, tuple[int, ...]], ...], vram_bar, va_start, va_size,
|
||||
dev_impl_t, base_class:int|None=None):
|
||||
self.pci_dev = System.pci_probe_device(dn:=dev.__class__.__name__[:-6], dev_id, vendor, devices, base_class=base_class)
|
||||
if self.is_local(): System.reserve_va(va_start, va_size)
|
||||
with contextlib.suppress(Exception): self.pci_dev.resize_bar(vram_bar)
|
||||
self.dev_impl = dev_impl_t(self.pci_dev)
|
||||
self.dev, self.vram_bar, self.count = dev, vram_bar, len(hcq_filter_visible_devices(System.list_devices(vendor, devices, base_class), dn))
|
||||
|
||||
def alloc(self, size:int, host=False, uncached=False, cpu_access=False, contiguous=False, force_devmem=False, **kwargs) -> HCQBuffer:
|
||||
should_use_sysmem = host or ((cpu_access if self.is_bar_small() else (uncached and cpu_access)) and not force_devmem)
|
||||
|
||||
# Align size to huge pages for large allocations, otherwise the unaligned tail falls back to 4KB pages, increasing TLB pressure.
|
||||
size = round_up(size, mmap.PAGESIZE if should_use_sysmem else ((2 << 20) if size >= (8 << 20) else (4 << 10)))
|
||||
|
||||
if should_use_sysmem:
|
||||
vaddr = self.dev_impl.mm.alloc_vaddr(size:=round_up(size, mmap.PAGESIZE), align=mmap.PAGESIZE)
|
||||
memview, paddrs = self.pci_dev.alloc_sysmem(size, vaddr=vaddr, contiguous=contiguous)
|
||||
mapping = self.dev_impl.mm.map_range(vaddr, size, [(paddr, 0x1000) for paddr in paddrs], aspace=AddrSpace.SYS, snooped=True, uncached=True)
|
||||
return HCQBuffer(vaddr, size, meta=PCIAllocationMeta(mapping, has_cpu_mapping=True, hMemory=paddrs[0]), view=memview, owner=self.dev)
|
||||
|
||||
mapping = self.dev_impl.mm.valloc(size:=round_up(size, 0x1000), uncached=uncached, contiguous=cpu_access)
|
||||
barview = self.pci_dev.map_bar(bar=self.vram_bar, off=mapping.paddrs[0][0], size=mapping.size) if cpu_access else None
|
||||
return HCQBuffer(mapping.va_addr, size, view=barview, meta=PCIAllocationMeta(mapping, cpu_access, hMemory=mapping.paddrs[0][0]), owner=self.dev)
|
||||
|
||||
def free(self, b:HCQBuffer):
|
||||
if b.owner != self.dev: self.dev.iface.dev_impl.mm.unmap_range(b.va_addr, b.size)
|
||||
if b.owner == self.dev and b.meta.mapping.aspace is AddrSpace.PHYS: self.dev_impl.mm.vfree(b.meta.mapping)
|
||||
if b.owner == self.dev and self.is_local() and b.meta.has_cpu_mapping: FileIOInterface.munmap(b.va_addr, b.size)
|
||||
|
||||
def p2p_paddrs(self, paddrs:list[tuple[int,int]]) -> tuple[list[tuple[int,int]], AddrSpace]:
|
||||
return [(p + self.pci_dev.bar_info(self.vram_bar)[0], sz) for p, sz in paddrs], AddrSpace.SYS
|
||||
|
||||
def map(self, b:HCQBuffer):
|
||||
if b.owner is not None and b.owner._is_cpu():
|
||||
if not self.is_local(): raise RuntimeError(f"P2P mapping not supported for remote devices: {b.owner} -> {self.dev}")
|
||||
|
||||
System.lock_memory(int(b.va_addr), b.size)
|
||||
paddrs, aspace = [(x, 0x1000) for x in System.system_paddrs(int(b.va_addr), round_up(b.size, 0x1000))], AddrSpace.SYS
|
||||
snooped, uncached = True, True
|
||||
elif (ifa:=getattr(b.owner, "iface", None)) is not None and isinstance(ifa, PCIIfaceBase):
|
||||
if ifa.is_bar_small(): raise RuntimeError(f"P2P mapping not supported for small bar devices: {b.owner} -> {self.dev}")
|
||||
|
||||
snooped, uncached = True, b.meta.mapping.uncached
|
||||
if b.meta.mapping.aspace is AddrSpace.SYS: paddrs, aspace = b.meta.mapping.paddrs, AddrSpace.SYS
|
||||
else: paddrs, aspace = ifa.p2p_paddrs(b.meta.mapping.paddrs)
|
||||
else: raise RuntimeError(f"map failed: {b.owner} -> {self.dev}")
|
||||
|
||||
self.dev_impl.mm.map_range(int(b.va_addr), round_up(b.size, 0x1000), paddrs, aspace=aspace, snooped=snooped, uncached=uncached)
|
||||
return HCQBuffer(b.va_addr, b.size, meta=b.meta, owner=b.owner)
|
||||
|
||||
# *** Remote PCI Devices
|
||||
|
||||
class RemoteCmd(enum.IntEnum):
|
||||
PROBE,MAP_BAR,MAP_SYSMEM_FD,CFG_READ,CFG_WRITE,RESET,MMIO_READ,MMIO_WRITE,MAP_SYSMEM,SYSMEM_READ,SYSMEM_WRITE,RESIZE_BAR,PING = range(13)
|
||||
|
||||
class RemoteMMIOInterface(MMIOInterface):
|
||||
def __init__(self, dev:RemotePCIDevice, residx:int, nbytes:int, fmt='B', off=0, rd_cmd=RemoteCmd.MMIO_READ, wr_cmd=RemoteCmd.MMIO_WRITE):
|
||||
self.dev, self.residx, self.nbytes, self.fmt, self.off, self.el_sz = dev, residx, nbytes, fmt, off, struct.calcsize(fmt)
|
||||
self.rd_cmd, self.wr_cmd = rd_cmd, wr_cmd
|
||||
|
||||
def __getitem__(self, index):
|
||||
sl = index if isinstance(index, slice) else slice(index, index + 1)
|
||||
start, stop = (sl.start or 0) * self.el_sz, (sl.stop or len(self)) * self.el_sz
|
||||
data = self.dev._bulk_read(self.rd_cmd, self.residx, self.off + start, stop - start)
|
||||
result = data if self.fmt == 'B' else list(struct.unpack(f'<{(stop - start) // self.el_sz}{self.fmt}', data))
|
||||
return result if isinstance(index, slice) else result[0]
|
||||
|
||||
def __setitem__(self, index, val):
|
||||
start = (index.start or 0) * self.el_sz if isinstance(index, slice) else index * self.el_sz
|
||||
data = (val if self.fmt == 'B' else struct.pack(f'<{len(val)}{self.fmt}', *val)) if isinstance(index, slice) else struct.pack(f'<{self.fmt}', val)
|
||||
self.dev._bulk_write(self.wr_cmd, self.residx, self.off + start, data)
|
||||
|
||||
def view(self, offset:int=0, size:int|None=None, fmt=None):
|
||||
return RemoteMMIOInterface(self.dev, self.residx, size or (self.nbytes - offset), fmt or self.fmt, self.off + offset, self.rd_cmd, self.wr_cmd)
|
||||
|
||||
class RemotePCIDevice(PCIDevice):
|
||||
_bulk_sent:int = 0
|
||||
_bulk_recv:int = 0
|
||||
_rpc_count:int = 0
|
||||
_start_time:float = 0.0
|
||||
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def remote_sock(host:str, port:int) -> socket.socket:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
sock.settimeout(getenv("REMOTE_TIMEOUT", 3))
|
||||
sock.connect((host, port))
|
||||
sock.settimeout(None)
|
||||
if DEBUG >= 1 and RemotePCIDevice._start_time == 0.0:
|
||||
RemotePCIDevice._start_time = time.perf_counter()
|
||||
def _print_stats():
|
||||
dt = time.perf_counter() - RemotePCIDevice._start_time
|
||||
sent_mb, recv_mb = RemotePCIDevice._bulk_sent / 1e6, RemotePCIDevice._bulk_recv / 1e6
|
||||
print(f"remote: sent {sent_mb:,.2f} MB ({sent_mb/dt:,.2f} MB/s), recv {recv_mb:,.2f} MB ({recv_mb/dt:,.2f} MB/s), "
|
||||
f"{RemotePCIDevice._rpc_count:,} roundtrips in {dt:.2f}s")
|
||||
atexit.register(_print_stats)
|
||||
return sock
|
||||
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def remote_list(vendor:int, devices:tuple[tuple[int, tuple[int, ...]], ...], base_class:int|None) -> list[tuple[socket.socket, str]]:
|
||||
payload = array.array('I', itertools.chain.from_iterable((m, d) for m, ds in devices for d in ds)).tobytes()
|
||||
def q(r:str) -> list[tuple[socket.socket, str]]:
|
||||
sock = RemotePCIDevice.remote_sock((host:=r.strip().split(":")[0]), (port:=int(r.strip().split(":")[1]) if ":" in r else 6667))
|
||||
data_len, _, _, _ = RemotePCIDevice._rpc(sock, 0, RemoteCmd.PROBE, base_class or 0, len(payload), vendor, payload=payload)
|
||||
return [(sock, f"remote:{host}:{port}:{d}") for d in RemotePCIDevice._recvall(sock, data_len).decode().split('\n')]
|
||||
return flatten([q(r) for r in getenv("REMOTE", "").split(",") if r.strip()])
|
||||
|
||||
@staticmethod
|
||||
def _recvall(sock:socket.socket, n:int) -> bytes:
|
||||
data = b''
|
||||
while len(data) < n and (chunk:=sock.recv(n - len(data))): data += chunk
|
||||
if len(data) < n: raise RuntimeError("Connection closed")
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _rpc(sock:socket.socket, dev_id:int, cmd:int, *args:int, bar:int=0, readout_size:int=0, payload:bytes=b'', has_fd=False):
|
||||
sock.sendall(struct.pack('<BIIQQQ', cmd, dev_id, bar, *(*args, 0, 0, 0)[:3]) + payload)
|
||||
if has_fd:
|
||||
msg, anc, _, _ = sock.recvmsg(17, socket.CMSG_LEN(4))
|
||||
fd = struct.unpack('<i', anc[0][2][:4])[0]
|
||||
else: msg, fd = RemotePCIDevice._recvall(sock, 17), None
|
||||
if (resp:=struct.unpack('<BQQ', msg))[0] != 0:
|
||||
raise RuntimeError(f"RPC failed: {RemotePCIDevice._recvall(sock, resp[1]).decode('utf-8') if resp[1] > 0 else 'unknown error'}")
|
||||
RemotePCIDevice._rpc_count += 1
|
||||
return (resp[1], resp[2]) + ((RemotePCIDevice._recvall(sock, readout_size) if readout_size > 0 else None),) + (fd,)
|
||||
|
||||
def __init__(self, devpref:str, pcibus:str, sock:socket.socket):
|
||||
self.sock, self.pcibus, self.dev_id = sock, pcibus, int(pcibus.split(':')[-1]) if ':' in pcibus else 0
|
||||
self.peer_group = sock.getpeername()[0]
|
||||
for buft in [socket.SO_SNDBUF, socket.SO_RCVBUF]: self.sock.setsockopt(socket.SOL_SOCKET, buft, 64 << 20)
|
||||
|
||||
self.lock_fd = System.flock_acquire(f"{devpref.lower()}_{pcibus.lower()}.lock")
|
||||
|
||||
def _bulk_read(self, cmd:int, idx:int, offset:int, size:int) -> bytes:
|
||||
RemotePCIDevice._bulk_recv += size
|
||||
return unwrap(self._rpc(self.sock, self.dev_id, cmd, offset, size, bar=idx, readout_size=size)[2])
|
||||
def _bulk_write(self, cmd:int, idx:int, offset:int, data:bytes):
|
||||
RemotePCIDevice._bulk_sent += len(data)
|
||||
self.sock.sendall(struct.pack('<BIIQQQ', cmd, self.dev_id, idx, offset, len(data), 0) + data)
|
||||
|
||||
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
|
||||
paddrs_len, handle, _, _ = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM, size, int(contiguous))
|
||||
paddrs = list(struct.unpack(f'<{paddrs_len // 8}Q', self._recvall(self.sock, paddrs_len)))
|
||||
return RemoteMMIOInterface(self, handle, size, fmt='B', rd_cmd=RemoteCmd.SYSMEM_READ, wr_cmd=RemoteCmd.SYSMEM_WRITE), paddrs
|
||||
|
||||
def reset(self): self._rpc(self.sock, self.dev_id, RemoteCmd.RESET)
|
||||
def read_config(self, offset:int, size:int): return self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_READ, offset, size)[0]
|
||||
def write_config(self, offset:int, value:int, size:int): self._rpc(self.sock, self.dev_id, RemoteCmd.CFG_WRITE, offset, size, value)
|
||||
|
||||
@functools.cache
|
||||
def bar_info(self, bar_idx:int) -> tuple[int, int]: return self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_BAR, bar=bar_idx)[:2]
|
||||
def map_bar(self, bar:int, off:int=0, addr:int=0, size:int|None=None, fmt='B') -> MMIOInterface:
|
||||
return RemoteMMIOInterface(self, bar, size or self.bar_info(bar)[1], fmt).view(off, size, fmt)
|
||||
def resize_bar(self, bar_idx:int): self._rpc(self.sock, self.dev_id, RemoteCmd.RESIZE_BAR, bar=bar_idx)
|
||||
|
||||
class APLRemotePCIDevice(RemotePCIDevice):
|
||||
APP_PATH = "/Applications/TinyGPU.app/Contents/MacOS/TinyGPU"
|
||||
|
||||
@classmethod
|
||||
def ensure_app(cls):
|
||||
commit = "c0d024f9ff0e1dc8fdf217f255da7101d91e8323"
|
||||
app_name = f"TinyGPU_{commit}.zip"
|
||||
if (_ensure_downloads_dir() / app_name).is_file() and os.path.exists(cls.APP_PATH): return
|
||||
print("Downloading TinyGPU.app...")
|
||||
with contextlib.suppress(RuntimeError): system("pkill -f TinyGPU")
|
||||
system(f"ditto -xk {fetch(f'https://github.com/tinygrad/tinygpu_releases/raw/{commit}/TinyGPU.zip', name=app_name)} /Applications")
|
||||
print(system(f"{cls.APP_PATH} install"))
|
||||
|
||||
def __init__(self, devpref:str, pcibus:str):
|
||||
self.ensure_app()
|
||||
sock_path, sock = getenv("APL_REMOTE_SOCK", temp("tinygpu.sock")), socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
for i in range(100):
|
||||
with contextlib.suppress(ConnectionRefusedError, FileNotFoundError):
|
||||
sock.connect(sock_path)
|
||||
break
|
||||
if i == 0: subprocess.Popen([self.APP_PATH, "server", sock_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
time.sleep(0.05)
|
||||
else: raise RuntimeError(f"Failed to connect to TinyGPU server at {sock_path}.")
|
||||
super().__init__(devpref, "usb4", sock=sock)
|
||||
|
||||
def alloc_sysmem(self, size:int, vaddr:int=0, contiguous:bool=False) -> tuple[MMIOInterface, list[int]]:
|
||||
mapped_size, _, _, fd = self._rpc(self.sock, self.dev_id, RemoteCmd.MAP_SYSMEM_FD, size, int(contiguous), has_fd=True)
|
||||
memview = MMIOInterface(FileIOInterface(fd=fd).mmap(0, mapped_size, mmap.PROT_READ | mmap.PROT_WRITE, mmap.MAP_SHARED, 0), mapped_size, fmt='B')
|
||||
|
||||
# paddrs are returned as (paddr, size) pairs until a (paddr=0, size=0) terminator in the beginning of the mapping.
|
||||
paddrs_raw = list(itertools.takewhile(lambda p: p[1] != 0, zip(memview.view(fmt='Q')[0::2], memview.view(fmt='Q')[1::2])))
|
||||
return memview, [p + i for p, sz in paddrs_raw for i in range(0, sz, 0x1000)][:ceildiv(size, 0x1000)]
|
||||
476
tinygrad_repo/tinygrad/runtime/support/usb.py
Normal file
476
tinygrad_repo/tinygrad/runtime/support/usb.py
Normal file
@@ -0,0 +1,476 @@
|
||||
import ctypes, struct, dataclasses, array, itertools, time, functools
|
||||
from typing import Sequence
|
||||
from tinygrad.runtime.autogen import libusb
|
||||
from tinygrad.helpers import DEBUG, DEV, to_mv, round_up, OSX, getenv, ceildiv
|
||||
from tinygrad.runtime.support.hcq import MMIOInterface
|
||||
from tinygrad.runtime.support import c
|
||||
|
||||
def alloc_cbuffer(sz:int) -> tuple[ctypes.Array, memoryview]: return (buf:=(ctypes.c_ubyte * sz)()), to_mv(ctypes.addressof(buf), sz)
|
||||
def checked(fn, msg=None):
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args):
|
||||
if (rc:=fn(*args)) < 0: raise RuntimeError(f"{msg or fn.__name__}: {ctypes.string_at(libusb.libusb_strerror(rc)).decode()}")
|
||||
return rc
|
||||
return wrapper
|
||||
|
||||
class USB3:
|
||||
@staticmethod
|
||||
@functools.cache
|
||||
def ctx():
|
||||
ctx = c.init_c_var(ctypes.POINTER(libusb.struct_libusb_context), checked(libusb.libusb_init))
|
||||
if DEBUG >= 6: checked(libusb.libusb_set_option)(ctx, libusb.LIBUSB_OPTION_LOG_LEVEL, 4)
|
||||
return ctx
|
||||
|
||||
@classmethod
|
||||
@functools.cache
|
||||
def list_devices(cls, vendor:int, dev:int) -> list[tuple[c.POINTER[libusb.struct_libusb_device], str]]:
|
||||
ret = []
|
||||
for i in range(checked(libusb.libusb_get_device_list)(cls.ctx(), devs:=ctypes.POINTER(ctypes.POINTER(libusb.struct_libusb_device))())):
|
||||
desc = c.init_c_var(libusb.struct_libusb_device_descriptor, lambda x: checked(libusb.libusb_get_device_descriptor)(devs[i], x))
|
||||
if (desc.idVendor, desc.idProduct) == (vendor, dev):
|
||||
ret.append((libusb.libusb_ref_device(devs[i]), f"usb:{libusb.libusb_get_bus_number(devs[i])}-{libusb.libusb_get_device_address(devs[i])}"))
|
||||
libusb.libusb_free_device_list(devs, 1)
|
||||
return ret
|
||||
|
||||
def __init__(self, dev:c.POINTER[libusb.struct_libusb_device], ep_data_in:int, ep_stat_in:int, ep_data_out:int, ep_cmd_out:int,
|
||||
max_streams:int=31, use_bot=False):
|
||||
self.ep_data_in, self.ep_stat_in, self.ep_data_out, self.ep_cmd_out = ep_data_in, ep_stat_in, ep_data_out, ep_cmd_out
|
||||
self.max_streams, self.use_bot = max_streams, use_bot
|
||||
self._transferred = ctypes.c_int(0)
|
||||
self._bulk_in_buf, self._bulk_in_mv = alloc_cbuffer(4 << 20)
|
||||
self._bulk_out_buf, self._bulk_out_mv = alloc_cbuffer(4 << 20)
|
||||
|
||||
self.handle = c.init_c_var(c.POINTER[libusb.struct_libusb_device_handle], lambda x: checked(libusb.libusb_open)(dev, x))
|
||||
|
||||
# Read product string descriptor
|
||||
_buf = (ctypes.c_ubyte * 256)()
|
||||
_desc = libusb.struct_libusb_device_descriptor()
|
||||
checked(libusb.libusb_get_device_descriptor)(libusb.libusb_get_device(self.handle), ctypes.byref(_desc))
|
||||
_ret = checked(libusb.libusb_get_string_descriptor_ascii)(self.handle, _desc.iProduct, _buf, 256)
|
||||
self.product = bytes(_buf[:_ret]).decode("ascii", errors="replace")
|
||||
self.is_custom = self.product.startswith("custom")
|
||||
if self.is_custom: self.use_bot = use_bot = True
|
||||
|
||||
# Detach kernel driver if needed
|
||||
if checked(libusb.libusb_kernel_driver_active)(self.handle, 0):
|
||||
checked(libusb.libusb_detach_kernel_driver)(self.handle, 0)
|
||||
checked(libusb.libusb_reset_device)(self.handle)
|
||||
|
||||
# Set configuration and claim interface
|
||||
checked(libusb.libusb_set_configuration)(self.handle, 1)
|
||||
checked(libusb.libusb_claim_interface)(self.handle, 0)
|
||||
|
||||
if use_bot:
|
||||
checked(libusb.libusb_set_interface_alt_setting)(self.handle, 0, 0)
|
||||
self._tag = 0
|
||||
else:
|
||||
checked(libusb.libusb_set_interface_alt_setting)(self.handle, 0, 1)
|
||||
|
||||
# Clear any stalled endpoints
|
||||
all_eps = (self.ep_data_out, self.ep_data_in, self.ep_stat_in, self.ep_cmd_out)
|
||||
for ep in all_eps: checked(libusb.libusb_clear_halt)(self.handle, ep)
|
||||
|
||||
# Allocate streams
|
||||
stream_eps = (ctypes.c_uint8 * 3)(self.ep_data_out, self.ep_data_in, self.ep_stat_in)
|
||||
checked(libusb.libusb_alloc_streams)(self.handle, self.max_streams * len(stream_eps), stream_eps, len(stream_eps))
|
||||
|
||||
# Base cmd
|
||||
cmd_template = bytes([0x01, 0x00, 0x00, 0x01, *([0] * 12), 0xE4, 0x24, 0x00, 0xB2, 0x1A, 0x00, 0x00, 0x00, *([0] * 8)])
|
||||
|
||||
# Init pools
|
||||
self.tr = {ep: [libusb.libusb_alloc_transfer(0) for _ in range(self.max_streams)] for ep in all_eps}
|
||||
|
||||
self.buf_cmd = [(ctypes.c_uint8 * len(cmd_template))(*cmd_template) for _ in range(self.max_streams)]
|
||||
self.buf_stat = [(ctypes.c_uint8 * 64)() for _ in range(self.max_streams)]
|
||||
self.buf_data_in = [(ctypes.c_uint8 * 0x1000)() for _ in range(self.max_streams)]
|
||||
self.buf_data_out = [(ctypes.c_uint8 * 0x80000)() for _ in range(self.max_streams)]
|
||||
self.buf_data_out_mvs = [to_mv(ctypes.addressof(self.buf_data_out[i]), 0x80000) for i in range(self.max_streams)]
|
||||
|
||||
for slot in range(self.max_streams): struct.pack_into(">B", self.buf_cmd[slot], 3, slot + 1)
|
||||
|
||||
def _prep_transfer(self, tr, ep, stream_id, buf, length):
|
||||
tr.contents.dev_handle, tr.contents.endpoint, tr.contents.length, tr.contents.buffer = self.handle, ep, length, buf
|
||||
tr.contents.status, tr.contents.flags, tr.contents.timeout, tr.contents.num_iso_packets = 0xff, 0, 1000, 0
|
||||
tr.contents.type = (libusb.LIBUSB_TRANSFER_TYPE_BULK_STREAM if stream_id is not None else libusb.LIBUSB_TRANSFER_TYPE_BULK)
|
||||
if stream_id is not None: libusb.libusb_transfer_set_stream_id(tr, stream_id)
|
||||
return tr
|
||||
|
||||
def _submit_and_wait(self, cmds):
|
||||
for tr in cmds: checked(libusb.libusb_submit_transfer)(tr)
|
||||
|
||||
running = len(cmds)
|
||||
while running:
|
||||
checked(libusb.libusb_handle_events)(USB3.ctx())
|
||||
running = len(cmds)
|
||||
for tr in cmds:
|
||||
if tr.contents.status == libusb.LIBUSB_TRANSFER_COMPLETED: running -= 1
|
||||
elif tr.contents.status != 0xFF: raise RuntimeError(f"EP 0x{tr.contents.endpoint:02X} error: {tr.contents.status}")
|
||||
|
||||
def _bulk_out(self, ep: int, payload: bytes, timeout: int = 1000):
|
||||
if len(payload) > len(self._bulk_out_mv): self._bulk_out_buf, self._bulk_out_mv = alloc_cbuffer(len(payload))
|
||||
self._bulk_out_mv[:len(payload)] = payload
|
||||
checked(libusb.libusb_bulk_transfer, f"bulk OUT 0x{ep:02X} failed")(self.handle, ep, self._bulk_out_buf, len(payload), self._transferred, timeout)
|
||||
assert self._transferred.value == len(payload), f"bulk OUT short write on 0x{ep:02X}: {self._transferred.value}/{len(payload)} bytes"
|
||||
|
||||
def _bulk_in(self, ep: int, length: int, timeout: int = 1000) -> memoryview:
|
||||
if length > len(self._bulk_in_mv): self._bulk_in_buf, self._bulk_in_mv = alloc_cbuffer(length)
|
||||
checked(libusb.libusb_bulk_transfer, f"bulk IN 0x{ep:02X} failed")(self.handle, ep, self._bulk_in_buf, length, self._transferred, timeout)
|
||||
return self._bulk_in_mv[:self._transferred.value]
|
||||
|
||||
def send_batch(self, cdbs:list[bytes], idata:list[int]|None=None, odata:list[bytes|None]|None=None) -> list[bytes|None]:
|
||||
idata, odata = idata or [0] * len(cdbs), odata or [None] * len(cdbs)
|
||||
results:list[bytes|None] = []
|
||||
tr_window, op_window = [], []
|
||||
|
||||
for idx, (cdb, rlen, send_data) in enumerate(zip(cdbs, idata, odata)):
|
||||
if self.use_bot:
|
||||
dir_in = rlen > 0
|
||||
data_len = rlen if dir_in else (len(send_data) if send_data is not None else 0)
|
||||
assert not (rlen > 0 and send_data is not None), "BOT mode only supports either read or write per command"
|
||||
|
||||
# CBW
|
||||
self._tag += 1
|
||||
flags = 0x80 if dir_in else 0x00
|
||||
cbw = struct.pack("<IIIBBB", 0x43425355, self._tag, data_len, flags, 0, len(cdb)) + cdb + b"\x00" * (16 - len(cdb))
|
||||
self._bulk_out(self.ep_data_out, cbw)
|
||||
|
||||
# DAT
|
||||
if dir_in:
|
||||
results.append(bytes(self._bulk_in(self.ep_data_in, rlen)))
|
||||
else:
|
||||
if send_data is not None:
|
||||
self._bulk_out(self.ep_data_out, send_data)
|
||||
results.append(None)
|
||||
|
||||
# CSW
|
||||
sig, rtag, residue, status = struct.unpack("<IIIB", self._bulk_in(self.ep_data_in, 13, timeout=2000))
|
||||
assert sig == 0x53425355, f"Bad CSW signature 0x{sig:08X}, expected 0x53425355"
|
||||
assert rtag == self._tag, f"CSW tag mismatch: got {rtag}, expected {self._tag}"
|
||||
assert status == 0, f"SCSI command failed, CSW status=0x{status:02X}, residue={residue}"
|
||||
else:
|
||||
# allocate slot and stream. stream is 1-based
|
||||
slot, stream = idx % self.max_streams, (idx % self.max_streams) + 1
|
||||
|
||||
# build cmd packet
|
||||
self.buf_cmd[slot][16:16+len(cdb)] = list(cdb)
|
||||
|
||||
# cmd + stat transfers
|
||||
tr_window.append(self._prep_transfer(self.tr[self.ep_cmd_out][slot], self.ep_cmd_out, None, self.buf_cmd[slot], len(self.buf_cmd[slot])))
|
||||
tr_window.append(self._prep_transfer(self.tr[self.ep_stat_in][slot], self.ep_stat_in, stream, self.buf_stat[slot], 64))
|
||||
|
||||
if rlen:
|
||||
if rlen > len(self.buf_data_in[slot]): self.buf_data_in[slot] = (ctypes.c_uint8 * round_up(rlen, 0x1000))()
|
||||
tr_window.append(self._prep_transfer(self.tr[self.ep_data_in][slot], self.ep_data_in, stream, self.buf_data_in[slot], rlen))
|
||||
|
||||
if send_data is not None:
|
||||
if len(send_data) > len(self.buf_data_out[slot]):
|
||||
self.buf_data_out[slot] = (ctypes.c_uint8 * len(send_data))()
|
||||
self.buf_data_out_mvs[slot] = to_mv(ctypes.addressof(self.buf_data_out[slot]), len(send_data))
|
||||
|
||||
self.buf_data_out_mvs[slot][:len(send_data)] = bytes(send_data)
|
||||
tr_window.append(self._prep_transfer(self.tr[self.ep_data_out][slot], self.ep_data_out, stream, self.buf_data_out[slot], len(send_data)))
|
||||
|
||||
op_window.append((idx, slot, rlen))
|
||||
if (idx + 1 == len(cdbs)) or len(op_window) >= self.max_streams:
|
||||
self._submit_and_wait(tr_window)
|
||||
for idx, slot, rlen in op_window: results.append(bytes(self.buf_data_in[slot][:rlen]) if rlen else None)
|
||||
tr_window = []
|
||||
|
||||
return results
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class WriteOp: addr:int; data:bytes; ignore_cache:bool=True # noqa: E702
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class ReadOp: addr:int; size:int # noqa: E702
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class ScsiWriteOp: data:bytes; lba:int=0 # noqa: E702
|
||||
|
||||
class CustomASM24Controller:
|
||||
def __init__(self, usb:USB3|None=None):
|
||||
if not usb:
|
||||
devs = USB3.list_devices(0xADD1, 0x0001)
|
||||
assert len(devs), "no ASM24 controller found"
|
||||
self.usb = USB3(devs[0][0], 0x81, 0x83, 0x02, 0x04, use_bot=True)
|
||||
else: self.usb = usb
|
||||
self._pci_cacheable: list[tuple[int, int]] = []
|
||||
self._pci_cache: dict[int, int|None] = {}
|
||||
|
||||
self._f0_out_buf, self._f0_out_mv = alloc_cbuffer(0x1000) # for f0 and e4, allocate big enough for e4
|
||||
self._f0_in_buf, _ = alloc_cbuffer(8)
|
||||
|
||||
# Custom firmware now boots with PCIe off. Power it on before probing the link.
|
||||
ltssm = self.read(0xB450, 1)[0]
|
||||
if ltssm != 0x78: self.set_pcie_power(True)
|
||||
ltssm = self.read(0xB450, 1)[0]
|
||||
if ltssm != 0x78: raise RuntimeError(f"PCIe link not up (LTSSM=0x{ltssm:02X}), custom firmware not ready")
|
||||
|
||||
def set_pcie_power(self, enabled:bool, timeout:int=10000):
|
||||
checked(libusb.libusb_control_transfer,
|
||||
f"F3 PCIe power {'on' if enabled else 'off'} failed")(self.usb.handle, 0x40, 0xF3, int(enabled), 0, None, 0, timeout)
|
||||
|
||||
# === PCIe TLP via 0xF0 vendor command ===
|
||||
|
||||
def _f0_out(self, fmt_type:int, byte_en:int, address:int, value:int, mode:int=0):
|
||||
struct.pack_into('<III', self._f0_out_mv, 0, address & 0xFFFFFFFF, address >> 32, value)
|
||||
ret = libusb.libusb_control_transfer(self.usb.handle, 0x40, 0xF0, fmt_type | (byte_en << 8), mode & 0x03, self._f0_out_buf, 12, 5000)
|
||||
assert ret == 12, f"F0 OUT failed: {ret}"
|
||||
|
||||
def _f0_in(self) -> tuple[int, int, int]:
|
||||
ret = libusb.libusb_control_transfer(self.usb.handle, 0xC0, 0xF0, 0, 0, self._f0_in_buf, 8, 5000)
|
||||
assert ret == 8, f"F0 IN failed: {ret}"
|
||||
return struct.unpack_from('<I', self._f0_in_buf, 0)[0], (self._f0_in_buf[4] >> 5) & 0x7, self._f0_in_buf[7]
|
||||
|
||||
def _is_pci_cacheable(self, addr:int) -> bool: return any(x <= addr <= x + sz for x, sz in self._pci_cacheable)
|
||||
|
||||
def pcie_request(self, fmt_type:int, address:int, value:int|None=None, size:int=4, cnt:int=10):
|
||||
if fmt_type == 0x60 and size == 4 and self._is_pci_cacheable(address) and self._pci_cache.get(address) == value: return
|
||||
assert size > 0 and size <= 4, f"Invalid size {size}"
|
||||
if DEBUG >= 5: print("pcie_request", hex(fmt_type), hex(address), value, size)
|
||||
|
||||
offset = address & 0x3
|
||||
byte_en = ((1 << size) - 1) << offset
|
||||
self._pci_cache[address] = value if size == 4 and fmt_type == 0x60 else None
|
||||
|
||||
self._f0_out(fmt_type, byte_en, address & ~0x3, (value << (8 * offset)) if value is not None else 0)
|
||||
|
||||
# Fast path: memory writes and messages don't return completions (same logic as ASM24Controller).
|
||||
if ((fmt_type & 0b11011111) == 0b01000000) or ((fmt_type & 0b10111000) == 0b00110000): return
|
||||
|
||||
# Read TLPs and config writes: read completion via 0xF0 IN. Retry on error/timeout.
|
||||
data, cpl_status, ret_status = self._f0_in()
|
||||
if ret_status != 0:
|
||||
time.sleep(0.001) # TODO: this sleep is very picky
|
||||
if cnt > 0:
|
||||
return self.pcie_request(fmt_type, address, value, size, cnt=cnt-1)
|
||||
raise RuntimeError(f"TLP error after retries: ret_status={ret_status}, address={address:#x}")
|
||||
|
||||
if cpl_status:
|
||||
status_map = {0b001: f"Unsupported Request: {address:#x}", 0b100: "Completer Abort", 0b010: "Config Retry"}
|
||||
raise RuntimeError(f"TLP completion status: {status_map.get(cpl_status, f'Reserved (0b{cpl_status:03b})')}")
|
||||
|
||||
if value is None: return (data >> (8 * offset)) & ((1 << (8 * size)) - 1)
|
||||
|
||||
def pcie_cfg_req(self, byte_addr:int, bus:int=1, dev:int=0, fn:int=0, value:int|None=None, size:int=4):
|
||||
assert byte_addr >> 12 == 0 and bus >> 8 == 0 and dev >> 5 == 0 and fn >> 3 == 0
|
||||
fmt_type = (0x44 if value is not None else 0x4) | int(bus > 0)
|
||||
address = (bus << 24) | (dev << 19) | (fn << 16) | (byte_addr & 0xfff)
|
||||
return self.pcie_request(fmt_type, address, value, size)
|
||||
|
||||
def pcie_mem_req(self, address:int, value:int|None=None, size:int=4):
|
||||
return self.pcie_request(0x60 if value is not None else 0x20, address, value, size)
|
||||
|
||||
def pcie_mem_write(self, address:int, values:list[int], size:int):
|
||||
"""Streaming PCIe memory write via 0xF0 mode 1 + bulk OUT. Data is little-endian dwords on the wire."""
|
||||
if not values: return
|
||||
self._f0_out(0x60, 0x0F, address, len(values), mode=1)
|
||||
self.usb._bulk_out(0x02, struct.pack(f'<{len(values)}I', *values))
|
||||
|
||||
def pcie_mem_read(self, address:int, nbytes:int) -> bytes:
|
||||
"""Streaming PCIe memory read via 0xF0 mode 2 + bulk IN. Returns little-endian bytes."""
|
||||
assert nbytes % 4 == 0, f"pcie_mem_read requires 4-byte aligned size, got {nbytes}"
|
||||
self._f0_out(0x20, 0x0F, address, nbytes // 4, mode=2)
|
||||
return self.usb._bulk_in(0x81, nbytes, timeout=30000)
|
||||
|
||||
# === XDATA read/write (0xE4/0xE5 vendor control transfers) ===
|
||||
|
||||
def read(self, base_addr:int, length:int, **kwargs) -> bytes:
|
||||
"""Read from chip XDATA via vendor control IN (bRequest=0xE4). wValue=addr, wLength=size."""
|
||||
result = b''
|
||||
for off in range(0, length, 0xFF):
|
||||
chunk = min(0xFF, length - off)
|
||||
ret = libusb.libusb_control_transfer(self.usb.handle, 0xC0, 0xE4, base_addr + off, 0, self._f0_out_buf, chunk, 1000)
|
||||
assert ret == chunk, f"read(0x{base_addr + off:04X}, {chunk}) failed: {ret}"
|
||||
result += bytes(self._f0_out_buf[:ret])
|
||||
return result[:length]
|
||||
|
||||
def write(self, base_addr:int, data:bytes, **kwargs):
|
||||
"""Write to chip XDATA via vendor control OUT (bRequest=0xE5). wValue=addr, wIndex=val."""
|
||||
for off, val in enumerate(data):
|
||||
checked(libusb.libusb_control_transfer,
|
||||
f"write(0x{base_addr + off:04X}, 0x{val:02X}) failed")(self.usb.handle, 0x40, 0xE5, base_addr + off, val, None, 0, 1000)
|
||||
|
||||
def scsi_write(self, buf:bytes, lba:int=0):
|
||||
"""Write to SRAM via 0xF2 vendor command + bulk OUT."""
|
||||
buf_padded = buf + b'\x00' * (round_up(len(buf), 512) - len(buf))
|
||||
sectors = len(buf_padded) // 512
|
||||
num_slots = round_up(len(buf_padded), 0x4000) // 0x4000 # 16KB per slot
|
||||
# 0xF2 OUT: wValue=sectors, wIndex=start_slot|(num_slots<<8)
|
||||
windex = (num_slots & 0xFF) << 8
|
||||
checked(libusb.libusb_control_transfer, "F2 setup failed")(self.usb.handle, 0x40, 0xF2, sectors, windex, None, 0, 1000)
|
||||
self.usb._bulk_out(0x02, buf_padded)
|
||||
|
||||
def scsi_read_arm(self, size:int):
|
||||
windex = (ceildiv(size, 0x4000) & 0xFF) << 8
|
||||
checked(libusb.libusb_control_transfer,
|
||||
"F2 read arm failed")(self.usb.handle, 0x40, 0xF2, (ceildiv(size, 512) & 0x7FFF) | 0x8000, windex, None, 0, 1000)
|
||||
|
||||
def scsi_read(self, size:int) -> memoryview: return self.usb._bulk_in(0x81, round_up(size, 512), timeout=10000)[:size]
|
||||
|
||||
class ASM24Controller:
|
||||
def __init__(self, usb:USB3|None=None):
|
||||
if not usb:
|
||||
devs = USB3.list_devices(0xADD1, 0x0001)
|
||||
assert len(devs), "no ASM24 controller found"
|
||||
self.usb = USB3(devs[0][0], 0x81, 0x83, 0x02, 0x04, use_bot=bool(getenv("USE_BOT", 0)))
|
||||
else: self.usb = usb
|
||||
self._cache: dict[int, int|None] = {}
|
||||
self._pci_cacheable: list[tuple[int, int]] = []
|
||||
self._pci_cache: dict[int, int|None] = {}
|
||||
|
||||
# Init controller.
|
||||
self.exec_ops([WriteOp(0x54b, b' '), WriteOp(0x54e, b'\x04'), WriteOp(0x5a8, b'\x02'), WriteOp(0x5f8, b'\x04'),
|
||||
WriteOp(0x7ec, b'\x01\x00\x00\x00'), WriteOp(0xc422, b'\x02'), WriteOp(0x0, b'\x33')])
|
||||
|
||||
def exec_ops(self, ops:Sequence[WriteOp|ReadOp|ScsiWriteOp]):
|
||||
cdbs:list[bytes] = []
|
||||
idata:list[int] = []
|
||||
odata:list[bytes|None] = []
|
||||
|
||||
def _add_req(cdb:bytes, i:int, o:bytes|None):
|
||||
nonlocal cdbs, idata, odata
|
||||
cdbs, idata, odata = cdbs + [cdb], idata + [i], odata + [o]
|
||||
|
||||
for op in ops:
|
||||
if isinstance(op, WriteOp):
|
||||
for off, value in enumerate(op.data):
|
||||
addr = ((op.addr + off) & 0x1FFFF) | 0x500000
|
||||
if not op.ignore_cache and self._cache.get(addr) == value: continue
|
||||
_add_req(struct.pack('>BBBHB', 0xE5, value, addr >> 16, addr & 0xFFFF, 0), 0, None)
|
||||
self._cache[addr] = value
|
||||
elif isinstance(op, ReadOp):
|
||||
assert op.size <= 0xff
|
||||
addr = (op.addr & 0x1FFFF) | 0x500000
|
||||
_add_req(struct.pack('>BBBHB', 0xE4, op.size, addr >> 16, addr & 0xFFFF, 0), op.size, None)
|
||||
for i in range(op.size): self._cache[addr + i] = None
|
||||
elif isinstance(op, ScsiWriteOp):
|
||||
sectors = round_up(len(op.data), 512) // 512
|
||||
_add_req(struct.pack('>BBQIBB', 0x8A, 0, op.lba, sectors, 0, 0), 0, op.data+b'\x00'*((sectors*512)-len(op.data)))
|
||||
|
||||
return self.usb.send_batch(cdbs, idata, odata)
|
||||
|
||||
def write(self, base_addr:int, data:bytes, ignore_cache:bool=True): return self.exec_ops([WriteOp(base_addr, data, ignore_cache)])
|
||||
|
||||
def scsi_write(self, buf:bytes, lba:int=0):
|
||||
if len(buf) > 0x4000: buf += b'\x00' * (round_up(len(buf), 0x10000) - len(buf))
|
||||
|
||||
for i in range(0, len(buf), 0x10000):
|
||||
self.exec_ops([ScsiWriteOp(buf[i:i+0x10000], lba), WriteOp(0x171, b'\xff\xff\xff', ignore_cache=True)])
|
||||
self.exec_ops([WriteOp(0xce6e, b'\x00\x00', ignore_cache=True)])
|
||||
|
||||
if len(buf) > 0x4000:
|
||||
for i in range(4): self.exec_ops([WriteOp(0xce40 + i, b'\x00', ignore_cache=True)])
|
||||
|
||||
def read(self, base_addr:int, length:int, stride:int=0xff) -> bytes:
|
||||
parts = self.exec_ops([ReadOp(base_addr + off, min(stride, length - off)) for off in range(0, length, stride)])
|
||||
return b''.join(p or b'' for p in parts)[:length]
|
||||
|
||||
def _is_pci_cacheable(self, addr:int) -> bool: return any(x <= addr <= x + sz for x, sz in self._pci_cacheable)
|
||||
def pcie_prep_request(self, fmt_type:int, address:int, value:int|None=None, size:int=4) -> list[WriteOp]:
|
||||
if fmt_type == 0x60 and size == 4 and self._is_pci_cacheable(address) and self._pci_cache.get(address) == value: return []
|
||||
|
||||
assert fmt_type >> 8 == 0 and size > 0 and size <= 4, f"Invalid fmt_type {fmt_type} or size {size}"
|
||||
if DEBUG >= 5: print("pcie_request", hex(fmt_type), hex(address), value, size)
|
||||
|
||||
masked_address, offset = address & 0xFFFFFFFC, address & 0x3
|
||||
assert size + offset <= 4 and (value is None or value >> (8 * size) == 0)
|
||||
self._pci_cache[address] = value if size == 4 and fmt_type == 0x60 else None
|
||||
|
||||
return ([WriteOp(0xB220, struct.pack('>I', value << (8 * offset)), ignore_cache=False)] if value is not None else []) + \
|
||||
[WriteOp(0xB218, struct.pack('>I', masked_address), ignore_cache=False), WriteOp(0xB21c, struct.pack('>I', address>>32), ignore_cache=False),
|
||||
WriteOp(0xB217, bytes([((1 << size) - 1) << offset]), ignore_cache=False), WriteOp(0xB210, bytes([fmt_type]), ignore_cache=False),
|
||||
WriteOp(0xB254, b"\x0f", ignore_cache=True), WriteOp(0xB296, b"\x04", ignore_cache=True)]
|
||||
|
||||
def pcie_request(self, fmt_type, address, value=None, size=4, cnt=10):
|
||||
self.exec_ops(self.pcie_prep_request(fmt_type, address, value, size))
|
||||
|
||||
# Fast path for write requests
|
||||
if ((fmt_type & 0b11011111) == 0b01000000) or ((fmt_type & 0b10111000) == 0b00110000): return
|
||||
|
||||
while (stat:=self.read(0xB296, 1)[0]) & 2 == 0:
|
||||
if stat & 1:
|
||||
self.write(0xB296, bytes([0x01]))
|
||||
if cnt > 0: return self.pcie_request(fmt_type, address, value, size, cnt=cnt-1)
|
||||
assert stat == 2, f"stat read 2 was {stat}"
|
||||
|
||||
# Retrieve completion data from Link Status (0xB22A, 0xB22B)
|
||||
b284 = self.read(0xB284, 1)[0]
|
||||
completion = struct.unpack('>H', self.read(0xB22A, 2))
|
||||
|
||||
# Validate completion status based on PCIe request typ
|
||||
# Completion TLPs for configuration requests always have a byte count of 4.
|
||||
assert completion[0] & 0xfff == (4 if (fmt_type & 0xbe == 0x04) else size)
|
||||
|
||||
# Extract completion status field
|
||||
status = (completion[0] >> 13) & 0x7
|
||||
|
||||
# Handle completion errors or inconsistencies
|
||||
if status or ((fmt_type & 0xbe == 0x04) and (((value is None) and (not (b284 & 0x01))) or ((value is not None) and (b284 & 0x01)))):
|
||||
status_map = {0b001: f"Unsupported Request: invalid address/function (target might not be reachable): {address:#x}",
|
||||
0b100: "Completer Abort: abort due to internal error", 0b010: "Configuration Request Retry Status: configuration space busy"}
|
||||
raise RuntimeError(f"TLP status: {status_map.get(status, 'Reserved (0b{:03b})'.format(status))}")
|
||||
|
||||
if value is None: return (struct.unpack('>I', self.read(0xB220, 4))[0] >> (8 * (address & 0x3))) & ((1 << (8 * size)) - 1)
|
||||
|
||||
def pcie_cfg_req(self, byte_addr, bus=1, dev=0, fn=0, value=None, size=4):
|
||||
assert byte_addr >> 12 == 0 and bus >> 8 == 0 and dev >> 5 == 0 and fn >> 3 == 0, f"Invalid byte_addr {byte_addr}, bus {bus}, dev {dev}, fn {fn}"
|
||||
|
||||
fmt_type = (0x44 if value is not None else 0x4) | int(bus > 0)
|
||||
address = (bus << 24) | (dev << 19) | (fn << 16) | (byte_addr & 0xfff)
|
||||
return self.pcie_request(fmt_type, address, value, size)
|
||||
|
||||
def pcie_mem_req(self, address, value=None, size=4): return self.pcie_request(0x60 if value is not None else 0x20, address, value, size)
|
||||
|
||||
def pcie_mem_write(self, address, values, size):
|
||||
ops = [self.pcie_prep_request(0x60, address + i * size, value, size) for i, value in enumerate(values)]
|
||||
|
||||
# Send in batches of 4 for OSX and 16 for Linux (benchmarked values)
|
||||
for i in range(0, len(ops), bs:=(4 if OSX else 16)): self.exec_ops(list(itertools.chain.from_iterable(ops[i:i+bs])))
|
||||
|
||||
class USBMMIOInterface(MMIOInterface):
|
||||
def __init__(self, usb, addr, size, fmt, pcimem=True): # pylint: disable=super-init-not-called
|
||||
self.usb, self.addr, self.nbytes, self.fmt, self.pcimem, self.el_sz = usb, addr, size, fmt, pcimem, struct.calcsize(fmt)
|
||||
|
||||
def __getitem__(self, index): return self._access_items(index)
|
||||
def __setitem__(self, index, val): self._access_items(index, val)
|
||||
|
||||
def _access_items(self, index, val=None):
|
||||
if isinstance(index, slice): return self._acc((index.start or 0) * self.el_sz, ((index.stop or len(self))-(index.start or 0)) * self.el_sz, val)
|
||||
return self._acc_one(index * self.el_sz, self.el_sz, val) if self.pcimem else self._acc(index * self.el_sz, self.el_sz, val)
|
||||
|
||||
def view(self, offset:int=0, size:int|None=None, fmt=None):
|
||||
return USBMMIOInterface(self.usb, self.addr+offset, size or (self.nbytes - offset), fmt=fmt or self.fmt, pcimem=self.pcimem)
|
||||
|
||||
def _acc_size(self, sz): return next(x for x in [('I', 4), ('H', 2), ('B', 1)] if sz % x[1] == 0)
|
||||
|
||||
def _acc_one(self, off, sz, val=None):
|
||||
upper = 0 if sz < 8 else self.usb.pcie_mem_req(self.addr + off + 4, val if val is None else (val >> 32), 4)
|
||||
lower = self.usb.pcie_mem_req(self.addr + off, val if val is None else val & 0xffffffff, min(sz, 4))
|
||||
if val is None: return lower | (upper << 32)
|
||||
|
||||
def _acc(self, off, sz, data=None):
|
||||
if data is None: # read op
|
||||
if not self.pcimem:
|
||||
if self.addr == 0xf000 and hasattr(self.usb, 'scsi_read'): return self.usb.scsi_read(sz)
|
||||
return int.from_bytes(self.usb.read(self.addr + off, sz), "little") if sz == self.el_sz else self.usb.read(self.addr + off, sz)
|
||||
|
||||
# Fast path: streaming PCIe read if controller supports it
|
||||
if hasattr(self.usb, 'pcie_mem_read') and sz >= 4 and sz % 4 == 0:
|
||||
return self.usb.pcie_mem_read(self.addr + off, sz)
|
||||
|
||||
acc, acc_size = self._acc_size(sz)
|
||||
return bytes(array.array(acc, [self._acc_one(off + i * acc_size, acc_size) for i in range(sz // acc_size)]))
|
||||
|
||||
# write op
|
||||
data = struct.pack(self.fmt, data) if isinstance(data, int) else bytes(data)
|
||||
|
||||
if not self.pcimem:
|
||||
# Fast path for writing into buffer 0xf000
|
||||
use_cache = 0xa800 <= self.addr <= 0xb000
|
||||
return self.usb.scsi_write(bytes(data)) if self.addr == 0xf000 else self.usb.write(self.addr + off, bytes(data), ignore_cache=not use_cache)
|
||||
|
||||
_, acc_sz = self._acc_size(len(data) * struct.calcsize(self.fmt))
|
||||
self.usb.pcie_mem_write(self.addr+off, [int.from_bytes(data[i:i+acc_sz], "little") for i in range(0, len(data), acc_sz)], acc_sz)
|
||||
|
||||
if DEV.interface.startswith("MOCK"): from test.mockgpu.usb import MockUSB3 as USB3 # type: ignore # noqa: F811
|
||||
Reference in New Issue
Block a user