1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The Lance Authors
// AMX-FP16 tile kernels, and the tile-configuration plumbing they share.
//
// Every kernel here computes fp16 x fp16 dot products with TDPFP16PS
// (fp16 x fp16 -> fp32 accumulate). The tile *shapes* differ per kernel and are
// declared next to each one; the code that turns a shape into a loaded
// LDTILECFG image is shared, because the one subtle step in it (the
// dead-store barrier below) fails silently and only on some compilers, so it
// must exist exactly once.
//
// Two kernels:
// * `lance_amx_dot_f16_batch_16` -- one query against 16 candidates, for
// choosing the IVF partitions a query probes (16 centroids per call).
// Ported in spirit from FAISS PR
// facebookresearch/faiss#5235's AMX-BF16 kernel: fp16 and bf16 are both
// 2-byte tile elements consumed by the same `_tile_dp*ps` shape, so only the
// instruction (`_tile_dpbf16ps` -> `_tile_dpfp16ps`) and the element ->
// float conversion (bf16 bit-shift -> real IEEE fp16 via F16C `_cvtsh_ss`)
// differ. Its C tile is fed by three independent (A, B) tile pairs so three
// TDPFP16PS can be in flight at once; see the tile roles below.
// * `lance_amx_dot_f16_gemm` -- an m x n GEMM, for scoring many vectors
// against many centroids at once (k-means assignment). Uses all eight tiles
// as a 2x2 register-blocked accumulator.
//
// ## Tile configuration is reloaded on every call
//
// Each kernel has one compile-time tile shape (`SEARCH_TILES` / `GEMM_TILES`),
// so caching its LDTILECFG is tempting: that reconfiguration costs a few
// hundred cycles, against the ~64 cycles of useful tile work a dim-128 search
// call performs. It is still wrong, because Lance does not own the tile unit:
// LDTILECFG and TILERELEASE are architectural per-logical-processor state, so
// another AMX user on the thread (oneDNN under PyTorch, ONNX Runtime, same
// Python process) can retire or reshape a configuration Lance believes is live
// -- a foreign TILERELEASE leaves the tiles in INIT and the next tile op raises
// #UD, a foreign LDTILECFG silently substitutes wrong shapes. Neither is
// observable from here, and a kernel reached from arbitrary Rust and C cannot
// bound what runs between two of its own calls.
//
// So nothing is cached: every kernel configures the tiles on entry and releases
// them on exit, and pays that per call. Against a GEMM over a whole block of
// vectors it is noise; against a batch-16 search it is most of the call, and is
// spent anyway, because the alternative is a configuration whose validity this
// file has no way to establish.
//
// ## Thread safety
//
// LDTILECFG sets per-logical-processor state, and nothing here is shared
// mutably: the spec tables are `static const` and the 64-byte config *image* is
// built on the calling thread's stack, so one thread's shape can never reach
// another's tile ops -- as with the XTILECFG it is loaded into, which rides in
// the thread's own XSAVE area. That says nothing about what *other* libraries
// on this thread have done to the tile unit, which is what reconfiguring on
// every entry is for.
//
// SAFETY: executing any AMX tile instruction without first (a) confirming the
// amx-tile + amx-fp16 CPUID bits and (b) obtaining XTILEDATA permission from
// the kernel raises SIGILL. Both are the Rust caller's responsibility (see
// `simd/amx_fp16.rs`); `lance_amx_fp16_request_perm` below performs (b).
// Must precede all includes: exposes the glibc `syscall()` prototype from
// <unistd.h>, which -std=c17 otherwise hides.
// ---------------------------------------------------------------------------
// XTILEDATA permission
// ---------------------------------------------------------------------------
// Ask the kernel to enable AMX tile data state for this process, via
// arch_prctl(ARCH_REQ_XCOMP_PERM, XFEATURE_XTILEDATA). Returns 0 on success,
// non-zero otherwise. Requesting XTILEDATA (18) implicitly also grants
// XTILECFG (17). Constants per Linux Documentation/arch/x86/xstate.rst.
//
// XTILEDATA is the single dynamically-enabled XSAVE state component backing the
// physical TMM tile registers, shared by every AMX compute instruction
// (TDPBUUD / TDPBF16PS / TDPFP16PS ...). The syscall is idempotent, so
// requesting an already-granted permission is harmless.
int
// ---------------------------------------------------------------------------
// Shared tile configuration
// ---------------------------------------------------------------------------
// Tile configuration kinds. One per distinct tile shape, and the selector
// `lance_amx_tilecfg_image` exposes to the Rust tests so they can pin a shape
// without executing a tile instruction. Kept in sync with the `AMX_CFG_*`
// constants in `simd/amx_fp16.rs`.
// The 64-byte tile configuration image loaded by LDTILECFG
// (`_tile_loadconfig`). Layout per Intel SDM: palette_id, start_row, 14
// reserved bytes, then a u16 colsb[16] (bytes-per-row) array and a u8 rows[16]
// array. Slots not named by a kernel's spec table stay zero.
//
// 64-byte aligned so the configuration load does not straddle a cache line.
typedef struct __attribute__ lance_amx_tilecfg;
// The image LDTILECFG reads is exactly 64 bytes; a layout change that altered
// the size would silently feed the instruction garbage.
;
// One tile register's shape. `tmm` is the register index (0..7), `colsb` its
// bytes per row, `rows` its row count.
typedef struct lance_amx_tile_spec;
// Fill `cfg` with the LDTILECFG image described by `specs`, without loading it.
// Split from the load so `lance_amx_tilecfg_image` can hand the Rust tests the
// exact bytes a kernel would configure.
static void
// Load the tile configuration described by `specs`. Unconditional: see the file
// header for why no state is kept about what is already loaded.
//
// The barrier is not optional: some GCC versions do not model
// _tile_loadconfig as reading the 64-byte cfg image, dead-store-eliminate the
// rows/colsb writes, and load an all-zero (unconfigured) tile shape -> #UD on
// the first tile op (documented in FAISS #5235). Keeping it here, in the one
// function that owns the image, is why kernels do not build configurations
// themselves. Harmless under clang.
static inline void
// Hand the tile unit back at the end of a kernel. Pairs with
// `lance_amx_tile_ensure`, and must run on every exit path of a kernel that
// configured tiles.
//
// Not for Lance's own benefit -- `lance_amx_tile_ensure` reloads on every entry,
// so a stale configuration could never reach one of these kernels either way. It
// is for everyone else on the thread: a live tile configuration keeps 8 KB of
// XTILEDATA in this thread's XSAVE area across every context switch, and leaves
// a shape another AMX user did not ask for sitting on hardware it also uses.
//
// Because it is nobody's correctness but the neighbours', deleting it leaves
// every result right and every "did it crash?" test green. Only
// `lance_amx_tilecfg_current_for_test`, which reads the hardware, notices.
static inline void
// Test hook: retire the tile configuration the way a *foreign* AMX user on this
// thread would, so a test can put the tile unit in INIT under a kernel that is
// about to run and check the kernel reconfigures instead of assuming.
//
// Hidden visibility because this manufactures the state the rest of this file
// exists to prevent: it is linked into the crate for its regression test, but is
// not something a shipped shared object should offer to whatever else is in the
// process.
//
// TILERELEASE needs no XTILEDATA grant of its own -- XFD traps only instructions
// that touch a TMM register -- but a test calling this is about to call a kernel
// that does, so it should have gone through `amx_supported()` anyway.
void
// Test hook: copy this logical processor's *live* tile configuration into the
// 64 bytes at `out`, via STTILECFG. A `palette_id` of 0 means the tile unit is
// in INIT state -- nothing configured.
//
// This reads the hardware rather than any record Lance keeps, which is what
// makes it able to catch the half of this design that has no other symptom:
// `lance_amx_tile_ensure` reloading on every entry is what keeps results
// correct, so dropping `lance_amx_tile_done`'s release leaves behaviour right
// and every "did it crash?" test passing, and only a live `palette_id` reported
// back here says the tile unit was never handed over.
//
// STTILECFG does not touch a TMM register, so unlike the kernels it is legal
// without the XTILEDATA grant. Hidden for the same reason as the clobber hook.
void
// ---------------------------------------------------------------------------
// Kernel: batch-16 search (one query x 16 candidates)
// ---------------------------------------------------------------------------
// Tile roles. These are immediate operands of the tile intrinsics, so they must
// be compile-time constants; `#define` rather than `enum` avoids relying on how
// strictly a compiler treats enum constants as immediates.
//
// One TDPFP16PS pass covers K = 32 fp16 dims. With N = 1 the query needs no
// VNNI repacking: B.row[k].fp16[i] = query[k*2 + i] is just the contiguous
// query halfwords, obtained by loading with a 4-byte row stride. The candidates
// form the A tile's rows; they live at unrelated addresses, so this kernel
// stages them a few k-blocks at a time into a fixed-stride scratch buffer that
// the tile load can read (see `lance_amx_stage_rows`).
//
// Three (A, B) pairs, not one. A single pair would make the loop strictly
// serial -- every TDPFP16PS waiting on the two loads that just overwrote its
// own operands -- so the tile unit would idle through each load's latency.
// Three independent pairs let three k-blocks' loads issue before the first
// TDPFP16PS needs its result, which is enough to keep the dp ops back to back.
// Seven tiles is what that costs; tmm7 is left unconfigured.
static const lance_amx_tile_spec SEARCH_TILES = ;
// Halfwords of one k-block: 32 fp16 dims, the K a single TDPFP16PS covers.
// Bytes one tile row spans in one k-block: 32 fp16, the A tile's full row width.
// K-blocks gathered per staging step. Three, so one staged buffer feeds exactly
// the three (A, B) pairs the main loop issues together.
// One staging buffer: the 16 rows a tile load always reads, whatever the batch
// actually holds.
// The furthest-reaching tile load is A2: 64 bytes at offset 128 of the last of
// 16 rows, so the last byte it touches is at 128 + 15*192 + 63 and the span it
// covers is exactly SEARCH_STAGE_BYTES. Asserted because an overrun here would
// be a stack smash with no other symptom.
//
// What this actually pins is that the three A tiles tile one staging row with
// no gap and no overlap, i.e. SEARCH_STAGE_BLOCKS == 3; it does not check
// SEARCH_K_BLOCK, nor the 64-byte A-tile width, which is hardcoded separately
// in SEARCH_TILES.
;
// Gather `row_bytes` starting at k-block `kb` out of each of the first `count`
// candidates into `dst`, one candidate per row.
//
// Rows are SEARCH_STAGE_ROW_BYTES apart even when `row_bytes` is smaller: an A
// tile only reads 64 bytes at its own offset within a row, so a wider stride
// simply leaves the trailing bytes unread. Keeping the stride fixed across
// every staging step is what lets rows [count, 16) be zeroed once per call --
// each step then rewrites the same rows at the same addresses.
//
// The k-blocks a row covers are adjacent inside the candidate vector, so each
// candidate costs exactly one straight-line copy no matter how many k-blocks the
// step covers. `row_bytes` should stay a compile-time constant at every call
// site: a constant size expands to inline wide moves, while a variable one
// becomes a libc `memcpy` call that `-funroll-loops` then multiplies (five PLT
// calls for the one remainder loop, measured under clang-16).
//
// Zeroing the padding rows is deliberately *not* routed through here: it writes
// rows [count, 16) rather than [0, count), and when the rows are full width it
// is one contiguous `memset` rather than a per-row loop.
static inline void
// out[i] = sum_{d in 0..dim} f32(query[d]) * f32(candidates[i][d]), i < count.
//
// `query` -- IEEE-754 binary16 values as raw uint16_t bit patterns
// (half::f16 has identical layout); `dim` valid halfwords.
// `candidates` -- pointers to `dim` halfwords each; only the first `count` are
// read. The vectors live wherever the storage put them; this
// kernel owns the gather.
// `count` -- candidates carrying a real vector. **Precondition:
// 1 <= count <= 16**, rejected at the Rust boundary
// (`dot_f16_batch_16`) rather than clamped here; a larger value
// would run the gather off the end of a staging buffer.
// `dim` -- vector dimension.
// `out` -- destination for 16 fp32 dot products. Lanes [count, 16) are
// written as 0, not left untouched.
//
// ## Why `count` rather than always 16
//
// The caller sweeps centroids 16 at a time, so when their count is not a
// multiple of 16 the last group is short and it fills the spare slots by
// repeating a row it already holds. Staging all 16 rows would copy that row an
// extra 16 - count times; skipping them saves that much of a memcpy on at most
// one group per sweep, so `count` buys far less here than it did for a caller
// whose batches were usually partial. Only rows [0, count) are gathered, so the
// copying scales with the vectors actually scored while the tile work stays one
// fixed-cost pass. The padded rows still have to exist, since a tile load reads
// 16 rows unconditionally; they are zeroed once per call, and an all-zero A row
// yields a zero dot product.
//
// ## Why the gather is here and not in the caller
//
// A tile load reads 16 rows at one fixed stride from one base pointer, and no
// stride is guaranteed between this kernel's 16 candidate pointers, so their
// bytes have to be brought together somewhere. Doing it in the caller means
// copying 16 * dim * 2 bytes -- 24 KB at dim 768, 32 KB at dim 1024 -- and every
// one of those bytes has to land before the first TDPFP16PS can issue. Against a
// 48 KB L1D that buffer alone is half the cache, and the copy is pure exposed
// latency: nothing overlaps it.
//
// Staging inside the k-block loop instead keeps the working buffer at 3 KB and
// lets the copies for one triple run underneath the tile ops of the previous
// one. The bytes moved are identical; what changes is that they move while the
// tile unit is busy rather than before it starts. (Measured on the caller-side
// version at dim 1024: __memmove 3.7M cycles/query against 0.5M for the scalar
// kernel, IPC 1.70 -> 1.17, and a critical path 18% longer even though total CPU
// work per query was 6.5% lower. The same structure is what
// epeshared/hnswlib-amx uses for its AMX-BF16 kernel.)
//
// For dim > 32 we accumulate across floor(dim/32) tile passes into the same C
// tile, three k-blocks at a time. The tail (dim % 32 dims) is computed in
// scalar fp32 afterwards. The result is NOT bit-exact against a sequential
// scalar loop: floating-point tile-order accumulation rounds differently. It
// matches an f32-accumulated reference dot product to within fp16 precision,
// which is all the fp16 distance path requires (see amx_fp16.rs / the Rust-side
// tests).
void
// ---------------------------------------------------------------------------
// Kernel: M x N GEMM (many vectors x many centroids)
// ---------------------------------------------------------------------------
// Tile roles for the 2x2-register-blocked GEMM. Two A tiles (32 vectors) and
// two B tiles (32 centroids) feed four C accumulators, so one k-pass issues 4
// TDPFP16PS against 4 tile loads -- the highest compute-per-load ratio the 8
// physical tiles allow, and the reason all 8 are claimed here.
// All eight at the architectural maximum (16 rows x 64 bytes = 1 KB), which is
// exactly the 8 KB of tile state AMX provides.
static const lance_amx_tile_spec GEMM_TILES = ;
// Halfwords per packed B block: 16 tile rows x 32 halfwords per row.
// out[i*out_stride + j] = sum_{d in 0..dim} f32(data[i*data_stride + d]) *
// f32(centroids[j*dim + d]).
//
// `data` -- [m, dim] row-major fp16 bit patterns, rows `data_stride`
// halfwords apart (`data_stride >= dim`).
// `m` -- number of vectors; **must be a multiple of 32**.
// `packed_b` -- centroids pre-interleaved by `pack_centroids_vnni` (see
// `amx_fp16.rs`), holding only the floor(dim/32) whole
// 32-dim k-blocks.
// `centroids` -- the same [n, dim] row-major centroids `packed_b` was built
// from. Read only for the `dim % 32` tail dims, which are not
// worth a tile pass and so are never packed; still required
// when dim % 32 == 0, where it goes unread.
// `n` -- number of centroids; **must be a multiple of 32**.
// `dim` -- vector dimension; any value, the tail runs scalar.
// `out` -- [m, n] row-major fp32, rows `out_stride` floats apart
// (`out_stride >= n`).
//
// The m and n multiple-of-32 requirements are preconditions, not something this
// kernel checks or works around: they let the register-blocked loop run with no
// edge cases, and the Rust caller is the layer that knows how to pad or split.
//
// The k dimension carries no such requirement. TDPFP16PS accumulation rounds
// differently from a sequential scalar loop, so results match an f32-accumulated
// reference to fp16 precision rather than bit-exactly -- same contract as
// `lance_amx_dot_f16_batch_16`.
//
// B's VNNI interleave is what makes A loadable straight out of `data`: with
// packed_b[((kb*(n/16) + jb)*512) + k*32 + nn*2 + p]
// == centroids[(jb*16 + nn)*dim + kb*32 + 2*k + p]
// TDPFP16PS's b.row[k].fp16[2*nn+p] lands on centroid (jb*16+nn) dim
// (kb*32+2*k+p), pairing it with a.row[mm].fp16[2*k+p] = the same dim of vector
// (i+mm). k-blocks are the outer index so one k-pass reads the two B tiles it
// needs from adjacent memory.
void
// ---------------------------------------------------------------------------
// Configuration introspection
// ---------------------------------------------------------------------------
// Write the 64-byte LDTILECFG image for `cfg_kind` (a `LANCE_AMX_CFG_*`
// constant) into `out`, without loading it. Returns 0 on success, -1 for an
// unknown `cfg_kind`.
//
// Exposed so the Rust tests can pin each kernel's tile shape byte for byte. A
// wrong shape does not fail cleanly — it is a #UD or silently wrong results —
// so the shape is asserted directly rather than inferred from kernel output.
int