mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
Vectors for kernels: masks and select, shuffles and byte lookup, dot_add, @cpu and cpu_has; --cpu native on Nix
- comparisons of vectors give masks (boolxN): select(m, a, b), m.any/all/count, & | ^ !;
f32x8(list, i, fill) and v.store_part(list, i) for the end of a list (masked load/store)
- v.shuffle(constant lanes), v.lo(), v.hi(), concat(a, b), table.lookup(indices):
pshufb / vpshufb on x86 (SSSE3 and up), tbl on ARM64 (checked in the assembly, LLVM 18 and 21)
- acc.dot_add(a, b): i32 lanes gain 4 byte products each, exactly; vpdpbusd with AVX-VNNI or AVX-512 VNNI,
usdot/sdot/udot on ARM64, the sum written out elsewhere (pmaddwd); run here with all three paths agreeing
- @cpu("avx2,fma") fn: LLVM target-features for one function (+evex512 for AVX-512, which LLVM 18+ needs);
cpu_has("avx2"): CPUID + XGETBV on x86, hwcaps on Linux ARM64, sysctl on macOS; a wide vector can't be passed
between functions with different @cpu (its registers differ), and the compiler says so
- the compiler asks clang which features --cpu means (its predefined macros, cached), so dot_add picks the instruction
- Nix's clang wrapper dropped -march=native in nix-shell (reproduced): finch tells it NIX_ENFORCE_NO_NATIVE=0, shell.nix too,
and warns if --cpu native still made no difference
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Psc42moPLwP43Cmb9oZqSR
This commit is contained in:
parent
0ceaaa27c4
commit
04f1f94a21
23 changed files with 964 additions and 45 deletions
7
tests/fail/cpu_attr_name.fch
Normal file
7
tests/fail/cpu_attr_name.fch
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// expect: @cpu: 'avx3' isn't a feature Finch knows
|
||||
@cpu("avx2,avx3")
|
||||
fn f() {
|
||||
}
|
||||
fn main() {
|
||||
f()
|
||||
}
|
||||
4
tests/fail/cpu_has_name.fch
Normal file
4
tests/fail/cpu_has_name.fch
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// expect: cpu_has: 'avx-512' isn't a feature Finch knows
|
||||
fn main() {
|
||||
print(cpu_has("avx-512"))
|
||||
}
|
||||
8
tests/fail/cpu_wide_vector.fch
Normal file
8
tests/fail/cpu_wide_vector.fch
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// expect: can't be passed between them (its registers differ)
|
||||
@cpu("avx2")
|
||||
fn double(f32x8 v) -> f32x8 {
|
||||
return v * 2.0
|
||||
}
|
||||
fn main() {
|
||||
print(double(f32x8(1.0)))
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
// expect: use min(v, w), max(v, w), or compare lanes: v[i] < w[i]
|
||||
// expect: use .all() or .any()
|
||||
fn main() {
|
||||
a := f32x4(1.0)
|
||||
print(a < a)
|
||||
if a < a {
|
||||
print("no")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
tests/fail/vec_shuffle_range.fch
Normal file
5
tests/fail/vec_shuffle_range.fch
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// expect: lane 4 isn't in a f32x4 (its lanes are 0 to 3)
|
||||
fn main() {
|
||||
v := f32x4(1.0)
|
||||
print(v.shuffle(0, 1, 2, 4))
|
||||
}
|
||||
6
tests/fail/vec_shuffle_var.fch
Normal file
6
tests/fail/vec_shuffle_var.fch
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// expect: shuffle(...) takes lane numbers written in the code
|
||||
fn main() {
|
||||
v := f32x4(1.0)
|
||||
k := 2
|
||||
print(v.shuffle(k, 0, 1, 3))
|
||||
}
|
||||
69
tests/run/dot_int8.fch
Normal file
69
tests/run/dot_int8.fch
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// The int8 dot product of quantized models: plain, with dot_add, and in @cpu functions picked by cpu_has
|
||||
|
||||
fn plain([]u8 a, []i8 b) -> int {
|
||||
total := 0
|
||||
for i in 0..a.len {
|
||||
total += int(a[i]) * int(b[i])
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
// 32 bytes a step, 8 sums kept in a vector until the end
|
||||
fn with_vectors([]u8 a, []i8 b) -> int {
|
||||
acc := i32x8(0)
|
||||
i := 0
|
||||
while i + 32 <= a.len {
|
||||
acc = acc.dot_add(u8x32(a, i), i8x32(b, i))
|
||||
i += 32
|
||||
}
|
||||
return int(acc.sum())
|
||||
}
|
||||
|
||||
@cpu("avxvnni")
|
||||
fn with_vnni([]u8 a, []i8 b) -> int {
|
||||
acc := i32x8(0)
|
||||
i := 0
|
||||
while i + 32 <= a.len {
|
||||
acc = acc.dot_add(u8x32(a, i), i8x32(b, i))
|
||||
i += 32
|
||||
}
|
||||
return int(acc.sum())
|
||||
}
|
||||
|
||||
@cpu("avx512f,avx512bw,avx512vl,avx512vnni,i8mm")
|
||||
fn with_avx512([]u8 a, []i8 b) -> int {
|
||||
acc := i32x16(0)
|
||||
i := 0
|
||||
while i + 64 <= a.len {
|
||||
acc = acc.dot_add(u8x64(a, i), i8x64(b, i))
|
||||
i += 64
|
||||
}
|
||||
return int(acc.sum())
|
||||
}
|
||||
|
||||
fn best([]u8 a, []i8 b) -> int {
|
||||
if cpu_has("avx512vnni") && cpu_has("avx512vl") && cpu_has("avx512bw") {
|
||||
return with_avx512(a, b)
|
||||
}
|
||||
if cpu_has("avxvnni") {
|
||||
return with_vnni(a, b)
|
||||
}
|
||||
return with_vectors(a, b)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
[]u8 a
|
||||
[]i8 b
|
||||
for k in 0..1024 {
|
||||
a.push(u8((k * 37 + 11) % 256))
|
||||
b.push(i8((k * 53 + 7) % 256 - 128))
|
||||
}
|
||||
want := plain(a, b)
|
||||
print(want, with_vectors(a, b) == want, best(a, b) == want)
|
||||
|
||||
// the extremes: 255 * -128 four times in a lane, no saturation
|
||||
acc := i32x4(1, 2, 3, 4).dot_add(u8x16(255), i8x16(-128))
|
||||
print(acc, i32x4(0).dot_add(i8x16(-128), i8x16(-128)), i32x4(0).dot_add(u8x16(255), u8x16(255)))
|
||||
print(i32x4(0).dot_add(i8x16(-1), u8x16(200)))
|
||||
print(cpu_has("avx2") || cpu_has("neon") || true)
|
||||
}
|
||||
4
tests/run/dot_int8.out
Normal file
4
tests/run/dot_int8.out
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
-68096 true true
|
||||
i32x4(-130559, -130558, -130557, -130556) i32x4(65536, 65536, 65536, 65536) i32x4(260100, 260100, 260100, 260100)
|
||||
i32x4(-800, -800, -800, -800)
|
||||
true
|
||||
50
tests/run/masks.fch
Normal file
50
tests/run/masks.fch
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Masks: comparisons of vectors, select, any/all/count, and the end of a list
|
||||
|
||||
fn relu(f32x8 v) -> f32x8 {
|
||||
return select(v > 0.0, v, 0.0)
|
||||
}
|
||||
|
||||
// the sum of a list of any length: whole vectors, then the last piece with its missing lanes as 0
|
||||
fn total([]f32 xs) -> f32 {
|
||||
acc := f32x8(0.0)
|
||||
i := 0
|
||||
while i < xs.len {
|
||||
acc = acc + f32x8(xs, i, 0.0)
|
||||
i += 8
|
||||
}
|
||||
return acc.sum()
|
||||
}
|
||||
|
||||
// add 1 to every number of a list, in place, the last piece too
|
||||
fn bump([]f32 xs) {
|
||||
i := 0
|
||||
while i < xs.len {
|
||||
(f32x8(xs, i, 0.0) + 1.0).store_part(xs, i)
|
||||
i += 8
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0)
|
||||
m := v > 0.0
|
||||
print(m, m.count(), m.any(), m.all(), m[3])
|
||||
print(relu(v), select(!m, v, -1.0))
|
||||
print((v >= -1.0) & (v < 3.0), (v == 0.0) | (v == 5.0), m ^ (v > 1.0))
|
||||
|
||||
a := i32x4(1, 5, 3, 7)
|
||||
b := i32x4(4, 4, 4, 4)
|
||||
print(select(a > b, a, b), (a == a).all(), (a != b).all())
|
||||
u := u8x16(200)
|
||||
print((u > u8x16(100)).all(), i32x4(a > b), f32x4(a < b))
|
||||
k := i32x4(0, 2, 0, -1)
|
||||
print(boolx4(k), boolx4(true, false, true, false))
|
||||
|
||||
[]f32 xs
|
||||
for n in 0..13 {
|
||||
xs.push(f32(n))
|
||||
}
|
||||
print(total(xs))
|
||||
bump(xs)
|
||||
print(xs)
|
||||
print(f32x8(xs, 8, -1.0), f32x8(xs, 13, 9.0), f32x8(xs, 100, 7.0))
|
||||
}
|
||||
9
tests/run/masks.out
Normal file
9
tests/run/masks.out
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
boolx8(false, false, false, true, true, true, false, true) 4 true false true
|
||||
f32x8(0, 0, 0, 1, 2, 3, 0, 5) f32x8(-2, -1, 0, -1, -1, -1, -4, -1)
|
||||
boolx8(false, true, true, true, true, false, false, false) boolx8(false, false, true, false, false, false, false, true) boolx8(false, false, false, true, false, false, false, false)
|
||||
i32x4(4, 5, 4, 7) true true
|
||||
true i32x4(0, 1, 0, 1) f32x4(1, 0, 1, 0)
|
||||
boolx4(false, true, false, true) boolx4(true, false, true, false)
|
||||
78
|
||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
|
||||
f32x8(9, 10, 11, 12, 13, -1, -1, -1) f32x8(9, 9, 9, 9, 9, 9, 9, 9) f32x8(7, 7, 7, 7, 7, 7, 7, 7)
|
||||
27
tests/run/shuffles.fch
Normal file
27
tests/run/shuffles.fch
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Moving lanes: shuffle, lo/hi, concat, and a byte table lookup (how 4-bit weights are unpacked)
|
||||
|
||||
// sixteen 4-bit numbers, two to a byte, made into sixteen bytes through a table of what each nibble means
|
||||
fn unpack4(u8x8 packed, i8x16 table) -> i8x16 {
|
||||
pairs := packed.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // u8x16: each byte twice
|
||||
odd := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1
|
||||
nibbles := select(odd, pairs >> 4, pairs & 15) // low nibble first, then the high one
|
||||
return table.lookup(nibbles)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
v := f32x4(1.0, 2.0, 3.0, 4.0)
|
||||
print(v.shuffle(3, 2, 1, 0), v.shuffle(0, 0, 1, 1, 2, 2, 3, 3), v.shuffle(1, 2))
|
||||
w := i32x8(0, 1, 2, 3, 4, 5, 6, 7)
|
||||
print(w.lo(), w.hi(), concat(w.hi(), w.lo()))
|
||||
print(concat(w, w).len, concat(v, v * 10.0).shuffle(0, 4, 1, 5, 2, 6, 3, 7))
|
||||
|
||||
table := u8x16(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
|
||||
print(table.lookup(u8x16(15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)))
|
||||
print(table.lookup(u8x16(16, 17, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)).lo())
|
||||
print(table.lookup(u8x32(3)).hi().lo().lo())
|
||||
|
||||
q4 := i8x16(-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7) // what each 4-bit number means
|
||||
print(unpack4(u8x8(0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe), q4))
|
||||
m := f32x4(1.0, -1.0, 2.0, -2.0) > 0.0
|
||||
print(m.shuffle(1, 0, 3, 2), m.lo())
|
||||
}
|
||||
8
tests/run/shuffles.out
Normal file
8
tests/run/shuffles.out
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
f32x4(4, 3, 2, 1) f32x8(1, 1, 2, 2, 3, 3, 4, 4) f32x2(2, 3)
|
||||
i32x4(0, 1, 2, 3) i32x4(4, 5, 6, 7) i32x8(4, 5, 6, 7, 0, 1, 2, 3)
|
||||
16 f32x8(1, 10, 2, 20, 3, 30, 4, 40)
|
||||
u8x16(25, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
|
||||
u8x8(10, 11, 25, 10, 10, 10, 10, 10)
|
||||
u8x4(13, 13, 13, 13)
|
||||
i8x16(-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7)
|
||||
boolx4(false, true, false, true) boolx2(true, false)
|
||||
|
|
@ -36,7 +36,7 @@ fn main() {
|
|||
// lanes
|
||||
a[0] = 9.0
|
||||
print(a[0], a[3], a)
|
||||
print(a == f32x4(9.0, 2.0, 3.0, 4.0), a != a)
|
||||
print((a == f32x4(9.0, 2.0, 3.0, 4.0)).all(), (a != a).any())
|
||||
|
||||
// whole numbers, bits, conversions
|
||||
n := i32x8(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
|
|
|
|||
Loading…
Reference in a new issue