code wiki / _hdl_build / nx_simtcore.nx

nx_simtcore.nx source

↩ module page · 1615 lines · 70217 B

1// nx_simtcore.nx -- the SOVEREIGN SIMT GPGPU EXECUTION CORE. 2// 3// This is the missing organ named by our OWN ruler: nx_sovgpu_census grades the cell 4// "open GPU core on OUR FPGA (Vortex/Nyuzi class)" as GAP. This file closes it. It is a 5// SIMT (single-instruction multiple-thread) execution core built from the first bit in 6// NishiLang on top of the sovereign RV64IM decoder + ALU already in nishi-silicon/hdl. 7// 8// WHAT SIMT ADDS OVER THE SCALAR CORE (rv64im_min_sim): 9// scalar core : 1 PC, 1 register file, 1 instruction -> 1 result 10// SIMT core : NW warps, each with 1 PC and a THREAD MASK; each warp owns NT threads; 11// each thread owns its own 32-entry register file. One fetched instruction 12// executes in lockstep across every ACTIVE thread of the selected warp. 13// A masked-off thread commits NOTHING -- no register write, no memory write. That single 14// rule is what makes control-flow divergence expressible on a machine with one PC. 15// 16// ===== BENCHMARK ORACLE (external, measured, never copied) ===== 17// Vortex (Georgia Tech, MICRO'54 2021, arXiv 2110.10857) is the reference open-source 18// RISC-V GPGPU. We use its PUBLISHED ARCHITECTURE AND MEASUREMENTS as a measuring stick. 19// We copy no Vortex source, RTL, or asset. Verified facts driving this design: 20// - Vortex extends RISC-V with SIX instructions: wspawn, tmc, split, join, bar, tex. 21// "by adding only six new instructions to the standard RISC-V ISA" (MICRO'21 sec 1). 22// - Divergence uses an IPDOM (immediate post-dominator) stack, TWO pushes per split. 23// - Thread mask: "If the bit in the thread mask for a specific thread is zero, no 24// modifications would be made to that thread's register file and no changes to the 25// cache would be made based on that thread." (arXiv 2002.12151). 26// - Scheduler picks ONE wavefront per cycle from a visible mask; stalled/barrier masks 27// remove warps from contention (MICRO'21 sec 4.1.1). 28// - Cache banks are kept equal to the threads-per-warp count (CARRV 2019). 29// - Baseline config 4W-4T; configs evaluated 4W-4T, 2W-8T, 8W-2T, 4W-8T, 8W-4T. 30// 31// ===== SOVEREIGN ENCODING, NOT VORTEX'S ===== 32// RISC-V reserves four opcodes for user extensions: 0x0B, 0x2B, 0x5B, 0x7B. Vortex 33// documents 0x5B for its `tex`. We take custom-0 = 0x0B so there is ZERO encoding 34// collision with Vortex: the CAPABILITY set is benchmarked, the BITS are our own. 35// 36// NEVER-BRICK (cardinal 26): this core is a pure behavioural model over caller-owned 37// memory. It writes no firmware, no device, no persistent hardware state. It is 38// deterministic and bounded by construction -- every run is replayable bit-for-bit. 39// 40// Status: SEED. 2026-07-31. license_tier: ORIGINAL expect_exit: 0 41 42import "nx_syscalls.nx" 43import "nishi_hdl_primitives.nx" 44import "rv64im_min_decoder.nx" 45import "rv64im_min_alu.nx" 46import "rv64im_min_sim.nx" 47 48// ===== Structural limits ================================================= 49// Hard ceilings on allocation. The core's ACTUAL warp/thread counts are runtime 50// configuration (nw/nt) so one binary can run every Vortex comparison point. 51 52const NX_SIMT_MAX_WARPS: i64 = 16 53const NX_SIMT_MAX_THREADS: i64 = 16 54const NX_SIMT_NREGS: i64 = 32 // RISC-V architectural GPRs, per thread 55const NX_SIMT_IPDOM_DEPTH: i64 = 32 // divergence nesting depth, per warp 56const NX_SIMT_NBARRIERS: i64 = 8 // hardware barrier slots 57// "the barrier ID encodes whether it has local scope (intra-core) or global scope 58// (inter-core)... the MSB of the barrier ID indicates global scope" (MICRO'21 sec 4.1.3). 59// We mirror that exactly: bit 7 set = global, and the low bits still select the slot. 60const NX_SIMT_BAR_GLOBAL: i64 = 128 61 62// ===== Timing model constants ================================================= 63// Rule 11 (no magic numbers): every latency is a named knob, and the memory numbers are 64// now SOURCED rather than invented. 65// 66// PROVENANCE: Vortex MICRO'21 Figure 21 ("The effect of memory scaling on performance") 67// carries its parameter legend INSIDE the plot bitmap -- no PDF text layer, so text 68// extraction reports it as UNKNOWN. Read off the rendered figure at 9x: 69// sh: 24-cycle memory latency lg: 200-cycle memory latency 70// 2c: 2-channel memory 8c: 8-channel memory l2: L2 cache enabled 71// The same figure shows sgemm at 103 IPC under `sh` versus 16.5 IPC under `lg` -- a 6.2x 72// swing from memory latency ALONE, which is why a model that assumes short latency will 73// over-predict how much warp/thread partitioning matters. 74// 75// An earlier revision of this file used an invented 20-cycle latency. That silently 76// modelled the OPTIMISTIC end of Vortex's own range and is why the first benchmark could 77// only claim direction, never magnitude. 78 79const NX_SIMT_LAT_ALU: i64 = 1 // issue-to-issue for an ALU op 80 81// ===== MULTIPLY-ACCUMULATE LATENCY -- NOW SOURCED, NOT FITTED ===== 82// ★★★THIS CONSTANT WAS 3 AND UNSOURCED. It is now 8, and the difference matters more than 83// the number: 8 is ADOPTED FROM VORTEX'S OWN SOURCE, not chosen because it fits. 84// 85// PROVENANCE, triple-anchored and independent: 86// 1. `VX_config.toml:116` resolves VX_CFG_FMA_LATENCY to 8 for the default build. 87// 2. Their FPU design doc (docs/designs/floating_point_unit.md:138) independently states 88// "native STD = 8 (F32) / 12 (F64)". 89// 3. `VX_fma_unit_rtl.sv:55-69` DECOMPOSES that 8 into named pipeline stages that sum: 90// INI(1) -> MUL(LATENCY-6) -> ALN(1) -> ACC(1) -> NRM(1) -> RND(2), guarded by a 91// STATIC_ASSERT(MUL_LATENCY >= 1). 92// 93// ★THAT ASSERT RETROSPECTIVELY CONVICTS OUR OLD VALUE: it puts a HARD FLOOR OF 7 on F32 FMA, 94// so the 3 this model used was not merely wrong, it was STRUCTURALLY IMPOSSIBLE in the design 95// we were claiming to model. 96// 97// ⚠WE APPLY AN FMA LATENCY TO AN INTEGER MAC. Our calibration kernel uses the M-extension 98// multiply as a proxy for sgemm, which is floating-point on the real machine. Modelling the 99// proxy at the FMA latency is the faithful choice for calibrating AGAINST sgemm, and it is 100// stated here rather than hidden. 101// ⚠FPGA CAVEAT, DECLARED: Figure 14's IPC was measured on an FPGA, and on that path Vortex 102// selects VENDOR IP whose latency differs (16 on Xilinx; an Altera value of 4 that is 103// CONTESTED inside their own tree by a STATIC_ASSERT expecting 16). 8 is the default 104// soft-logic/SimX figure. If the 1200 baseline is FPGA-derived, the vendor number applies. 105// ★A SWEEP THAT LANDS ON THE SAME VALUE AS A SOURCE IS CORROBORATION, NOT CIRCULARITY -- the 106// sweep is why we looked; the source is why we adopted. Any residual gap after this is to be 107// REPORTED AS UNEXPLAINED, never closed by moving this constant again. 108const NX_SIMT_LAT_MULDIV: i64 = 8 109const NX_SIMT_LAT_MEM_SHORT: i64 = 24 // Vortex "sh" -- cache-resident / short latency 110const NX_SIMT_LAT_MEM_LONG: i64 = 200 // Vortex "lg" -- DRAM / long latency 111const NX_SIMT_CHAN_LOW: i64 = 2 // Vortex "2c" -- 2-channel memory 112const NX_SIMT_CHAN_HIGH: i64 = 8 // Vortex "8c" -- 8-channel memory 113const NX_SIMT_MAX_CHANNELS: i64 = 16 114 115// Cycles a channel stays busy per burst transferred. Without this a request occupied a 116// channel for a single cycle, so the service rate (nchan per cycle) always exceeded the 117// arrival rate (at most one warp issues per cycle) and NO QUEUE COULD EVER FORM -- the 118// channel count was inert and the model was latency-only while claiming to be a memory 119// system. A real DRAM burst holds its channel for several cycles; that is what makes 120// bandwidth a finite resource. Queueing appears once cost*BURST exceeds nchan. 121const NX_SIMT_BURST_CYCLES: i64 = 4 122 123// ===== L1 data cache ================================================= 124// Vortex MICRO'21 Figure 15 puts the D-cache at **35% of core area** -- the single largest 125// block, bigger than the texture unit (20%) and dwarfing the register file (1%). A model 126// without one is missing the most expensive thing on the chip. 127// 128// It is also missing a real BEHAVIOUR, not just a cost: the DATE'25 warp-features study 129// (arXiv 2505.03102) found a kernel, mse_forward, where the SOFTWARE emulation of warp ops 130// BEATS the hardware path, because loop serialisation collapses memory traffic that the 131// hardware path re-issues per lane. That crossover is pure cache reuse. A cacheless model 132// cannot represent it and will over-report the hardware win on EVERY kernel. 133// 134// Geometry from CARRV'19: "a two-way set associative L1 cache with 16 Kbytes". 135// 128 sets x 2 ways x 64B line = 16KB. 136const NX_SIMT_CACHE_LINE_B: i64 = 64 137const NX_SIMT_CACHE_SETS: i64 = 128 138const NX_SIMT_CACHE_WAYS: i64 = 2 139const NX_SIMT_LAT_CACHE_HIT: i64 = 4 // cycles when every touched line is resident 140 141// ===== SIMT opcode: RISC-V custom-0 ================================================= 142// R-type layout, exactly like the base ISA: funct7 | rs2 | rs1 | funct3 | rd | opcode. 143// funct3 selects the SIMT operation. 144 145const NX_SIMT_OPCODE: i64 = 0x0B 146 147const NX_SIMT_F3_TMC: i64 = 0 // tmc rs1 -- thread mask control 148const NX_SIMT_F3_WSPAWN: i64 = 1 // wspawn rs1, rs2 -- activate rs1 warps at PC rs2 149const NX_SIMT_F3_SPLIT: i64 = 2 // split rs1 -- control-flow divergence 150const NX_SIMT_F3_JOIN: i64 = 3 // join -- control-flow reconvergence 151const NX_SIMT_F3_BAR: i64 = 4 // bar rs1, rs2 -- barrier id rs1, warp count rs2 152const NX_SIMT_F3_TID: i64 = 5 // tid rd -- rd = this thread's lane id 153const NX_SIMT_F3_WID: i64 = 6 // wid rd -- rd = this warp's id 154const NX_SIMT_F3_NTID: i64 = 7 // ntid rd -- rd = threads per warp 155 156// ===== Warp-collective extension: RISC-V custom-1 ================================================= 157// Lane-to-lane data exchange and predicate reduction, in one instruction, without a round 158// trip through memory. These are what make a warp a cooperative unit rather than N 159// independent threads that happen to share a PC. 160// 161// ORACLE: Vortex's own DATE'25 study ("Hardware vs. Software Implementation of Warp-Level 162// Features in Vortex RISC-V GPU", arXiv 2505.03102) added hardware vx_shfl / vx_vote and 163// measured 2.42x geomean IPC over the software emulation (the abstract's "up to 4x" is a 164// per-kernel maximum, not the geomean -- cite 2.42x). 165// 166// Reading an INACTIVE lane returns 0 here. CUDA leaves that undefined; we define it, so 167// a divergent shuffle stays deterministic and replayable instead of exposing stale state. 168 169const NX_SIMT_OPCODE_W: i64 = 0x2B 170 171const NX_SIMT_W_SHFL: i64 = 0 // shfl rd, rs1, rs2 -- rd[t] = rs1[ rs2[t] ] 172const NX_SIMT_W_SHFL_UP: i64 = 1 // shfl.up rd, rs1, rs2 -- rd[t] = rs1[t - rs2[t]] 173const NX_SIMT_W_SHFL_DOWN:i64 = 2 // shfl.down rd, rs1, rs2 -- rd[t] = rs1[t + rs2[t]] 174const NX_SIMT_W_SHFL_XOR: i64 = 3 // shfl.xor rd, rs1, rs2 -- rd[t] = rs1[t ^ rs2[t]] 175const NX_SIMT_W_BALLOT: i64 = 4 // ballot rd, rs1 -- rd = mask of lanes with rs1 != 0 176const NX_SIMT_W_ALL: i64 = 5 // all rd, rs1 -- rd = 1 if every active lane nonzero 177const NX_SIMT_W_ANY: i64 = 6 // any rd, rs1 -- rd = 1 if any active lane nonzero 178const NX_SIMT_W_ACTIVEMASK: i64 = 7 // activemask rd -- rd = the current thread mask 179 180// ===== Texture sample unit: RISC-V custom-3, R4-type ================================================= 181// The second-largest block on a real Vortex core: MICRO'21 Figure 15 puts the texture unit at 182// 20 percent of core area, behind only the D-cache at 35 percent. 183// 184// ORACLE, recovered from MICRO'21 Figure 20 (execution time in ms, single core): 185// point SW 128 HW 127 = 1.01x (no benefit) 186// bilinear SW 508 HW 265 = 1.92x ("almost 2x on a single core" in their prose) 187// trilinear SW 560 HW 468 = 1.20x 188// Point sampling shows NO benefit because their source texture is already RGBA, so the 189// software path degenerates to a copy. Trilinear gains less because it doubles memory traffic. 190// 191// TWO DESIGN CHOICES COPIED FROM THEIR PAPER BECAUSE THEY ARE MEASURED, NOT AESTHETIC: 192// 1. "The texel sampler implements only bilinear filtering. Point sampling is executed using 193// bilinear filtering with blend values of 0." They judged the muxing and synchronisation 194// for a variable-latency sampler not worth the single cycle saved. We do the same -- ONE 195// filter path, point is bilinear with zero weights. 196// 2. "The texture memory unit first de-duplicates memory accesses that are repeated across 197// threads." We already have that machinery: nx_simt_coalesce. 198// 199// R4-type is required because a sample needs THREE sources (u, v, lod); rs3 sits in bits 31:27, 200// exactly as the base ISA's FMA encoding does. Vortex documents opcode 0x5B for its tex; we 201// take custom-3 (0x7B) so there is zero encoding collision. 202// 203// Coordinates are 24.8 FIXED POINT texel units -- NishiLang has no float type, and a GPU 204// texture unit is fixed-point in hardware anyway. Texels are packed RGBA8888. 205 206const NX_SIMT_OPCODE_TEX: i64 = 0x7B 207const NX_SIMT_TEX_FRACBITS: i64 = 8 // 24.8 fixed point 208const NX_SIMT_TEX_FRACMASK: i64 = 255 209const NX_SIMT_LAT_TEX_FILTER: i64 = 2 // their sampler's two-cycle bilinear interpolation 210 211// ===== Status codes ================================================= 212 213const NX_SIMT_OK: i64 = 0 214const NX_SIMT_E_ARG: i64 = 1 // bad argument / unconfigured core 215const NX_SIMT_E_IPDOM: i64 = 2 // IPDOM stack overflow or join-without-split 216const NX_SIMT_E_ILLEGAL: i64 = 3 // illegal instruction 217const NX_SIMT_E_BOUNDS: i64 = 4 // memory access out of the modelled aperture 218 219// ===== The core ================================================= 220// 221// All state is flat i64 arrays allocated once at init (rule: never allocate in the hot 222// loop -- sys_mmap is page-granular with no free, so a per-step allocation leaks). 223// 224// Indexing conventions: 225// regs[(w * nt + t) * NX_SIMT_NREGS + r] thread t of warp w, register r 226// ipdom_*[w * NX_SIMT_IPDOM_DEPTH + sp] divergence stack entry sp of warp w 227 228struct NxSimtCore { 229 nw: i64 // warps per core (runtime config) 230 nt: i64 // threads per warp (runtime config) 231 232 regs: *i64 // nw * nt * NX_SIMT_NREGS 233 pc: *i64 // nw -- one program counter per warp 234 tmask: *i64 // nw -- active-thread bitmask per warp 235 wactive: *i64 // nw -- 1 = warp is running 236 wstall_until: *i64 // nw -- cycle at which a memory-stalled warp may issue 237 wbarrier: *i64 // nw -- 1 = parked at a barrier 238 239 ipdom_mask: *i64 // nw * NX_SIMT_IPDOM_DEPTH -- saved thread mask 240 ipdom_pc: *i64 // nw * NX_SIMT_IPDOM_DEPTH -- saved resume PC 241 ipdom_ft: *i64 // nw * NX_SIMT_IPDOM_DEPTH -- 1 = fall-through entry 242 ipdom_sp: *i64 // nw -- divergence stack pointer 243 244 bar_count: *i64 // NX_SIMT_NBARRIERS -- warps still expected at this barrier 245 bar_mask: *i64 // NX_SIMT_NBARRIERS -- bitmask of warps parked here 246 247 // ----- GLOBAL (inter-core) barriers ----- 248 // These three point at CLUSTER-OWNED arrays so every core arrives at the same counter. 249 // Release is performed by the cluster loop, not by a core: a core cannot wake a warp 250 // that belongs to a different core, and pretending otherwise is how you get a barrier 251 // that releases locally and hangs globally. 252 gbar_on: i64 // 1 = wired into a cluster 253 gbar_count: *i64 // NX_SIMT_NBARRIERS, shared -- warps arrived so far 254 gbar_expect: *i64 // NX_SIMT_NBARRIERS, shared -- warps required 255 wbar_id: *i64 // NX_SIMT_MAX_WARPS -- which global barrier a warp waits on, -1 = none 256 257 // Lane-exchange staging for the warp-collective instructions. A shuffle is a 258 // PERMUTATION, so every lane's source value must be snapshotted BEFORE any 259 // destination is written -- writing in place would let an early lane clobber a 260 // source a later lane still needs. Allocated once at init, never in the hot loop. 261 lanebuf: *i64 // NX_SIMT_MAX_THREADS 262 263 // ----- memory system configuration (Vortex Fig 21 parameter space) ----- 264 // Latency alone is not a memory system: with only latency modelled, unlimited requests 265 // proceed in parallel and BANDWIDTH never binds, so adding warps looks free forever. 266 // Channels give the model a throughput ceiling -- each in-flight request occupies a 267 // channel, so once every channel is busy, extra warps queue instead of overlapping. 268 mem_lat: i64 // cycles to first data (NX_SIMT_LAT_MEM_SHORT or _LONG) 269 nchan: i64 // memory channels 270 chan_free: *i64 // NX_SIMT_MAX_CHANNELS -- cycle each channel next goes idle 271 272 // ----- register scoreboard (RAW dependency stalls) ----- 273 // reg_ready[w*NREGS + r] = the cycle at which register r of warp w holds its result. 274 // A warp cannot ISSUE until every source it reads is ready. This is what makes a 275 // multi-cycle unit cost anything: without it, an ALU result feeds the very next 276 // instruction and pipeline depth is invisible. It is also the mechanism by which EXTRA 277 // WARPS EARN THEIR KEEP -- one warp's dependency bubble is another warp's issue slot. 278 scoreboard_on: i64 279 reg_ready: *i64 // NX_SIMT_MAX_WARPS * NX_SIMT_NREGS 280 dep_stall_cycles: i64 // cycles where the picked-over warps were all dependency-blocked 281 282 // ----- L1 data cache ----- 283 cache_on: i64 // 0 = bypass (the old cacheless behaviour, kept for A/B) 284 cache_tag: *i64 // SETS*WAYS -- line address, or -1 for an invalid way 285 cache_age: *i64 // SETS*WAYS -- LRU stamp 286 linebuf: *i64 // NX_SIMT_MAX_THREADS -- distinct line addresses this access 287 cache_hits: i64 288 cache_misses: i64 289 290 // ----- texture unit state (Vortex keeps this in CSRs; we keep it in the core) ----- 291 tex_base: i64 // byte address of texel (0,0) 292 tex_w: i64 // texels 293 tex_h: i64 294 tex_ops: i64 // tex instructions executed 295 tex_texels: i64 // DISTINCT texel fetches after de-duplication 296 297 mem_base: i64 298 mem_buf: *u8 299 mem_size: i64 300 301 rr_next: i64 // round-robin scheduler cursor 302 303 valid: i64 304 halted: i64 305 status: i64 306 307 // ----- instrumentation: the numbers the benchmark reads ----- 308 cycles: i64 // total scheduler cycles elapsed 309 instret: i64 // WARP-instructions retired (Vortex's IPC numerator) 310 lane_instret: i64 // LANE-instructions retired (actual work performed) 311 stall_cycles: i64 // cycles where no warp could issue 312 mem_ops: i64 // warp-level memory instructions executed 313 bank_conflict_cycles: i64 // cycles lost purely to bank serialisation 314 mem_queue_cycles: i64 // cycles spent waiting for a free channel (bandwidth, not latency) 315} 316 317// ===== Mask helpers ================================================= 318// Defined before every user: this toolchain is single-pass, so a call that precedes its 319// definition is a build failure, not a link-time fixup. 320 321// All-ones mask for nt threads. Built by shifting rather than a literal so it stays 322// correct for every configured width. 323func nx_simt_full_mask(nt: i64) -> i64 { 324 var m: i64 = 0 325 var i: i64 = 0 326 while i < nt { m = m | (1 << i); i = i + 1 } 327 return m 328} 329 330func nx_simt_popcount(m: i64) -> i64 { 331 var n: i64 = 0 332 var v: i64 = m 333 while v != 0 { 334 if (v & 1) != 0 { n = n + 1 } 335 v = v >> 1 336 } 337 return n 338} 339 340// Lowest active lane -- the lane that supplies warp-scope operands (thread mask, warp 341// count, barrier id). Returns -1 when the mask is empty. 342func nx_simt_lowest_lane(tmask: i64) -> i64 { 343 var t: i64 = 0 344 while t < NX_SIMT_MAX_THREADS { 345 if (tmask & (1 << t)) != 0 { return t } 346 t = t + 1 347 } 348 return 0 - 1 349} 350 351// ===== Allocation + init ================================================= 352// 353// One allocation pass sized to the STRUCTURAL maximum, so a core can be reconfigured 354// across the Vortex comparison points without reallocating. 355 356func nx_simt_alloc(c: *NxSimtCore) -> i64 { 357 if (c as i64) == 0 { return 0 - NX_SIMT_E_ARG } 358 let nregs: i64 = NX_SIMT_MAX_WARPS * NX_SIMT_MAX_THREADS * NX_SIMT_NREGS 359 let nipd: i64 = NX_SIMT_MAX_WARPS * NX_SIMT_IPDOM_DEPTH 360 c.regs = (sys_mmap(8 * nregs)) as *i64 361 c.pc = (sys_mmap(8 * NX_SIMT_MAX_WARPS)) as *i64 362 c.tmask = (sys_mmap(8 * NX_SIMT_MAX_WARPS)) as *i64 363 c.wactive = (sys_mmap(8 * NX_SIMT_MAX_WARPS)) as *i64 364 c.wstall_until = (sys_mmap(8 * NX_SIMT_MAX_WARPS)) as *i64 365 c.wbarrier = (sys_mmap(8 * NX_SIMT_MAX_WARPS)) as *i64 366 c.ipdom_mask = (sys_mmap(8 * nipd)) as *i64 367 c.ipdom_pc = (sys_mmap(8 * nipd)) as *i64 368 c.ipdom_ft = (sys_mmap(8 * nipd)) as *i64 369 c.ipdom_sp = (sys_mmap(8 * NX_SIMT_MAX_WARPS)) as *i64 370 c.bar_count = (sys_mmap(8 * NX_SIMT_NBARRIERS)) as *i64 371 c.bar_mask = (sys_mmap(8 * NX_SIMT_NBARRIERS)) as *i64 372 c.lanebuf = (sys_mmap(8 * NX_SIMT_MAX_THREADS)) as *i64 373 c.chan_free = (sys_mmap(8 * NX_SIMT_MAX_CHANNELS)) as *i64 374 c.cache_tag = (sys_mmap(8 * NX_SIMT_CACHE_SETS * NX_SIMT_CACHE_WAYS)) as *i64 375 c.cache_age = (sys_mmap(8 * NX_SIMT_CACHE_SETS * NX_SIMT_CACHE_WAYS)) as *i64 376 c.linebuf = (sys_mmap(8 * NX_SIMT_MAX_THREADS)) as *i64 377 c.reg_ready = (sys_mmap(8 * NX_SIMT_MAX_WARPS * NX_SIMT_NREGS)) as *i64 378 c.wbar_id = (sys_mmap(8 * NX_SIMT_MAX_WARPS)) as *i64 379 return NX_SIMT_OK 380} 381 382// Configure and reset. entry_pc is where warp 0 begins; every other warp starts 383// INACTIVE, exactly as a real SIMT core boots -- warp 0 spawns the rest with wspawn. 384 385func nx_simt_init(c: *NxSimtCore, nw: i64, nt: i64, 386 mem_base: i64, mem_buf: *u8, mem_size: i64, 387 entry_pc: i64) -> i64 { 388 if (c as i64) == 0 { return 0 - NX_SIMT_E_ARG } 389 if nw < 1 { return 0 - NX_SIMT_E_ARG } 390 if nt < 1 { return 0 - NX_SIMT_E_ARG } 391 if nw > NX_SIMT_MAX_WARPS { return 0 - NX_SIMT_E_ARG } 392 if nt > NX_SIMT_MAX_THREADS { return 0 - NX_SIMT_E_ARG } 393 if (mem_buf as i64) == 0 { return 0 - NX_SIMT_E_ARG } 394 if mem_size <= 0 { return 0 - NX_SIMT_E_ARG } 395 396 c.nw = nw 397 c.nt = nt 398 c.mem_base = mem_base 399 c.mem_buf = mem_buf 400 c.mem_size = mem_size 401 402 var w: i64 = 0 403 while w < NX_SIMT_MAX_WARPS { 404 c.pc[w] = 0 405 c.tmask[w] = 0 406 c.wactive[w] = 0 407 c.wstall_until[w] = 0 408 c.wbarrier[w] = 0 409 c.ipdom_sp[w] = 0 410 c.wbar_id[w] = 0 - 1 411 w = w + 1 412 } 413 c.gbar_on = 0 414 var i: i64 = 0 415 let nregs: i64 = NX_SIMT_MAX_WARPS * NX_SIMT_MAX_THREADS * NX_SIMT_NREGS 416 while i < nregs { c.regs[i] = 0; i = i + 1 } 417 var b: i64 = 0 418 while b < NX_SIMT_NBARRIERS { c.bar_count[b] = 0; c.bar_mask[b] = 0; b = b + 1 } 419 420 // Warp 0 boots with every thread active; all other warps are dormant. 421 c.pc[0] = entry_pc 422 c.tmask[0] = nx_simt_full_mask(nt) 423 c.wactive[0] = 1 424 425 // Memory system defaults to the OPTIMISTIC corner (short latency, wide channels). 426 // Callers comparing against Vortex must set the corner explicitly with 427 // nx_simt_set_memory -- a benchmark that silently uses the fastest memory in the 428 // parameter space will flatter every configuration equally and hide the effect. 429 c.mem_lat = NX_SIMT_LAT_MEM_SHORT 430 c.nchan = NX_SIMT_CHAN_HIGH 431 var ch: i64 = 0 432 while ch < NX_SIMT_MAX_CHANNELS { c.chan_free[ch] = 0; ch = ch + 1 } 433 434 // Cache starts ON and cold. Every way invalid (-1 is not a reachable line address 435 // because addresses are non-negative), every LRU stamp zero. 436 c.cache_on = 1 437 var cw: i64 = 0 438 let nways: i64 = NX_SIMT_CACHE_SETS * NX_SIMT_CACHE_WAYS 439 while cw < nways { c.cache_tag[cw] = 0 - 1; c.cache_age[cw] = 0; cw = cw + 1 } 440 c.cache_hits = 0 441 c.cache_misses = 0 442 443 // Scoreboard on by default: a core without RAW stalls is not a pipeline. 444 c.tex_base = 0 445 c.tex_w = 0 446 c.tex_h = 0 447 c.tex_ops = 0 448 c.tex_texels = 0 449 450 c.scoreboard_on = 1 451 c.dep_stall_cycles = 0 452 var rr: i64 = 0 453 let nrr: i64 = NX_SIMT_MAX_WARPS * NX_SIMT_NREGS 454 while rr < nrr { c.reg_ready[rr] = 0; rr = rr + 1 } 455 456 c.rr_next = 0 457 c.valid = 1 458 c.halted = 0 459 c.status = NX_SIMT_OK 460 c.cycles = 0 461 c.instret = 0 462 c.lane_instret = 0 463 c.stall_cycles = 0 464 c.mem_ops = 0 465 c.bank_conflict_cycles = 0 466 c.mem_queue_cycles = 0 467 return NX_SIMT_OK 468} 469 470// ===== Per-thread register file ================================================= 471// x0 is hardwired zero, per RISC-V. A write to a masked-off thread never reaches here. 472 473func nx_simt_rf_read(c: *NxSimtCore, w: i64, t: i64, r: i64) -> i64 { 474 if r == 0 { return 0 } 475 if r < 0 { return 0 } 476 if r >= NX_SIMT_NREGS { return 0 } 477 return c.regs[(w * c.nt + t) * NX_SIMT_NREGS + r] 478} 479 480func nx_simt_rf_write(c: *NxSimtCore, w: i64, t: i64, r: i64, v: i64) -> i64 { 481 if r == 0 { return NX_SIMT_OK } 482 if r < 0 { return 0 - NX_SIMT_E_ARG } 483 if r >= NX_SIMT_NREGS { return 0 - NX_SIMT_E_ARG } 484 c.regs[(w * c.nt + t) * NX_SIMT_NREGS + r] = v 485 return NX_SIMT_OK 486} 487 488// ===== Memory ================================================= 489// A flat aperture. Out-of-range accesses are refused, never wrapped -- a silent wrap 490// would manufacture wrong results that look like a working kernel. 491 492func nx_simt_in_range(c: *NxSimtCore, addr: i64, width: i64) -> i64 { 493 let off: i64 = addr - c.mem_base 494 if off < 0 { return 0 } 495 if off + width > c.mem_size { return 0 } 496 return 1 497} 498 499func nx_simt_load32(c: *NxSimtCore, addr: i64) -> i64 { 500 if nx_simt_in_range(c, addr, 4) == 0 { c.status = NX_SIMT_E_BOUNDS; return 0 } 501 let o: i64 = addr - c.mem_base 502 let b0: i64 = c.mem_buf[o] as i64 503 let b1: i64 = c.mem_buf[o + 1] as i64 504 let b2: i64 = c.mem_buf[o + 2] as i64 505 let b3: i64 = c.mem_buf[o + 3] as i64 506 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 507} 508 509func nx_simt_store32(c: *NxSimtCore, addr: i64, v: i64) -> i64 { 510 if nx_simt_in_range(c, addr, 4) == 0 { c.status = NX_SIMT_E_BOUNDS; return 0 - NX_SIMT_E_BOUNDS } 511 let o: i64 = addr - c.mem_base 512 c.mem_buf[o] = (v & 0xff) as u8 513 c.mem_buf[o + 1] = ((v >> 8) & 0xff) as u8 514 c.mem_buf[o + 2] = ((v >> 16) & 0xff) as u8 515 c.mem_buf[o + 3] = ((v >> 24) & 0xff) as u8 516 return NX_SIMT_OK 517} 518 519func nx_simt_load64(c: *NxSimtCore, addr: i64) -> i64 { 520 let lo: i64 = nx_simt_load32(c, addr) & 0xffffffff 521 let hi: i64 = nx_simt_load32(c, addr + 4) & 0xffffffff 522 return lo | (hi << 32) 523} 524 525func nx_simt_store64(c: *NxSimtCore, addr: i64, v: i64) -> i64 { 526 nx_simt_store32(c, addr, v & 0xffffffff) 527 nx_simt_store32(c, addr + 4, (v >> 32) & 0xffffffff) 528 return NX_SIMT_OK 529} 530 531// ===== Bank-conflict model ================================================= 532// 533// Vortex keeps the data-cache bank count equal to the threads-per-warp count (CARRV'19: 534// "The number of banks is always kept the same as the number of threads in a warp"). A 535// warp-wide memory op therefore issues nt addresses into nt word-interleaved banks. 536// 537// Cost = the WORST per-bank load, in accesses. A unit-stride (coalesced) access spreads 538// one address per bank and costs 1. A stride-nt access piles every thread onto the same 539// bank and costs nt. This is the mechanism behind the paper's reported bank utilisation 540// (sgemm 67 percent, vecadd 71 percent at one port). 541// 542// bank_hits is caller-provided scratch of NX_SIMT_MAX_THREADS entries -- never allocated 543// here, because this runs once per memory instruction. 544 545func nx_simt_bank_cost(c: *NxSimtCore, addrs: *i64, active: i64, bank_hits: *i64) -> i64 { 546 var i: i64 = 0 547 while i < c.nt { bank_hits[i] = 0; i = i + 1 } 548 var t: i64 = 0 549 while t < c.nt { 550 if (active & (1 << t)) != 0 { 551 let word: i64 = addrs[t] >> 2 552 var bank: i64 = word % c.nt 553 if bank < 0 { bank = 0 - bank } 554 bank_hits[bank] = bank_hits[bank] + 1 555 } 556 t = t + 1 557 } 558 var worst: i64 = 0 559 var b: i64 = 0 560 while b < c.nt { 561 if bank_hits[b] > worst { worst = bank_hits[b] } 562 b = b + 1 563 } 564 if worst < 1 { worst = 1 } 565 return worst 566} 567 568// ===== Memory system configuration ================================================= 569// Select a corner of Vortex's published parameter space. lat should be 570// NX_SIMT_LAT_MEM_SHORT (their "sh") or NX_SIMT_LAT_MEM_LONG (their "lg"); nchan should be 571// NX_SIMT_CHAN_LOW ("2c") or NX_SIMT_CHAN_HIGH ("8c"). 572 573func nx_simt_set_memory(c: *NxSimtCore, lat: i64, nchan: i64) -> i64 { 574 if c.valid != 1 { return 0 - NX_SIMT_E_ARG } 575 if lat < 1 { return 0 - NX_SIMT_E_ARG } 576 if nchan < 1 { return 0 - NX_SIMT_E_ARG } 577 if nchan > NX_SIMT_MAX_CHANNELS { return 0 - NX_SIMT_E_ARG } 578 c.mem_lat = lat 579 c.nchan = nchan 580 var i: i64 = 0 581 while i < NX_SIMT_MAX_CHANNELS { c.chan_free[i] = 0; i = i + 1 } 582 return NX_SIMT_OK 583} 584 585// Enable or bypass the L1. Bypass exists so the cache's effect can be measured as an A/B 586// on one binary rather than argued for. 587func nx_simt_set_cache(c: *NxSimtCore, on: i64) -> i64 { 588 if c.valid != 1 { return 0 - NX_SIMT_E_ARG } 589 c.cache_on = on 590 var i: i64 = 0 591 let nways: i64 = NX_SIMT_CACHE_SETS * NX_SIMT_CACHE_WAYS 592 while i < nways { c.cache_tag[i] = 0 - 1; c.cache_age[i] = 0; i = i + 1 } 593 c.cache_hits = 0 594 c.cache_misses = 0 595 return NX_SIMT_OK 596} 597 598// Probe the L1 for one line address; install it on a miss. Returns 1 = hit, 0 = miss. 599// Set-associative with LRU victim selection, stamped by cycle. 600 601func nx_simt_cache_probe(c: *NxSimtCore, line: i64) -> i64 { 602 var set: i64 = line % NX_SIMT_CACHE_SETS 603 if set < 0 { set = 0 - set } 604 let base: i64 = set * NX_SIMT_CACHE_WAYS 605 606 var w: i64 = 0 607 while w < NX_SIMT_CACHE_WAYS { 608 if c.cache_tag[base + w] == line { 609 c.cache_age[base + w] = c.cycles 610 c.cache_hits = c.cache_hits + 1 611 return 1 612 } 613 w = w + 1 614 } 615 616 // Miss: prefer an invalid way, else evict the least recently used. 617 var victim: i64 = 0 618 var oldest: i64 = c.cache_age[base] 619 w = 0 620 while w < NX_SIMT_CACHE_WAYS { 621 if c.cache_tag[base + w] < 0 { victim = w; w = NX_SIMT_CACHE_WAYS } else { 622 if c.cache_age[base + w] < oldest { oldest = c.cache_age[base + w]; victim = w } 623 w = w + 1 624 } 625 } 626 c.cache_tag[base + victim] = line 627 c.cache_age[base + victim] = c.cycles 628 c.cache_misses = c.cache_misses + 1 629 return 0 630} 631 632// Collapse a warp's per-lane addresses to DISTINCT cache lines. This is what memory 633// coalescing physically is: lanes touching the same line cost one access, not N. Returns 634// the distinct-line count and fills c.linebuf. 635func nx_simt_coalesce(c: *NxSimtCore, addrs: *i64, active: i64) -> i64 { 636 var n: i64 = 0 637 var t: i64 = 0 638 while t < c.nt { 639 if (active & (1 << t)) != 0 { 640 let line: i64 = addrs[t] / NX_SIMT_CACHE_LINE_B 641 var seen: i64 = 0 642 var i: i64 = 0 643 while i < n { 644 if c.linebuf[i] == line { seen = 1 } 645 i = i + 1 646 } 647 if seen == 0 { c.linebuf[n] = line; n = n + 1 } 648 } 649 t = t + 1 650 } 651 if n < 1 { n = 1 } 652 return n 653} 654 655// Issue one warp-wide memory request through the channel array. 656// 657// The request takes the earliest-available channel and OCCUPIES it for `cost` cycles, 658// where cost is the bank-conflict serialisation factor. The requesting warp resumes 659// `mem_lat` cycles after its request actually starts -- not after it was issued. That 660// distinction is the whole bandwidth model: under contention a request waits for a free 661// channel first, so its total stall grows with load even though latency is constant. 662 663func nx_simt_mem_issue(c: *NxSimtCore, w: i64, cost: i64, nmiss: i64) -> i64 { 664 var best: i64 = 0 665 var best_free: i64 = c.chan_free[0] 666 var i: i64 = 1 667 while i < c.nchan { 668 if c.chan_free[i] < best_free { best_free = c.chan_free[i]; best = i } 669 i = i + 1 670 } 671 var start: i64 = c.cycles 672 if best_free > start { start = best_free } 673 // A channel is held for one burst per MISSING line -- hits never reach the channel. 674 c.chan_free[best] = start + nmiss * NX_SIMT_BURST_CYCLES 675 c.wstall_until[w] = start + c.mem_lat + cost 676 c.mem_ops = c.mem_ops + 1 677 c.bank_conflict_cycles = c.bank_conflict_cycles + (cost - 1) 678 c.mem_queue_cycles = c.mem_queue_cycles + (start - c.cycles) 679 return NX_SIMT_OK 680} 681 682// One warp-wide data access: coalesce to distinct lines, probe the L1, and go to memory 683// only for the lines that miss. A fully-resident access never touches a channel at all -- 684// which is exactly the effect that lets a serialised software path with high reuse beat a 685// per-lane hardware path, the mse_forward crossover a cacheless model cannot show. 686func nx_simt_mem_access(c: *NxSimtCore, w: i64, addrs: *i64, bank_hits: *i64) -> i64 { 687 let cost: i64 = nx_simt_bank_cost(c, addrs, c.tmask[w], bank_hits) 688 if c.cache_on != 1 { 689 return nx_simt_mem_issue(c, w, cost, cost) 690 } 691 let nline: i64 = nx_simt_coalesce(c, addrs, c.tmask[w]) 692 var nmiss: i64 = 0 693 var i: i64 = 0 694 while i < nline { 695 if nx_simt_cache_probe(c, c.linebuf[i]) == 0 { nmiss = nmiss + 1 } 696 i = i + 1 697 } 698 if nmiss == 0 { 699 // Every line resident: L1 hit latency, no channel occupancy, no queueing. 700 c.mem_ops = c.mem_ops + 1 701 c.wstall_until[w] = c.cycles + NX_SIMT_LAT_CACHE_HIT 702 return NX_SIMT_OK 703 } 704 return nx_simt_mem_issue(c, w, cost, nmiss) 705} 706 707// ===== Texture sampler ================================================= 708 709func nx_simt_set_texture(c: *NxSimtCore, base: i64, w: i64, h: i64) -> i64 { 710 if c.valid != 1 { return 0 - NX_SIMT_E_ARG } 711 if w < 1 { return 0 - NX_SIMT_E_ARG } 712 if h < 1 { return 0 - NX_SIMT_E_ARG } 713 c.tex_base = base 714 c.tex_w = w 715 c.tex_h = h 716 return NX_SIMT_OK 717} 718 719// Per-channel lerp over packed RGBA8888. 720// ★Written as (a*(256-f) + b*f) >> 8 rather than a + ((b-a)*f >> 8) DELIBERATELY: the second 721// form needs (b-a) which goes NEGATIVE whenever the texture darkens, and NishiLang's right 722// shift is LOGICAL -- a negative delta would shift in high bits and produce a bright band 723// exactly where the gradient falls. Both terms here are non-negative by construction. 724// Max intermediate is 255*256 = 65280, so no overflow. 725func nx_simt_lerp_rgba(a: i64, b: i64, f: i64) -> i64 { 726 var out: i64 = 0 727 var sh: i64 = 0 728 while sh < 32 { 729 let ca: i64 = (a >> sh) & 0xff 730 let cb: i64 = (b >> sh) & 0xff 731 let cc: i64 = (ca * (256 - f) + cb * f) >> NX_SIMT_TEX_FRACBITS 732 out = out | ((cc & 0xff) << sh) 733 sh = sh + 8 734 } 735 return out 736} 737 738// Fetch one texel with clamp-to-edge addressing. 739func nx_simt_texel(c: *NxSimtCore, ix: i64, iy: i64) -> i64 { 740 var x: i64 = ix 741 var y: i64 = iy 742 if x < 0 { x = 0 } 743 if y < 0 { y = 0 } 744 if x >= c.tex_w { x = c.tex_w - 1 } 745 if y >= c.tex_h { y = c.tex_h - 1 } 746 return nx_simt_load32(c, c.tex_base + (y * c.tex_w + x) * 4) & 0xffffffff 747} 748 749// Bilinear sample at 24.8 fixed-point texel coordinates. 750// Point sampling is this same path with fu = fv = 0, exactly as Vortex does it -- one filter, 751// no variable-latency mux. 752func nx_simt_tex_sample(c: *NxSimtCore, u: i64, v: i64) -> i64 { 753 let iu: i64 = u >> NX_SIMT_TEX_FRACBITS 754 let iv: i64 = v >> NX_SIMT_TEX_FRACBITS 755 let fu: i64 = u & NX_SIMT_TEX_FRACMASK 756 let fv: i64 = v & NX_SIMT_TEX_FRACMASK 757 let t00: i64 = nx_simt_texel(c, iu, iv) 758 let t10: i64 = nx_simt_texel(c, iu + 1, iv) 759 let t01: i64 = nx_simt_texel(c, iu, iv + 1) 760 let t11: i64 = nx_simt_texel(c, iu + 1, iv + 1) 761 let top: i64 = nx_simt_lerp_rgba(t00, t10, fu) 762 let bot: i64 = nx_simt_lerp_rgba(t01, t11, fu) 763 return nx_simt_lerp_rgba(top, bot, fv) 764} 765 766// ===== IPDOM divergence stack ================================================= 767// 768// The exact protocol published for Vortex (arXiv 2002.12151, restated in MICRO'21): 769// 770// SPLIT, when threads DISAGREE on the predicate: 771// 1) push the CURRENT thread mask as a FALL-THROUGH entry 772// 2) push {threads whose predicate is FALSE, PC+4} as a non-fall-through entry 773// 3) continue with the mask set to the threads whose predicate is TRUE 774// SPLIT, when all active threads AGREE or only one is active: acts as a NOP. 775// 776// JOIN pops ONE entry: 777// - non-fall-through -> jump to the stored PC with the stored mask (run the else side) 778// - fall-through -> continue to PC+4 with the stored mask (reconverged) 779// 780// So a fully divergent region executes TWO joins: the first switches to the else path, 781// the second restores the pre-split mask. Getting this wrong is the classic SIMT bug -- 782// it silently drops half the threads' work. 783 784func nx_simt_ipdom_push(c: *NxSimtCore, w: i64, mask: i64, pc: i64, ft: i64) -> i64 { 785 let sp: i64 = c.ipdom_sp[w] 786 if sp >= NX_SIMT_IPDOM_DEPTH { c.status = NX_SIMT_E_IPDOM; return 0 - NX_SIMT_E_IPDOM } 787 let idx: i64 = w * NX_SIMT_IPDOM_DEPTH + sp 788 c.ipdom_mask[idx] = mask 789 c.ipdom_pc[idx] = pc 790 c.ipdom_ft[idx] = ft 791 c.ipdom_sp[w] = sp + 1 792 return NX_SIMT_OK 793} 794 795func nx_simt_do_split(c: *NxSimtCore, w: i64, rs1: i64) -> i64 { 796 let cur: i64 = c.tmask[w] 797 var taken: i64 = 0 798 var t: i64 = 0 799 while t < c.nt { 800 if (cur & (1 << t)) != 0 { 801 if nx_simt_rf_read(c, w, t, rs1) != 0 { taken = taken | (1 << t) } 802 } 803 t = t + 1 804 } 805 let nottaken: i64 = cur & (0 - 1 ^ taken) 806 807 // No divergence: every active thread agrees, or only one is active. NOP. 808 if taken == 0 { return NX_SIMT_OK } 809 if nottaken == 0 { return NX_SIMT_OK } 810 if nx_simt_popcount(cur) < 2 { return NX_SIMT_OK } 811 812 let r1: i64 = nx_simt_ipdom_push(c, w, cur, c.pc[w] + 4, 1) 813 if r1 != NX_SIMT_OK { return r1 } 814 let r2: i64 = nx_simt_ipdom_push(c, w, nottaken, c.pc[w] + 4, 0) 815 if r2 != NX_SIMT_OK { return r2 } 816 c.tmask[w] = taken 817 return NX_SIMT_OK 818} 819 820// Returns 1 if the join redirected the PC (caller must not apply pc+4), else 0. 821// 822// DELIBERATE DEVIATION, documented: CARRV'19 says a join with no matching split raises an 823// exception. We make it a no-op instead, because it is REACHABLE IN CORRECT CODE -- a 824// split whose predicate is uniform across the warp legitimately does not push (arXiv 825// 2002.12151), so the matching join of a correctly-balanced pair finds an empty stack 826// whenever the branch happens to be uniform at run time. Faulting there would turn a 827// data-dependent uniform branch into a spurious exception. 828func nx_simt_do_join(c: *NxSimtCore, w: i64) -> i64 { 829 let sp: i64 = c.ipdom_sp[w] 830 if sp <= 0 { return 0 } 831 let nsp: i64 = sp - 1 832 c.ipdom_sp[w] = nsp 833 let idx: i64 = w * NX_SIMT_IPDOM_DEPTH + nsp 834 c.tmask[w] = c.ipdom_mask[idx] 835 if c.ipdom_ft[idx] == 0 { 836 c.pc[w] = c.ipdom_pc[idx] 837 return 1 838 } 839 return 0 840} 841 842// ===== Barriers ================================================= 843// 844// A barrier releases when the expected number of warps have arrived. Until then each 845// arriving warp parks (removed from scheduler contention). On release every parked warp 846// is woken at once -- that is the whole point of a hardware barrier. 847 848func nx_simt_do_bar(c: *NxSimtCore, w: i64, bar_id: i64, expect: i64) -> i64 { 849 var id: i64 = bar_id & (NX_SIMT_NBARRIERS - 1) 850 if id < 0 { id = 0 } 851 852 // GLOBAL scope: arrive on the shared counter and park. The CLUSTER releases, because a 853 // core has no way to wake a warp on another core. 854 if (bar_id & NX_SIMT_BAR_GLOBAL) != 0 { 855 if c.gbar_on == 1 { 856 c.gbar_count[id] = c.gbar_count[id] + 1 857 c.gbar_expect[id] = expect 858 c.wbarrier[w] = 1 859 c.wbar_id[w] = id 860 return NX_SIMT_OK 861 } 862 // ⚠A global barrier on a core that is NOT in a cluster degrades to a LOCAL one. 863 // Declared here rather than silently: a single-core run of a multi-core kernel then 864 // still completes, but it has NOT tested inter-core synchronisation. 865 } 866 c.bar_mask[id] = c.bar_mask[id] | (1 << w) 867 c.bar_count[id] = c.bar_count[id] + 1 868 if c.bar_count[id] >= expect { 869 // Release: wake every parked warp, reset the slot. 870 var i: i64 = 0 871 while i < c.nw { 872 if (c.bar_mask[id] & (1 << i)) != 0 { c.wbarrier[i] = 0 } 873 i = i + 1 874 } 875 c.bar_mask[id] = 0 876 c.bar_count[id] = 0 877 } else { 878 c.wbarrier[w] = 1 879 } 880 return NX_SIMT_OK 881} 882 883// ===== Warp scheduler ================================================= 884// 885// One warp issues per cycle. A warp is eligible when it is active, not parked at a 886// barrier, and not waiting out a memory stall. Round-robin from a rotating cursor gives 887// every warp equal access -- the property that lets extra warps hide memory latency. 888// Returns the warp id, or -1 when nothing can issue this cycle. 889 890// Stamp a destination register as available `lat` cycles from now. 891// x0 is hardwired zero and can never block, so it is never stamped. 892func nx_simt_mark_dest(c: *NxSimtCore, w: i64, rd: i64, lat: i64) -> i64 { 893 if c.scoreboard_on != 1 { return NX_SIMT_OK } 894 if rd <= 0 { return NX_SIMT_OK } 895 if rd >= NX_SIMT_NREGS { return NX_SIMT_OK } 896 c.reg_ready[w * NX_SIMT_NREGS + rd] = c.cycles + lat 897 return NX_SIMT_OK 898} 899 900// Can warp w ISSUE its next instruction, or is it waiting on a source register? 901// This peeks and decodes at the scheduler, which is exactly what a real scoreboard does: 902// the decision to issue is made from the instruction's source fields, before execute. 903func nx_simt_srcs_ready(c: *NxSimtCore, w: i64) -> i64 { 904 if c.scoreboard_on != 1 { return 1 } 905 let pc: i64 = c.pc[w] 906 if nx_simt_in_range(c, pc, 4) == 0 { return 1 } 907 let o: i64 = pc - c.mem_base 908 let b0: i64 = c.mem_buf[o] as i64 909 let b1: i64 = c.mem_buf[o + 1] as i64 910 let b2: i64 = c.mem_buf[o + 2] as i64 911 let b3: i64 = c.mem_buf[o + 3] as i64 912 let inst: i64 = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) 913 if inst == 0 { return 1 } 914 915 let op: i64 = inst & 0x7f 916 var use1: i64 = 1 917 var use2: i64 = 1 918 // Forms with no register sources at all. 919 if op == 0x37 { use1 = 0; use2 = 0 } // LUI 920 if op == 0x17 { use1 = 0; use2 = 0 } // AUIPC 921 if op == 0x6f { use1 = 0; use2 = 0 } // JAL 922 // Forms that read rs1 only. 923 if op == 0x13 { use2 = 0 } // OP-IMM 924 if op == 0x1b { use2 = 0 } // OP-IMM-32 925 if op == 0x03 { use2 = 0 } // LOAD 926 if op == 0x67 { use2 = 0 } // JALR 927 928 let base: i64 = w * NX_SIMT_NREGS 929 if use1 == 1 { 930 let rs1: i64 = nx_rv64im_rs1(inst) 931 if rs1 > 0 { if c.reg_ready[base + rs1] > c.cycles { return 0 } } 932 } 933 if use2 == 1 { 934 let rs2: i64 = nx_rv64im_rs2(inst) 935 if rs2 > 0 { if c.reg_ready[base + rs2] > c.cycles { return 0 } } 936 } 937 return 1 938} 939 940func nx_simt_pick_warp(c: *NxSimtCore) -> i64 { 941 var blocked_on_dep: i64 = 0 942 var i: i64 = 0 943 while i < c.nw { 944 var w: i64 = c.rr_next + i 945 while w >= c.nw { w = w - c.nw } 946 if c.wactive[w] == 1 { 947 if c.wbarrier[w] == 0 { 948 if c.wstall_until[w] <= c.cycles { 949 // Memory-ready, but its sources may still be in flight. 950 if nx_simt_srcs_ready(c, w) == 1 { 951 c.rr_next = w + 1 952 while c.rr_next >= c.nw { c.rr_next = c.rr_next - c.nw } 953 return w 954 } 955 blocked_on_dep = 1 956 } 957 } 958 } 959 i = i + 1 960 } 961 // Distinguish "nothing to run" from "everything is waiting on a RAW hazard" -- the 962 // second is the cost more warps are supposed to absorb, so it must be measurable. 963 if blocked_on_dep == 1 { c.dep_stall_cycles = c.dep_stall_cycles + 1 } 964 return 0 - 1 965} 966 967func nx_simt_any_active(c: *NxSimtCore) -> i64 { 968 var w: i64 = 0 969 while w < c.nw { 970 if c.wactive[w] == 1 { return 1 } 971 w = w + 1 972 } 973 return 0 974} 975 976// ===== One SIMT instruction ================================================= 977// 978// Fetch once for the warp, then execute across every active thread. The thread mask is 979// the only thing standing between a lane and a commit. 980 981func nx_simt_exec_warp(c: *NxSimtCore, w: i64, addrs: *i64, bank_hits: *i64) -> i64 { 982 let pc: i64 = c.pc[w] 983 let inst: i64 = nx_simt_load32(c, pc) & 0xffffffff 984 985 // All-zero word is the halt sentinel, matching the scalar core's convention. 986 if inst == 0 { 987 c.wactive[w] = 0 988 return NX_SIMT_OK 989 } 990 991 let opcode: i64 = inst & 0x7f 992 let rd: i64 = nx_rv64im_rd(inst) 993 let rs1: i64 = nx_rv64im_rs1(inst) 994 let rs2: i64 = nx_rv64im_rs2(inst) 995 let funct3: i64 = nx_rv64im_funct3(inst) 996 let funct7: i64 = nx_rv64im_funct7(inst) 997 let tmask: i64 = c.tmask[w] 998 999 c.instret = c.instret + 1 1000 c.lane_instret = c.lane_instret + nx_simt_popcount(tmask) 1001 1002 // ---------- SIMT extension (custom-0) ---------- 1003 if opcode == NX_SIMT_OPCODE { 1004 if funct3 == NX_SIMT_F3_TMC { 1005 // Thread mask comes from lane 0 -- it is a warp-scope control value. 1006 let lane0: i64 = nx_simt_lowest_lane(tmask) 1007 var nm: i64 = 0 1008 if lane0 >= 0 { nm = nx_simt_rf_read(c, w, lane0, rs1) } 1009 nm = nm & nx_simt_full_mask(c.nt) 1010 c.tmask[w] = nm 1011 if nm == 0 { c.wactive[w] = 0 } 1012 c.pc[w] = pc + 4 1013 return NX_SIMT_OK 1014 } 1015 if funct3 == NX_SIMT_F3_WSPAWN { 1016 let lane0: i64 = nx_simt_lowest_lane(tmask) 1017 var n: i64 = 0 1018 var target: i64 = 0 1019 if lane0 >= 0 { 1020 n = nx_simt_rf_read(c, w, lane0, rs1) 1021 target = nx_simt_rf_read(c, w, lane0, rs2) 1022 } 1023 if n > c.nw { n = c.nw } 1024 var i: i64 = 1 1025 while i < n { 1026 c.wactive[i] = 1 1027 c.pc[i] = target 1028 c.tmask[i] = nx_simt_full_mask(c.nt) 1029 c.ipdom_sp[i] = 0 1030 c.wbarrier[i] = 0 1031 c.wstall_until[i] = 0 1032 i = i + 1 1033 } 1034 c.pc[w] = pc + 4 1035 return NX_SIMT_OK 1036 } 1037 if funct3 == NX_SIMT_F3_SPLIT { 1038 let r: i64 = nx_simt_do_split(c, w, rs1) 1039 if r != NX_SIMT_OK { return r } 1040 c.pc[w] = pc + 4 1041 return NX_SIMT_OK 1042 } 1043 if funct3 == NX_SIMT_F3_JOIN { 1044 let redirected: i64 = nx_simt_do_join(c, w) 1045 if redirected == 0 { c.pc[w] = pc + 4 } 1046 return NX_SIMT_OK 1047 } 1048 if funct3 == NX_SIMT_F3_BAR { 1049 let lane0: i64 = nx_simt_lowest_lane(tmask) 1050 var bid: i64 = 0 1051 var expect: i64 = 1 1052 if lane0 >= 0 { 1053 bid = nx_simt_rf_read(c, w, lane0, rs1) 1054 expect = nx_simt_rf_read(c, w, lane0, rs2) 1055 } 1056 c.pc[w] = pc + 4 1057 nx_simt_do_bar(c, w, bid, expect) 1058 return NX_SIMT_OK 1059 } 1060 if funct3 == NX_SIMT_F3_TID { 1061 var t: i64 = 0 1062 while t < c.nt { 1063 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, t) } 1064 t = t + 1 1065 } 1066 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1067 c.pc[w] = pc + 4 1068 return NX_SIMT_OK 1069 } 1070 if funct3 == NX_SIMT_F3_WID { 1071 var t: i64 = 0 1072 while t < c.nt { 1073 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, w) } 1074 t = t + 1 1075 } 1076 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1077 c.pc[w] = pc + 4 1078 return NX_SIMT_OK 1079 } 1080 if funct3 == NX_SIMT_F3_NTID { 1081 var t: i64 = 0 1082 while t < c.nt { 1083 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, c.nt) } 1084 t = t + 1 1085 } 1086 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1087 c.pc[w] = pc + 4 1088 return NX_SIMT_OK 1089 } 1090 c.status = NX_SIMT_E_ILLEGAL 1091 c.wactive[w] = 0 1092 return 0 - NX_SIMT_E_ILLEGAL 1093 } 1094 1095 // ---------- Texture sample (custom-3, R4-type) ---------- 1096 if opcode == NX_SIMT_OPCODE_TEX { 1097 let rs3: i64 = (inst >> 27) & 0x1f 1098 var t: i64 = 0 1099 // Address generation for every lane first -- the paper's stage 1 converts (u,v) to 1100 // texel addresses "for all the threads in parallel", THEN the memory unit 1101 // de-duplicates across threads before issuing. 1102 while t < c.nt { 1103 addrs[t] = 0 1104 if (tmask & (1 << t)) != 0 { 1105 let u: i64 = nx_simt_rf_read(c, w, t, rs1) 1106 let v: i64 = nx_simt_rf_read(c, w, t, rs2) 1107 var iu: i64 = u >> NX_SIMT_TEX_FRACBITS 1108 var iv: i64 = v >> NX_SIMT_TEX_FRACBITS 1109 if iu < 0 { iu = 0 } 1110 if iv < 0 { iv = 0 } 1111 if iu >= c.tex_w { iu = c.tex_w - 1 } 1112 if iv >= c.tex_h { iv = c.tex_h - 1 } 1113 addrs[t] = c.tex_base + (iv * c.tex_w + iu) * 4 1114 } 1115 t = t + 1 1116 } 1117 // Sample. rs3 carries lod; v1 samples a single level, so lod selects nothing yet -- 1118 // ★DECLARED, NOT SILENT: trilinear in Vortex is a PSEUDO-INSTRUCTION issuing two tex 1119 // ops and lerping on frac(lod), so a single-level sampler is the correct primitive to 1120 // build first, but a caller passing lod!=0 today gets level 0 and must know that. 1121 t = 0 1122 while t < c.nt { 1123 if (tmask & (1 << t)) != 0 { 1124 let u: i64 = nx_simt_rf_read(c, w, t, rs1) 1125 let v: i64 = nx_simt_rf_read(c, w, t, rs2) 1126 nx_simt_rf_write(c, w, t, rd, nx_simt_tex_sample(c, u, v)) 1127 } 1128 t = t + 1 1129 } 1130 // One de-duplicated batch through the memory system, then the sampler's filter cost. 1131 let ntex: i64 = nx_simt_coalesce(c, addrs, tmask) 1132 c.tex_ops = c.tex_ops + 1 1133 c.tex_texels = c.tex_texels + ntex 1134 nx_simt_mem_access(c, w, addrs, bank_hits) 1135 c.wstall_until[w] = c.wstall_until[w] + NX_SIMT_LAT_TEX_FILTER 1136 nx_simt_mark_dest(c, w, rd, c.wstall_until[w] - c.cycles) 1137 c.pc[w] = pc + 4 1138 return NX_SIMT_OK 1139 } 1140 1141 // ---------- Warp collectives (custom-1) ---------- 1142 if opcode == NX_SIMT_OPCODE_W { 1143 // Snapshot every lane's source BEFORE writing any destination. A shuffle is a 1144 // permutation: writing in place lets lane 0's result overwrite a value lane 3 1145 // still needs to read. 1146 var t: i64 = 0 1147 while t < c.nt { 1148 c.lanebuf[t] = 0 1149 if (tmask & (1 << t)) != 0 { c.lanebuf[t] = nx_simt_rf_read(c, w, t, rs1) } 1150 t = t + 1 1151 } 1152 1153 if funct3 == NX_SIMT_W_BALLOT { 1154 var ballot: i64 = 0 1155 t = 0 1156 while t < c.nt { 1157 if (tmask & (1 << t)) != 0 { if c.lanebuf[t] != 0 { ballot = ballot | (1 << t) } } 1158 t = t + 1 1159 } 1160 t = 0 1161 while t < c.nt { 1162 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, ballot) } 1163 t = t + 1 1164 } 1165 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1166 c.pc[w] = pc + 4 1167 return NX_SIMT_OK 1168 } 1169 if funct3 == NX_SIMT_W_ALL { 1170 var allv: i64 = 1 1171 t = 0 1172 while t < c.nt { 1173 if (tmask & (1 << t)) != 0 { if c.lanebuf[t] == 0 { allv = 0 } } 1174 t = t + 1 1175 } 1176 t = 0 1177 while t < c.nt { 1178 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, allv) } 1179 t = t + 1 1180 } 1181 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1182 c.pc[w] = pc + 4 1183 return NX_SIMT_OK 1184 } 1185 if funct3 == NX_SIMT_W_ANY { 1186 var anyv: i64 = 0 1187 t = 0 1188 while t < c.nt { 1189 if (tmask & (1 << t)) != 0 { if c.lanebuf[t] != 0 { anyv = 1 } } 1190 t = t + 1 1191 } 1192 t = 0 1193 while t < c.nt { 1194 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, anyv) } 1195 t = t + 1 1196 } 1197 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1198 c.pc[w] = pc + 4 1199 return NX_SIMT_OK 1200 } 1201 if funct3 == NX_SIMT_W_ACTIVEMASK { 1202 t = 0 1203 while t < c.nt { 1204 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, tmask) } 1205 t = t + 1 1206 } 1207 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1208 c.pc[w] = pc + 4 1209 return NX_SIMT_OK 1210 } 1211 1212 // Shuffle family: each lane computes its own source lane index. 1213 t = 0 1214 while t < c.nt { 1215 if (tmask & (1 << t)) != 0 { 1216 let delta: i64 = nx_simt_rf_read(c, w, t, rs2) 1217 var src: i64 = t 1218 if funct3 == NX_SIMT_W_SHFL { src = delta } 1219 if funct3 == NX_SIMT_W_SHFL_UP { src = t - delta } 1220 if funct3 == NX_SIMT_W_SHFL_DOWN { src = t + delta } 1221 if funct3 == NX_SIMT_W_SHFL_XOR { src = t ^ delta } 1222 // Out-of-range source clamps to the lane itself, matching the CUDA 1223 // convention that an out-of-bounds shuffle leaves the value unchanged. 1224 if src < 0 { src = t } 1225 if src >= c.nt { src = t } 1226 nx_simt_rf_write(c, w, t, rd, c.lanebuf[src]) 1227 } 1228 t = t + 1 1229 } 1230 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1231 c.pc[w] = pc + 4 1232 return NX_SIMT_OK 1233 } 1234 1235 // ---------- Base RV64IM, executed per active lane ---------- 1236 let op_kind: i64 = nx_rv64im_decode_kind(inst) 1237 1238 // Branches and jumps are WARP-SCOPE: one PC per warp. Divergent conditionals are 1239 // expressed with split/join, not with a branch -- that is the SIMT contract. 1240 if op_kind == NX_RV64IM_OP_BRANCH { 1241 let lane0: i64 = nx_simt_lowest_lane(tmask) 1242 var take: i64 = 0 1243 if lane0 >= 0 { 1244 let a: i64 = nx_simt_rf_read(c, w, lane0, rs1) 1245 let b: i64 = nx_simt_rf_read(c, w, lane0, rs2) 1246 if funct3 == 0 { if a == b { take = 1 } } 1247 if funct3 == 1 { if a != b { take = 1 } } 1248 if funct3 == 4 { if a < b { take = 1 } } 1249 if funct3 == 5 { if a >= b { take = 1 } } 1250 if funct3 == 6 { take = nx_rv64im_ltu(a, b) } 1251 if funct3 == 7 { if nx_rv64im_ltu(a, b) == 0 { take = 1 } } 1252 } 1253 if take == 1 { c.pc[w] = pc + nx_rv64im_imm_b(inst) } else { c.pc[w] = pc + 4 } 1254 return NX_SIMT_OK 1255 } 1256 if op_kind == NX_RV64IM_OP_JAL { 1257 var t: i64 = 0 1258 while t < c.nt { 1259 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, pc + 4) } 1260 t = t + 1 1261 } 1262 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1263 c.pc[w] = pc + nx_rv64im_imm_j(inst) 1264 return NX_SIMT_OK 1265 } 1266 if op_kind == NX_RV64IM_OP_JALR { 1267 let lane0: i64 = nx_simt_lowest_lane(tmask) 1268 var tgt: i64 = pc + 4 1269 if lane0 >= 0 { tgt = (nx_simt_rf_read(c, w, lane0, rs1) + nx_rv64im_imm_i(inst)) & (0 - 2) } 1270 var t: i64 = 0 1271 while t < c.nt { 1272 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, pc + 4) } 1273 t = t + 1 1274 } 1275 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1276 c.pc[w] = tgt 1277 return NX_SIMT_OK 1278 } 1279 1280 // Memory: per-lane addresses, then one bank-conflict cost for the whole warp. 1281 if op_kind == NX_RV64IM_OP_LOAD { 1282 let imm: i64 = nx_rv64im_imm_i(inst) 1283 var t: i64 = 0 1284 while t < c.nt { 1285 addrs[t] = 0 1286 if (tmask & (1 << t)) != 0 { addrs[t] = nx_simt_rf_read(c, w, t, rs1) + imm } 1287 t = t + 1 1288 } 1289 t = 0 1290 while t < c.nt { 1291 if (tmask & (1 << t)) != 0 { 1292 var v: i64 = 0 1293 if funct3 == 2 { v = nx_rv64im_sext32(nx_simt_load32(c, addrs[t])) } 1294 if funct3 == 3 { v = nx_simt_load64(c, addrs[t]) } 1295 if funct3 == 6 { v = nx_simt_load32(c, addrs[t]) & 0xffffffff } 1296 nx_simt_rf_write(c, w, t, rd, v) 1297 } 1298 t = t + 1 1299 } 1300 nx_simt_mem_access(c, w, addrs, bank_hits) 1301 // The loaded value lands when the access completes, not at issue. 1302 nx_simt_mark_dest(c, w, rd, c.wstall_until[w] - c.cycles) 1303 c.pc[w] = pc + 4 1304 return NX_SIMT_OK 1305 } 1306 if op_kind == NX_RV64IM_OP_STORE { 1307 let imm: i64 = nx_rv64im_imm_s(inst) 1308 var t: i64 = 0 1309 while t < c.nt { 1310 addrs[t] = 0 1311 if (tmask & (1 << t)) != 0 { addrs[t] = nx_simt_rf_read(c, w, t, rs1) + imm } 1312 t = t + 1 1313 } 1314 t = 0 1315 while t < c.nt { 1316 if (tmask & (1 << t)) != 0 { 1317 let v: i64 = nx_simt_rf_read(c, w, t, rs2) 1318 if funct3 == 2 { nx_simt_store32(c, addrs[t], v) } 1319 if funct3 == 3 { nx_simt_store64(c, addrs[t], v) } 1320 } 1321 t = t + 1 1322 } 1323 nx_simt_mem_access(c, w, addrs, bank_hits) 1324 c.pc[w] = pc + 4 1325 return NX_SIMT_OK 1326 } 1327 1328 // Upper-immediate forms. 1329 if op_kind == NX_RV64IM_OP_LUI { 1330 let v: i64 = nx_rv64im_imm_u(inst) 1331 var t: i64 = 0 1332 while t < c.nt { 1333 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, v) } 1334 t = t + 1 1335 } 1336 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1337 c.pc[w] = pc + 4 1338 return NX_SIMT_OK 1339 } 1340 if op_kind == NX_RV64IM_OP_AUIPC { 1341 let v: i64 = pc + nx_rv64im_imm_u(inst) 1342 var t: i64 = 0 1343 while t < c.nt { 1344 if (tmask & (1 << t)) != 0 { nx_simt_rf_write(c, w, t, rd, v) } 1345 t = t + 1 1346 } 1347 nx_simt_mark_dest(c, w, rd, NX_SIMT_LAT_ALU) 1348 c.pc[w] = pc + 4 1349 return NX_SIMT_OK 1350 } 1351 1352 // Register-register and register-immediate ALU, per lane. 1353 var handled: i64 = 0 1354 var alu_op: i64 = NX_RV64IM_ALU_INVALID 1355 var use_imm: i64 = 0 1356 var imm_val: i64 = 0 1357 1358 if op_kind == NX_RV64IM_OP_OP { 1359 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP, funct3, funct7); handled = 1 1360 } 1361 if op_kind == NX_RV64IM_OP_M_MUL { 1362 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP, funct3, funct7); handled = 1 1363 } 1364 if op_kind == NX_RV64IM_OP_M_DIV { 1365 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP, funct3, funct7); handled = 1 1366 } 1367 if op_kind == NX_RV64IM_OP_OP_32 { 1368 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_32, funct3, funct7); handled = 1 1369 } 1370 if op_kind == NX_RV64IM_OP_M_MUL_32 { 1371 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_32, funct3, funct7); handled = 1 1372 } 1373 if op_kind == NX_RV64IM_OP_M_DIV_32 { 1374 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_32, funct3, funct7); handled = 1 1375 } 1376 if op_kind == NX_RV64IM_OP_OP_IMM { 1377 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_IMM, funct3, funct7) 1378 use_imm = 1; imm_val = nx_rv64im_imm_i(inst); handled = 1 1379 } 1380 if op_kind == NX_RV64IM_OP_OP_IMM_32 { 1381 alu_op = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_IMM_32, funct3, funct7) 1382 use_imm = 1; imm_val = nx_rv64im_imm_i(inst); handled = 1 1383 } 1384 1385 if handled == 1 { 1386 var t: i64 = 0 1387 while t < c.nt { 1388 if (tmask & (1 << t)) != 0 { 1389 let a: i64 = nx_simt_rf_read(c, w, t, rs1) 1390 var b: i64 = imm_val 1391 if use_imm == 0 { b = nx_simt_rf_read(c, w, t, rs2) } 1392 nx_simt_rf_write(c, w, t, rd, nx_rv64im_alu_compute(alu_op, a, b)) 1393 } 1394 t = t + 1 1395 } 1396 // Multiply and divide are multi-cycle. ★They must block their DEPENDENTS, not the 1397 // whole warp: an independent instruction following a multiply can still issue. 1398 // Stalling the warp outright (what this did before the scoreboard) both overstates 1399 // the cost and hides it from the warp-count comparison, because a wholesale warp 1400 // stall is not a bubble another warp can fill in the same way. 1401 var lat: i64 = NX_SIMT_LAT_ALU 1402 if op_kind == NX_RV64IM_OP_M_MUL { lat = NX_SIMT_LAT_MULDIV } 1403 if op_kind == NX_RV64IM_OP_M_DIV { lat = NX_SIMT_LAT_MULDIV } 1404 if op_kind == NX_RV64IM_OP_M_MUL_32 { lat = NX_SIMT_LAT_MULDIV } 1405 if op_kind == NX_RV64IM_OP_M_DIV_32 { lat = NX_SIMT_LAT_MULDIV } 1406 nx_simt_mark_dest(c, w, rd, lat) 1407 c.pc[w] = pc + 4 1408 return NX_SIMT_OK 1409 } 1410 1411 if op_kind == NX_RV64IM_OP_FENCE { 1412 c.pc[w] = pc + 4 1413 return NX_SIMT_OK 1414 } 1415 1416 // Anything else halts this warp loudly rather than silently skipping work. 1417 c.status = NX_SIMT_E_ILLEGAL 1418 c.wactive[w] = 0 1419 return 0 - NX_SIMT_E_ILLEGAL 1420} 1421 1422// ===== Cycle-level run loop ================================================= 1423// 1424// Every iteration is one CYCLE. A cycle in which no warp is eligible is a real stall 1425// cycle and is counted -- that is exactly how extra warps show up as higher IPC, and 1426// dropping it would make every configuration look identical. 1427 1428func nx_simt_run(c: *NxSimtCore, max_cycles: i64, addrs: *i64, bank_hits: *i64) -> i64 { 1429 if c.valid != 1 { return 0 - NX_SIMT_E_ARG } 1430 var n: i64 = 0 1431 while n < max_cycles { 1432 if nx_simt_any_active(c) == 0 { c.halted = 1; return NX_SIMT_OK } 1433 let w: i64 = nx_simt_pick_warp(c) 1434 if w < 0 { 1435 c.stall_cycles = c.stall_cycles + 1 1436 } else { 1437 nx_simt_exec_warp(c, w, addrs, bank_hits) 1438 if c.status == NX_SIMT_E_BOUNDS { return 0 - NX_SIMT_E_BOUNDS } 1439 if c.status == NX_SIMT_E_IPDOM { return 0 - NX_SIMT_E_IPDOM } 1440 } 1441 c.cycles = c.cycles + 1 1442 n = n + 1 1443 } 1444 return 0 - NX_SIMT_E_ARG 1445} 1446 1447// ===== Multi-core cluster ================================================= 1448// 1449// "Cores can be grouped into a cluster that can optionally be attached to a shared L2 cache." 1450// (MICRO'21 sec 4.1.4). We build the cluster; the optional L2 we deliberately do NOT build, 1451// because their own Figure 21 shows L2 barely moves the needle (sgemm 16.5 -> 18.5 IPC at 2 1452// channels, and NO change at 8) while channels move it 2.4x. Build what the data says matters. 1453// 1454// ★THE ONE THING THAT MUST BE SHARED IS BANDWIDTH. Each core keeps its own warps, register 1455// files and L1 -- but every core's `chan_free` is repointed at ONE cluster-owned array, so 1456// memory channels are contended. Without that, adding cores would scale PERFECTLY LINEARLY 1457// and the model would be vacuous: Figure 18 shows sgemm reaching only 53% of linear at 32 1458// cores precisely because memory saturates. 1459// 1460// ORACLE, MICRO'21 Figure 18 (aggregate IPC vs core count, sgemm): 1461// 1 core 1.9 · 2 cores 3.8 · 4 cores 7.8 · 8 cores 15.3 · 16 cores 27.1 · 32 cores 32.5 1462// So sgemm is near-LINEAR to 8 cores (8.05x = 100% of linear), then bends: 89% at 16, 53% at 1463// 32. A model that shows perfect linearity everywhere has not modelled contention at all. 1464 1465const NX_SIMT_MAX_CORES: i64 = 16 1466const NX_SIMT_CORE_STRIDE: i64 = 1024 // bytes reserved per core; struct is well under this 1467 1468struct NxSimtCluster { 1469 ncores: i64 1470 core_mem: *i64 // ncores contiguous NxSimtCore slots, NX_SIMT_CORE_STRIDE apart 1471 chan_free: *i64 // SHARED memory channels -- the contention point 1472 nchan: i64 1473 gbar_count: *i64 // SHARED global-barrier arrival counters 1474 gbar_expect: *i64 // SHARED global-barrier thresholds 1475 gbar_rel: i64 // global barrier releases performed (instrumentation) 1476 cycles: i64 1477 instret: i64 // warp-instructions retired across ALL cores 1478 stall_cycles: i64 // per-core issue slots that went unused 1479 halted: i64 1480 valid: i64 1481} 1482 1483// Cores live in one contiguous allocation indexed by a fixed stride, NOT in an array of 1484// pointers -- an *i64 array holding pointers is exactly the shape that gets read back as 1485// numbers, and this ecosystem has already paid for that once. 1486func nx_simt_cluster_core(cl: *NxSimtCluster, i: i64) -> *NxSimtCore { 1487 return (((cl.core_mem) as i64) + i * NX_SIMT_CORE_STRIDE) as *NxSimtCore 1488} 1489 1490func nx_simt_cluster_init(cl: *NxSimtCluster, ncores: i64, nw: i64, nt: i64, 1491 mem_base: i64, mem_buf: *u8, mem_size: i64, 1492 entry_pc: i64, lat: i64, nchan: i64) -> i64 { 1493 if ncores < 1 { return 0 - NX_SIMT_E_ARG } 1494 if ncores > NX_SIMT_MAX_CORES { return 0 - NX_SIMT_E_ARG } 1495 cl.ncores = ncores 1496 cl.core_mem = (sys_mmap(ncores * NX_SIMT_CORE_STRIDE)) as *i64 1497 cl.chan_free = (sys_mmap(8 * NX_SIMT_MAX_CHANNELS)) as *i64 1498 cl.nchan = nchan 1499 var ch: i64 = 0 1500 while ch < NX_SIMT_MAX_CHANNELS { cl.chan_free[ch] = 0; ch = ch + 1 } 1501 cl.gbar_count = (sys_mmap(8 * NX_SIMT_NBARRIERS)) as *i64 1502 cl.gbar_expect = (sys_mmap(8 * NX_SIMT_NBARRIERS)) as *i64 1503 var gb: i64 = 0 1504 while gb < NX_SIMT_NBARRIERS { cl.gbar_count[gb] = 0; cl.gbar_expect[gb] = 0; gb = gb + 1 } 1505 cl.gbar_rel = 0 1506 1507 var i: i64 = 0 1508 while i < ncores { 1509 let c: *NxSimtCore = nx_simt_cluster_core(cl, i) 1510 nx_simt_alloc(c) 1511 nx_simt_init(c, nw, nt, mem_base, mem_buf, mem_size, entry_pc) 1512 nx_simt_set_memory(c, lat, nchan) 1513 // ★Repoint this core's channel array at the CLUSTER's. Bandwidth is now shared and 1514 // finite across every core, which is what makes scaling sub-linear. 1515 c.chan_free = cl.chan_free 1516 // Wire this core into the cluster's global barrier table. 1517 c.gbar_count = cl.gbar_count 1518 c.gbar_expect = cl.gbar_expect 1519 c.gbar_on = 1 1520 i = i + 1 1521 } 1522 cl.cycles = 0 1523 cl.instret = 0 1524 cl.stall_cycles = 0 1525 cl.halted = 0 1526 cl.valid = 1 1527 return NX_SIMT_OK 1528} 1529 1530// One cluster cycle = one issue opportunity PER CORE. Cores are independent front-ends, so N 1531// cores can retire N warp-instructions in a cycle -- bounded only by the shared channels. 1532func nx_simt_cluster_run(cl: *NxSimtCluster, max_cycles: i64, 1533 addrs: *i64, bank_hits: *i64) -> i64 { 1534 if cl.valid != 1 { return 0 - NX_SIMT_E_ARG } 1535 var n: i64 = 0 1536 while n < max_cycles { 1537 var any_live: i64 = 0 1538 var i: i64 = 0 1539 while i < cl.ncores { 1540 let c: *NxSimtCore = nx_simt_cluster_core(cl, i) 1541 // Drive every core from the CLUSTER clock so channel reservations and stall 1542 // deadlines are compared on one timebase. 1543 c.cycles = cl.cycles 1544 if nx_simt_any_active(c) == 1 { 1545 any_live = 1 1546 let w: i64 = nx_simt_pick_warp(c) 1547 if w < 0 { 1548 cl.stall_cycles = cl.stall_cycles + 1 1549 } else { 1550 nx_simt_exec_warp(c, w, addrs, bank_hits) 1551 cl.instret = cl.instret + 1 1552 } 1553 } 1554 i = i + 1 1555 } 1556 // ----- global barrier release, cluster-scope ----- 1557 // Checked once per cluster cycle, AFTER every core has had its issue opportunity, so 1558 // arrivals within the same cycle all count. A barrier whose threshold is met wakes 1559 // every warp parked on it ACROSS ALL CORES at once -- that simultaneity is the whole 1560 // point of a hardware barrier, and it is unreachable from inside a single core. 1561 var b: i64 = 0 1562 while b < NX_SIMT_NBARRIERS { 1563 if cl.gbar_expect[b] > 0 { 1564 if cl.gbar_count[b] >= cl.gbar_expect[b] { 1565 var ci: i64 = 0 1566 while ci < cl.ncores { 1567 let cc: *NxSimtCore = nx_simt_cluster_core(cl, ci) 1568 var ww: i64 = 0 1569 while ww < cc.nw { 1570 if cc.wbar_id[ww] == b { 1571 cc.wbarrier[ww] = 0 1572 cc.wbar_id[ww] = 0 - 1 1573 } 1574 ww = ww + 1 1575 } 1576 ci = ci + 1 1577 } 1578 cl.gbar_count[b] = 0 1579 cl.gbar_expect[b] = 0 1580 cl.gbar_rel = cl.gbar_rel + 1 1581 } 1582 } 1583 b = b + 1 1584 } 1585 1586 if any_live == 0 { cl.halted = 1; return NX_SIMT_OK } 1587 cl.cycles = cl.cycles + 1 1588 n = n + 1 1589 } 1590 return 0 - NX_SIMT_E_ARG 1591} 1592 1593// Aggregate IPC across the cluster, in per-mille. This is Figure 18's metric. 1594func nx_simt_cluster_ipc_permille(cl: *NxSimtCluster) -> i64 { 1595 if cl.cycles <= 0 { return 0 } 1596 return (cl.instret * 1000) / cl.cycles 1597} 1598 1599// ===== Instrumentation readout ================================================= 1600// IPC in per-mille, matching Vortex's metric: WARP-instructions retired per cycle. 1601// 1000 permille = 1.0 IPC = one warp-instruction issued every cycle. 1602 1603func nx_simt_ipc_permille(c: *NxSimtCore) -> i64 { 1604 if c.cycles <= 0 { return 0 } 1605 return (c.instret * 1000) / c.cycles 1606} 1607 1608// Lane-IPC: the work actually performed per cycle. A wider warp does more per 1609// instruction, which warp-IPC alone cannot see -- reporting only one of these is how a 1610// configuration comparison misleads. 1611 1612func nx_simt_lane_ipc_permille(c: *NxSimtCore) -> i64 { 1613 if c.cycles <= 0 { return 0 } 1614 return (c.lane_instret * 1000) / c.cycles 1615}