diff --git a/README.md b/README.md index 938d072..d53017b 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ Pick the guide that fits you: | Arrays | `nums := [1, 2, 3]`, `nums.push(4)`, `nums[0]`, `nums.len`, `for i, n in nums { }` | | Maps | `ages := ["ann": 31]`, `ages["bob"] = 25`, `ages.has("cy")`, `counts[w] += 1`, `for k, v in ages { }` | | Errors | `fn parse(str s) -> int! { … return error("bad") }` → `parse(s) or 0`, `parse(s) or { … }`, `try parse(s)` | -| Vectors (SIMD) | `v := f32x8(list, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(list, i)`, `v[3]` | +| Vectors (SIMD) | `v := f32x8(list, i)`, `v * 2.0 + w`, `v.sum()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(list, i)` | +| Processors | `--cpu native`; `@cpu("avx2") fn …` with `if cpu_has("avx2")`; floats computed as written (`fma`/`muladd` to fuse) | | Text | `"hp \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | | Control | `if` / `else if` / `else` (also as a value), `while`, `for i in 0..10`, `break`, `continue`, `defer` | | Memory | automatic for arrays, text and structs; `new(...)` / `free(...)` for your own heap structures | @@ -161,6 +162,7 @@ Pick the guide that fits you: - [x] Threads (`import thread`) and atomics; C++ files; `--target android` - [x] The compiler on every system it builds for: Linux, macOS (Apple Silicon and Intel), Windows; releases made by CI - [x] Vector types (`f32x8`, `i8x32`, …) for SIMD, and `--cpu native` +- [x] Masks, shuffles, byte lookup and `dot_add` for kernels; `@cpu` and `cpu_has`; `fma`/`muladd`, float bits, f16 and bf16 ## Layout diff --git a/README.pl.md b/README.pl.md index 098f317..68dcc85 100644 --- a/README.pl.md +++ b/README.pl.md @@ -105,7 +105,8 @@ Wybierz przewodnik dla siebie: | Tablice | `liczby := [1, 2, 3]`, `liczby.push(4)`, `liczby[0]`, `liczby.len`, `for i, n in liczby { }` | | Mapy | `wiek := ["ania": 31]`, `wiek["bob"] = 25`, `wiek.has("cyryl")`, `ile[slowo] += 1`, `for k, v in wiek { }` | | Błędy | `fn parsuj(str s) -> int! { … return error("zła liczba") }` → `parsuj(s) or 0`, `parsuj(s) or { … }`, `try parsuj(s)` | -| Wektory (SIMD) | `v := f32x8(lista, i)`, `v * 2.0 + w`, `v.sum()`, `v.dot(w)`, `min(v, 1.0)`, `v.store(lista, i)`, `v[3]` | +| Wektory (SIMD) | `v := f32x8(lista, i)`, `v * 2.0 + w`, `v.sum()`, `select(v > 0.0, v, 0.0)`, `t.lookup(idx)`, `acc.dot_add(a, b)`, `v.store(lista, i)` | +| Procesory | `--cpu native`; `@cpu("avx2") fn …` razem z `if cpu_has("avx2")`; liczby zmiennoprzecinkowe liczone tak, jak zapisane (`fma`/`muladd` do łączenia) | | Tekst | `"hp: \(hp)"`, `"a" + "b"`, `str(42)`, `int("42")`, `s.split(",")`, `s.upper()`, `s[0]` | | Sterowanie | `if` / `else if` / `else` (również jako wartość), `while`, `for i in 0..10`, `break`, `continue`, `defer` | | Pamięć | tablice, teksty i struktury zwalniają się same; `new(...)` / `free(...)` dla własnych struktur na stercie | @@ -163,6 +164,7 @@ Wybierz przewodnik dla siebie: - [x] Wątki (`import thread`) i operacje atomowe; pliki C++; `--target android` - [x] Kompilator na każdym systemie, na który buduje: Linux, macOS (Apple Silicon i Intel), Windows; wydania robi CI - [x] Typy wektorowe (`f32x8`, `i8x32`, …) dla SIMD i `--cpu native` +- [x] Maski, przestawianie, tabele bajtów i `dot_add` dla jąder obliczeniowych; `@cpu` i `cpu_has`; `fma`/`muladd`, bity liczb, f16 i bf16 ## Co gdzie leży diff --git a/docs/en/cheatsheet.md b/docs/en/cheatsheet.md index 86fbf2e..6e21422 100644 --- a/docs/en/cheatsheet.md +++ b/docs/en/cheatsheet.md @@ -255,13 +255,19 @@ Types: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i | `f32x8(v)` | an 8-lane vector converted lane by lane (`f32x8(ints)`, `i32x8(floats)`) | | `v + w`, `v * 2.0`, `-v` | lane by lane; a plain number counts for every lane (`/` only on floats) | | `& \| ^ << >> ~` | lane by lane, whole-number lanes | -| `v == w`, `v != w` | bool: every lane equal / not | +| `v < w`, `v == w`, … | a mask `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` | +| `select(m, a, b)` | lane by lane: `a` where `m` is true, else `b` | | `v[i]`, `v[i] = x`, `v.len` | a lane; the number of lanes | | `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane | | `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | one number from all the lanes | | `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) | | `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: one rounding always / the fastest way here | | `v.store(list, i)` | lanes into `list[i]` … | +| `f32x8(list, i, fill)`, `v.store_part(list, i)` | the end of a list: only the lanes inside it (the others are `fill`) | +| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | lanes rearranged (shuffle takes numbers written in the code) | +| `table.lookup(idx)` | `table[idx % table.len]` per lane (`pshufb`, `tbl`) | +| `acc.dot_add(a, b)` | `i32x8` sums gain 4 byte products each (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` where there | +| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | a function that may use these features; check at run time before calling it | --- diff --git a/docs/en/for-engineers.md b/docs/en/for-engineers.md index 66c90cf..7b055a2 100644 --- a/docs/en/for-engineers.md +++ b/docs/en/for-engineers.md @@ -304,7 +304,13 @@ type goes; a call `f32x8(…)` goes to `vecMake()`. | `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… on the whole vector | | `v + w`, `v * 2.0` | `fadd`/`add`… on vectors; a scalar operand is coerced to the lane type, then splatted | | `v[i]` | a place: `getelementptr , ptr %v, i64 i` after a bounds check; or `extractelement` on a temporary | -| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` | +| `v < w`, `v == w` … | `fcmp`/`icmp` → ``, a mask (`boolxN`: `vec` with `bool` lanes) | +| `select(m, a, b)` | `select ` | +| `m.any()`, `.all()`, `.count()` | `llvm.vector.reduce.or`/`.and`; `zext` + `llvm.vector.reduce.add` | +| `f32x8(list, i, fill)`, `v.store_part(list, i)` | `llvm.masked.load`/`llvm.masked.store` with the mask `<0, 1, …> < len - i` | +| `v.shuffle(…)`, `.lo()`, `.hi()`, `concat(a, b)` | `shufflevector` with a constant mask | +| `table.lookup(idx)` | `and` with `len - 1`, then `extractelement`/`insertelement` per lane: the pattern the x86 and ARM backends turn into `pshufb`/`tbl` | +| `acc.dot_add(a, b)` | `llvm.x86.avx512.vpdpbusd.{128,256,512}` or `llvm.aarch64.neon.{usdot,sdot,udot}` when the function's features have them; else `zext`/`sext`, `mul`, four strided `shufflevector`s and `add`s | | `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` | | `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (with `reassoc`), `.add`, `.fmin`, `.smin`… | | `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | the `llvm.*` intrinsic of that name | @@ -323,6 +329,20 @@ aligned address and crash. **Which instructions** is clang's choice: `--cpu ` adds `-march=` (x86-64) or `-mcpu=` (ARM64) to the IR's compile, so the same IR becomes SSE2, AVX2 + FMA, AVX-512 or NEON code. Lanes wider than the registers are split. +Some operations need to know the features at code generation time (`dot_add` picks `vpdpbusd` only where it exists). +`linker.cpuFeatures()` asks clang for its predefined macros with the same target and `--cpu` +(`clang -march=X -dM -E empty.c`: `__AVXVNNI__`, `__AVX512VNNI__`, `__ARM_FEATURE_MATMUL_INT8` …), maps them to Finch's +feature names and caches the answer per clang and flags; they reach `gen` as `Options.features`. `@cpu("…")` on a function +(`Node.cpu`) adds its own: `define … "target-features"="+avx2,+fma"` (with `+evex512` for any AVX-512 feature, which +LLVM 18+ needs for 512-bit registers), and `g.curFeatures` is the union while that function is generated. Features of the +other architecture are dropped (`cpuOf`). `checkCpuCall` refuses a vector wider than 128 bits as a parameter or result +between functions whose `@cpu` differ, because caller and callee would lower it to different registers. +`cpu_has(name)` calls `finch_cpu_has` in the runtime: CPUID leaves 1 and 7 with XGETBV for the AVX/AVX-512 state on x86, +`getauxval(AT_HWCAP/AT_HWCAP2)` on Linux ARM64, `sysctlbyname("hw.optional.arm.FEAT_…")` on macOS. + +Clang is always run as `NIX_ENFORCE_NO_NATIVE=0 clang …` on Unix: Nix's cc-wrapper otherwise drops `-march=native` +inside `nix-shell`. If `--cpu native` still yields no SSE4.2 macro on x86, the driver warns. + --- ## 8. Ownership and memory @@ -572,5 +592,5 @@ evaluated twice and temporaries are never pointed to. - Generics are monomorphised (code size grows with instantiations); there are no trait bounds, a wrong use is an error inside the instantiated body. - A failed test's owned memory is not freed (the test runner `longjmp`s out of it). -- Vectors have no lane masks (`v < w`), shuffles or integer division. +- Vectors have no integer division, and `lookup` on baseline x86-64 (no SSSE3) is done lane by lane. diff --git a/docs/en/for-technicians.md b/docs/en/for-technicians.md index 02d55fe..c3ab1b7 100644 --- a/docs/en/for-technicians.md +++ b/docs/en/for-technicians.md @@ -1400,18 +1400,15 @@ arrays, maps and function parameters, and `print` and `str` show it. | `+ - * /` | lane by lane (`/` on floats only: x86 and ARM can't divide whole numbers lane by lane) | | `& \| ^ << >> ~` | lane by lane, on whole-number lanes | | `-v` | every lane negated | -| `v == w`, `v != w` | `true` if every lane is equal / if any differs | | `v[i]`, `v[i] = x`, `v.len` | one lane; the number of lanes | | `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | lane by lane; `lo`, `hi` and `w` can be plain numbers | | `v.sum()`, `v.min()`, `v.max()` | all the lanes made into one number | | `v.dot(w)` | `(v * w).sum()` | | `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | every lane (float lanes) | -| `v.fma(b, c)` | `v * b + c` with one rounding, always | -| `v.muladd(b, c)` | `v * b + c` the fastest way here: one instruction where the processor can | +| `v.fma(b, c)` | `v * b + c` with one rounding, always (the same on every processor) | +| `v.muladd(b, c)` | `v * b + c` the fastest way here: one instruction where the processor can (the last bit may differ between processors) | | `v.store(list, i)` | writes the lanes to `list[i]` … (or through a `ptr`) | -`<` and the other comparisons would give an answer per lane, so they aren't there; use `min`, `max` or `v[i] < w[i]`. - **A real loop.** The usual shape: as many whole vectors as fit, then the rest one by one. ```c @@ -1433,10 +1430,125 @@ fn dot([]f32 a, []f32 b) -> f32 { `examples/simd.fch` times this against the plain loop. -**Which instructions.** By default a program runs on every processor of its system (on x86-64 that means SSE2, so an -`f32x8` is done as two halves). `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the processor -that builds it has: AVX2 and FMA, AVX-512, or NEON's newer parts on ARM. Such a program needs a processor like that one to -run. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`. +### Masks: comparing, choosing, the end of a list + +Comparing vectors compares every lane, so the answer is a **mask**: one `true`/`false` a lane, of type `boolx8` (for 8 lanes). + +```c +v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0) +m := v > 0.0 // boolx8(false, false, false, true, true, true, false, true) +relu := select(m, v, 0.0) // v where the mask is true, 0 elsewhere +print(m.count(), m.any(), m.all()) // 4 true false +if (v == w).all() { ... } // a condition needs one bool: all() or any() +``` + +| | | +|---|---| +| `v < w`, `<=`, `>`, `>=`, `==`, `!=` | a mask (a plain number on one side counts for every lane) | +| `select(m, a, b)` | lane by lane: `a` where `m` is true, `b` where false (either may be a plain number) | +| `m.any()`, `m.all()`, `m.count()` | is any lane true, are all, how many | +| `m & n`, `m \| n`, `m ^ n`, `!m` | masks combined | +| `m[i]` | one lane, as a `bool` | +| `i32x8(m)`, `boolx8(v)` | true is 1; a lane that isn't 0 is true | + +**The end of a list.** A list is rarely a whole number of vectors long. Two forms touch only the lanes that are inside it: + +| | | +|---|---| +| `f32x8(list, i, fill)` | `list[i]` … as far as the list goes; the lanes past its end are `fill` | +| `v.store_part(list, i)` | writes only the lanes that fit in the list | + +```c +fn add_one([]f32 xs) { + i := 0 + while i < xs.len { // no separate loop for the rest + (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i) + i += 8 + } +} +``` + +### Moving lanes + +| | | +|---|---| +| `v.shuffle(3, 2, 1, 0)` | the lanes with these numbers (written in the code), in this order; as many lanes as numbers | +| `v.lo()`, `v.hi()` | the lower and the upper half | +| `concat(a, b)` | `a`'s lanes, then `b`'s | +| `table.lookup(idx)` | for every lane of `idx`: `table[idx % table.len]`. A byte table (`u8x16`, `i8x16`) is one `pshufb` on x86, `tbl` on ARM | + +Unpacking 4-bit numbers (two to a byte, as quantized models store them) is shuffle, mask and lookup: + +```c +fn unpack4(u8x8 packed, i8x16 meaning) -> i8x16 { + pairs := packed.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // 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, then high + return meaning.lookup(nibbles) // 0..15 -> what it stands for +} +``` + +### Int8 dot products: dot_add + +`acc.dot_add(a, b)` is the step of a quantized model's matrix multiply. `acc` is an `i32x8` of sums; `a` and `b` have four +times as many bytes (`u8x32` and `i8x32`; `i8` × `i8` and `u8` × `u8` work too). Each lane of `acc` gains the sum of four +products, `a[4k]*b[4k] + … + a[4k+3]*b[4k+3]`, exactly (no saturation), and the sums stay in the vector: + +```c +acc := i32x8(0) +i := 0 +while i + 32 <= weights.len { + acc = acc.dot_add(u8x32(activations, i), i8x32(weights, i)) + i += 32 +} +total := acc.sum() // once, at the end +``` + +It is one instruction where the processor has one: `vpdpbusd` with AVX-VNNI (Intel since Alder Lake, AMD Zen 5) or +AVX-512 VNNI, `usdot`/`sdot`/`udot` on ARM with the dot-product features. Elsewhere it is a few instructions +(`pmaddwd` and adds), with the same result. Which one is used depends on `--cpu` or the function's `@cpu` (below). + +### Choosing the instructions: --cpu, @cpu and cpu_has + +By default a program runs on every processor of its system (on x86-64 that means SSE2, so an `f32x8` is done as two +halves). Three ways to use more: + +**`--cpu`, for the whole program.** `--cpu native` (or `cpu = "native"` in `finch.toml`) lets LLVM use everything the +processor that builds it has: AVX2 and FMA, AVX-512, VNNI, or NEON's newer parts on ARM. The program then needs a processor +like that one. To pick a level instead: `--cpu x86-64-v3` (AVX2, most PCs since 2015), `--cpu x86-64-v4` (AVX-512), +`--cpu alderlake`, `--cpu apple-m1`, `--cpu neoverse-v1`. + +**`@cpu`, for one function, and `cpu_has` to choose at run time.** One program can carry an AVX-512 version, an AVX2 +version and a plain one, and pick the best the computer it runs on has: + +```c +@cpu("avx512f,avx512bw,avx512vl,avx512vnni") +fn matmul_avx512([]u8 a, []i8 b, []i32 out) { ... } // may use AVX-512 inside + +@cpu("x86-64-v3") +fn matmul_avx2([]u8 a, []i8 b, []i32 out) { ... } // a level works too + +fn matmul([]u8 a, []i8 b, []i32 out) { + if cpu_has("avx512vnni") && cpu_has("avx512vl") { + matmul_avx512(a, b, out) + } else if cpu_has("avx2") { + matmul_avx2(a, b, out) + } else { + matmul_plain(a, b, out) + } +} +``` + +- The features are x86's `sse3 ssse3 sse4.1 sse4.2 popcnt avx avx2 fma f16c bmi1 bmi2 avx512f avx512bw avx512dq avx512vl + avx512vnni avx512bf16 avx512fp16 avxvnni`, the levels `x86-64-v2`/`v3`/`v4`, and ARM's `neon fp16 dotprod i8mm bf16 sve sve2`. +- Names of the other architecture are left out when building, so the x86 and ARM versions sit side by side in one file + (and `cpu_has` says `false` for them). +- Call a `@cpu` function only after `cpu_has` said yes: on a processor without the feature it stops with *Illegal instruction*. +- A vector wider than 16 bytes (`f32x8`, `i32x16` …) can't be passed between functions with different `@cpu`: the two + sides would expect it in different registers. Pass the list (or a `ptr`) instead; Finch says so if you try. Lanes wider than the processor's registers are split by LLVM, and the same code builds for x86-64 and ARM64 alike. `v.sum()` on float lanes adds them in whatever order is fastest, so the last digits can differ from adding them one by one. + +On NixOS, Nix's clang wrapper removes `-march=native` inside `nix-shell`. Finch tells the wrapper to keep it, so +`--cpu native` works there too; if it ever has no effect, Finch warns. diff --git a/docs/pl/dla-inzynierow.md b/docs/pl/dla-inzynierow.md index 65d700f..eefbeb0 100644 --- a/docs/pl/dla-inzynierow.md +++ b/docs/pl/dla-inzynierow.md @@ -310,7 +310,13 @@ wszędzie tam, gdzie może stać typ; wywołanie `f32x8(…)` trafia do `vecMake | `f32x8(i32x8 v)` | `sitofp`, `fptosi`, `sext`, `trunc`… na całym wektorze | | `v + w`, `v * 2.0` | `fadd`/`add`… na wektorach; argument skalarny jest dopasowywany do typu elementu i rozpowielany | | `v[i]` | miejsce w pamięci: `getelementptr , ptr %v, i64 i` po sprawdzeniu zakresu; albo `extractelement` na wartości tymczasowej | -| `v == w` | `fcmp oeq`/`icmp eq` → `` → `llvm.vector.reduce.and` | +| `v < w`, `v == w` … | `fcmp`/`icmp` → ``, maska (`boolxN`: `vec` z elementami `bool`) | +| `select(m, a, b)` | `select ` | +| `m.any()`, `.all()`, `.count()` | `llvm.vector.reduce.or`/`.and`; `zext` + `llvm.vector.reduce.add` | +| `f32x8(lista, i, wypelnienie)`, `v.store_part(lista, i)` | `llvm.masked.load`/`llvm.masked.store` z maską `<0, 1, …> < len - i` | +| `v.shuffle(…)`, `.lo()`, `.hi()`, `concat(a, b)` | `shufflevector` ze stałą maską | +| `tabela.lookup(idx)` | `and` z `len - 1`, potem `extractelement`/`insertelement` dla każdego elementu: wzorzec, który zaplecza x86 i ARM zamieniają na `pshufb`/`tbl` | +| `acc.dot_add(a, b)` | `llvm.x86.avx512.vpdpbusd.{128,256,512}` albo `llvm.aarch64.neon.{usdot,sdot,udot}`, gdy funkcja ma te rozszerzenia; w przeciwnym razie `zext`/`sext`, `mul`, cztery `shufflevector` co czwarty element i `add` | | `min`, `max`, `abs` | `llvm.minnum`/`maxnum`, `llvm.smin`/`umin`…, `llvm.fabs`/`llvm.abs` | | `v.sum()`, `.min()`, `.max()` | `llvm.vector.reduce.fadd` (z `reassoc`), `.add`, `.fmin`, `.smin`… | | `v.sqrt()`, `.floor()`, `.ceil()`, `.round()` | funkcja wewnętrzna `llvm.*` o tej nazwie | @@ -331,6 +337,20 @@ użyć `vmovaps` na adresie wyrównanym do 16 i program by się wysypał. do kompilacji IR, więc to samo IR staje się kodem SSE2, AVX2 + FMA, AVX-512 albo NEON. Wektory szersze niż rejestry procesora są dzielone na części. +Niektóre działania muszą znać rozszerzenia procesora już przy generowaniu kodu (`dot_add` wybiera `vpdpbusd` tylko tam, +gdzie ono istnieje). `linker.cpuFeatures()` pyta clanga o makra, które sam definiuje dla tego samego systemu i `--cpu` +(`clang -march=X -dM -E pusty.c`: `__AVXVNNI__`, `__AVX512VNNI__`, `__ARM_FEATURE_MATMUL_INT8` …), zamienia je na nazwy +rozszerzeń Fincha i zapamiętuje odpowiedź dla danego clanga i flag; do `gen` trafiają jako `Options.features`. `@cpu("…")` +przy funkcji (`Node.cpu`) dodaje własne: `define … "target-features"="+avx2,+fma"` (z `+evex512` przy każdym rozszerzeniu +AVX-512, którego LLVM 18+ wymaga dla rejestrów 512-bitowych), a `g.curFeatures` to suma obu na czas generowania tej +funkcji. Rozszerzenia drugiej architektury są pomijane (`cpuOf`). `checkCpuCall` nie pozwala przekazać wektora szerszego +niż 128 bitów jako parametru albo wyniku między funkcjami o różnym `@cpu`, bo wywołujący i wywoływany umieściliby go +w innych rejestrach. `cpu_has(nazwa)` woła `finch_cpu_has` z runtime'u: na x86 CPUID (liście 1 i 7) z XGETBV dla stanu +AVX/AVX-512, na Linuksie ARM64 `getauxval(AT_HWCAP/AT_HWCAP2)`, na macOS `sysctlbyname("hw.optional.arm.FEAT_…")`. + +Na Uniksach clang jest zawsze uruchamiany jako `NIX_ENFORCE_NO_NATIVE=0 clang …`: inaczej nakładka cc-wrapper z Niksa +usuwa `-march=native` w `nix-shell`. Jeśli `--cpu native` mimo to nie daje na x86 makra SSE4.2, sterownik ostrzega. + --- ## 8. Własność i pamięć @@ -585,4 +605,4 @@ nie jest obliczane dwa razy, a wskaźnik nigdy nie wskazuje na wartość tymczas - Typy generyczne są monomorfizowane (kod rośnie z każdą instancją); nie ma ograniczeń na parametry typu, więc złe użycie daje błąd wewnątrz ciała instancji. - Pamięć, którą miał nieudany test, nie jest zwalniana (uruchamianie testów wychodzi z niego przez `longjmp`). -- Wektory nie mają masek (`v < w`), przetasowań ani dzielenia całkowitego. +- Wektory nie mają dzielenia całkowitego, a `lookup` na podstawowym x86-64 (bez SSSE3) działa element po elemencie. diff --git a/docs/pl/dla-technikow.md b/docs/pl/dla-technikow.md index 281a4a5..1285f4a 100644 --- a/docs/pl/dla-technikow.md +++ b/docs/pl/dla-technikow.md @@ -1408,18 +1408,15 @@ struktury, elementem tablicy, wartością w mapie i parametrem funkcji, a `print | `+ - * /` | element po elemencie (`/` tylko na liczbach zmiennoprzecinkowych: x86 i ARM nie dzielą liczb całkowitych element po elemencie) | | `& \| ^ << >> ~` | element po elemencie, na liczbach całkowitych | | `-v` | każdy element ze zmienionym znakiem | -| `v == w`, `v != w` | `true`, jeśli wszystkie elementy są równe / jeśli któryś się różni | | `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów | | `min(v, w)`, `max(v, w)`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie; `lo`, `hi` i `w` mogą być zwykłymi liczbami | | `v.sum()`, `v.min()`, `v.max()` | wszystkie elementy złożone w jedną liczbę | | `v.dot(w)` | `(v * w).sum()` | | `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) | -| `v.fma(b, c)` | `v * b + c` z jednym zaokrągleniem, zawsze | -| `v.muladd(b, c)` | `v * b + c` najszybciej, jak się da: jedną instrukcją, gdzie procesor to potrafi | +| `v.fma(b, c)` | `v * b + c` z jednym zaokrągleniem, zawsze (tak samo na każdym procesorze) | +| `v.muladd(b, c)` | `v * b + c` najszybciej, jak się da: jedną instrukcją, gdzie procesor to potrafi (ostatni bit może się różnić między procesorami) | | `v.store(lista, i)` | zapisuje elementy do `lista[i]` … (albo przez `ptr`) | -`<` i inne porównania dałyby osobną odpowiedź dla każdego elementu, więc ich nie ma; użyj `min`, `max` albo `v[i] < w[i]`. - **Prawdziwa pętla.** Zwykły schemat: tyle całych wektorów, ile się zmieści, a resztę po jednej liczbie. ```c @@ -1441,12 +1438,129 @@ fn iloczyn([]f32 a, []f32 b) -> f32 { `examples/simd.fch` mierzy czas tej pętli i porównuje go ze zwykłą. -**Jakie instrukcje.** Domyślnie program działa na każdym procesorze swojego systemu (na x86-64 oznacza to SSE2, więc -`f32x8` liczy się w dwóch połówkach). `--cpu native` (albo `cpu = "native"` w `finch.toml`) pozwala LLVM użyć wszystkiego, -co ma procesor, na którym budujesz: AVX2 i FMA, AVX-512 albo nowszych części NEON na ARM. Taki program potrzebuje potem +### Maski: porównywanie, wybieranie, koniec listy + +Porównanie wektorów porównuje każdy element, więc odpowiedzią jest **maska**: jedno `true`/`false` na element, +typu `boolx8` (dla 8 elementów). + +```c +v := f32x8(-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, -4.0, 5.0) +m := v > 0.0 // boolx8(false, false, false, true, true, true, false, true) +relu := select(m, v, 0.0) // v tam, gdzie maska jest prawdą, a gdzie indziej 0 +print(m.count(), m.any(), m.all()) // 4 true false +if (v == w).all() { ... } // warunek potrzebuje jednego bool: all() albo any() +``` + +| | | +|---|---| +| `v < w`, `<=`, `>`, `>=`, `==`, `!=` | maska (zwykła liczba po jednej stronie działa na każdy element) | +| `select(m, a, b)` | element po elemencie: `a` tam, gdzie `m` jest prawdą, `b` tam, gdzie fałszem (każde z nich może być zwykłą liczbą) | +| `m.any()`, `m.all()`, `m.count()` | czy któryś element jest prawdą, czy wszystkie, ile ich jest | +| `m & n`, `m \| n`, `m ^ n`, `!m` | łączenie masek | +| `m[i]` | jeden element jako `bool` | +| `i32x8(m)`, `boolx8(v)` | prawda to 1; element różny od 0 to prawda | + +**Koniec listy.** Lista rzadko ma długość będącą wielokrotnością wektora. Dwie formy dotykają tylko elementów, +które są w liście: + +| | | +|---|---| +| `f32x8(lista, i, wypelnienie)` | `lista[i]` … tak daleko, jak sięga lista; elementy za jej końcem dostają `wypelnienie` | +| `v.store_part(lista, i)` | zapisuje tylko elementy, które mieszczą się w liście | + +```c +fn dodaj_jeden([]f32 xs) { + i := 0 + while i < xs.len { // bez osobnej pętli na resztę + (f32x8(xs, i, 0.0) + 1.0).store_part(xs, i) + i += 8 + } +} +``` + +### Przestawianie elementów + +| | | +|---|---| +| `v.shuffle(3, 2, 1, 0)` | elementy o tych numerach (zapisanych w kodzie), w tej kolejności; tyle elementów, ile numerów | +| `v.lo()`, `v.hi()` | dolna i górna połowa | +| `concat(a, b)` | elementy `a`, a po nich elementy `b` | +| `tabela.lookup(idx)` | dla każdego elementu `idx`: `tabela[idx % tabela.len]`. Tabela bajtów (`u8x16`, `i8x16`) to jedno `pshufb` na x86 i `tbl` na ARM | + +Rozpakowanie liczb 4-bitowych (po dwie w bajcie, tak jak trzymają je skwantowane modele) to przestawienie, maska i tabela: + +```c +fn rozpakuj4(u8x8 spakowane, i8x16 znaczenie) -> i8x16 { + pary := spakowane.shuffle(0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7) // każdy bajt dwa razy + nieparzyste := u8x16(0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) == 1 + polowki := select(nieparzyste, pary >> 4, pary & 15) // najpierw dolne 4 bity, potem górne + return znaczenie.lookup(polowki) // 0..15 -> to, co oznacza +} +``` + +### Iloczyn skalarny na bajtach: dot_add + +`acc.dot_add(a, b)` to krok mnożenia macierzy w skwantowanym modelu. `acc` to `i32x8` z sumami; `a` i `b` mają cztery +razy więcej bajtów (`u8x32` i `i8x32`; działają też `i8` × `i8` i `u8` × `u8`). Każdy element `acc` dostaje sumę czterech +iloczynów, `a[4k]*b[4k] + … + a[4k+3]*b[4k+3]`, dokładnie (bez nasycania), a sumy zostają w wektorze: + +```c +acc := i32x8(0) +i := 0 +while i + 32 <= wagi.len { + acc = acc.dot_add(u8x32(aktywacje, i), i8x32(wagi, i)) + i += 32 +} +suma := acc.sum() // raz, na końcu +``` + +To jedna instrukcja tam, gdzie procesor ją ma: `vpdpbusd` z AVX-VNNI (Intel od Alder Lake, AMD Zen 5) albo +AVX-512 VNNI, `usdot`/`sdot`/`udot` na ARM z rozszerzeniami do iloczynu skalarnego. Gdzie indziej to kilka instrukcji +(`pmaddwd` i dodawania), z tym samym wynikiem. Wybór zależy od `--cpu` albo `@cpu` funkcji (niżej). + +### Wybór instrukcji: --cpu, @cpu i cpu_has + +Domyślnie program działa na każdym procesorze swojego systemu (na x86-64 oznacza to SSE2, więc `f32x8` liczy się +w dwóch połówkach). Są trzy sposoby, żeby użyć więcej: + +**`--cpu`, dla całego programu.** `--cpu native` (albo `cpu = "native"` w `finch.toml`) pozwala LLVM użyć wszystkiego, +co ma procesor, na którym budujesz: AVX2 i FMA, AVX-512, VNNI albo nowszych części NEON na ARM. Program potrzebuje wtedy podobnego procesora. Można też wybrać poziom: `--cpu x86-64-v3` (AVX2, większość komputerów od 2015 roku), -`--cpu x86-64-v4` (AVX-512), `--cpu apple-m1`. +`--cpu x86-64-v4` (AVX-512), `--cpu alderlake`, `--cpu apple-m1`, `--cpu neoverse-v1`. + +**`@cpu` dla jednej funkcji i `cpu_has` do wyboru w trakcie działania.** Jeden program może mieć wersję z AVX-512, +wersję z AVX2 i zwykłą, i wybrać najlepszą, jaką ma komputer, na którym działa: + +```c +@cpu("avx512f,avx512bw,avx512vl,avx512vnni") +fn mnoz_avx512([]u8 a, []i8 b, []i32 wynik) { ... } // może w środku używać AVX-512 + +@cpu("x86-64-v3") +fn mnoz_avx2([]u8 a, []i8 b, []i32 wynik) { ... } // poziom też działa + +fn mnoz([]u8 a, []i8 b, []i32 wynik) { + if cpu_has("avx512vnni") && cpu_has("avx512vl") { + mnoz_avx512(a, b, wynik) + } else if cpu_has("avx2") { + mnoz_avx2(a, b, wynik) + } else { + mnoz_zwykle(a, b, wynik) + } +} +``` + +- Rozszerzenia to dla x86 `sse3 ssse3 sse4.1 sse4.2 popcnt avx avx2 fma f16c bmi1 bmi2 avx512f avx512bw avx512dq avx512vl + avx512vnni avx512bf16 avx512fp16 avxvnni`, poziomy `x86-64-v2`/`v3`/`v4`, a dla ARM `neon fp16 dotprod i8mm bf16 sve sve2`. +- Nazwy z drugiej architektury są przy budowaniu pomijane, więc wersje dla x86 i ARM mogą stać obok siebie w jednym pliku + (a `cpu_has` zwraca dla nich `false`). +- Funkcję z `@cpu` wołaj dopiero wtedy, gdy `cpu_has` odpowie „tak”: na procesorze bez tego rozszerzenia program + zatrzyma się z *Illegal instruction*. +- Wektora szerszego niż 16 bajtów (`f32x8`, `i32x16` …) nie da się przekazać między funkcjami o różnym `@cpu`: obie strony + oczekiwałyby go w innych rejestrach. Przekaż zamiast tego listę (albo `ptr`); Finch o tym powie, jeśli spróbujesz. Wektory szersze niż rejestry procesora LLVM dzieli na części, a ten sam kod buduje się tak samo na x86-64 i na ARM64. `v.sum()` na liczbach zmiennoprzecinkowych dodaje elementy w kolejności, która jest najszybsza, więc ostatnie cyfry mogą się różnić od dodawania ich po kolei. + +Na NixOS nakładka Niksa na clanga usuwa `-march=native` w `nix-shell`. Finch mówi jej, żeby to zostawiła, więc +`--cpu native` działa i tam; gdyby kiedyś nie miało żadnego skutku, Finch ostrzeże. diff --git a/docs/pl/sciaga.md b/docs/pl/sciaga.md index fe062ae..1d88ed0 100644 --- a/docs/pl/sciaga.md +++ b/docs/pl/sciaga.md @@ -255,13 +255,19 @@ Typy: `f32x4/8/16`, `f64x2/4/8`, `i8x16/32/64`, `i16x8/16/32`, `i32x4/8/16`, `i6 | `f32x8(v)` | 8-elementowy wektor przeliczony element po elemencie (`f32x8(calkowite)`, `i32x8(ulamki)`) | | `v + w`, `v * 2.0`, `-v` | element po elemencie; zwykła liczba działa na każdy element (`/` tylko dla liczb zmiennoprzecinkowych) | | `& \| ^ << >> ~` | element po elemencie, dla liczb całkowitych | -| `v == w`, `v != w` | bool: czy wszystkie elementy są równe / czy któryś się różni | +| `v < w`, `v == w`, … | maska `boolx8`: `m.all()`, `m.any()`, `m.count()`, `m & n`, `!m`, `m[i]` | +| `select(m, a, b)` | element po elemencie: `a` tam, gdzie `m` jest prawdą, a gdzie nie, `b` | | `v[i]`, `v[i] = x`, `v.len` | jeden element; liczba elementów | | `min(v, w)`, `max`, `abs(v)`, `clamp(v, lo, hi)` | element po elemencie | | `v.sum()`, `v.min()`, `v.max()`, `v.dot(w)` | jedna liczba ze wszystkich elementów | | `v.sqrt()`, `v.floor()`, `v.ceil()`, `v.round()` | każdy element (liczby zmiennoprzecinkowe) | | `v.fma(b, c)`, `v.muladd(b, c)` | `v * b + c`: zawsze z jednym zaokrągleniem / najszybciej na tym procesorze | | `v.store(lista, i)` | zapisuje elementy do `lista[i]` … | +| `f32x8(lista, i, wypelnienie)`, `v.store_part(lista, i)` | koniec listy: tylko elementy, które są w niej (pozostałe to `wypelnienie`) | +| `v.shuffle(3, 2, 1, 0)`, `v.lo()`, `v.hi()`, `concat(a, b)` | przestawione elementy (shuffle przyjmuje numery zapisane w kodzie) | +| `tabela.lookup(idx)` | `tabela[idx % tabela.len]` dla każdego elementu (`pshufb`, `tbl`) | +| `acc.dot_add(a, b)` | sumy `i32x8` dostają po 4 iloczyny bajtów (`u8x32` × `i8x32`): `vpdpbusd`, `usdot` tam, gdzie są | +| `@cpu("avx2,fma") fn …`, `cpu_has("avx2")` | funkcja, która może używać tych rozszerzeń; przed jej wywołaniem sprawdź procesor | --- diff --git a/editors/kate/finch.xml b/editors/kate/finch.xml index f9f2642..b8a9229 100644 --- a/editors/kate/finch.xml +++ b/editors/kate/finch.xml @@ -82,6 +82,19 @@ atomic_store atomic_swap atomic_cas + select + concat + fma + muladd + cpu_has + f32_bits + f32_from_bits + f64_bits + f64_from_bits + f16_to_f32 + f32_to_f16 + bf16_to_f32 + f32_to_bf16 @@ -104,7 +117,8 @@ - + + diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md index a0a750c..fc8f613 100644 --- a/editors/vscode/CHANGELOG.md +++ b/editors/vscode/CHANGELOG.md @@ -2,7 +2,8 @@ ## 2.6.0 -- Vector types (`f32x8`, `i8x32`, `f64x4`, …) highlighted as types; completion of their methods (`sum`, `dot`, `fma`, `store`, …). +- Vector types (`f32x8`, `i8x32`, `f64x4`, …) and masks (`boolx8`) highlighted as types; completion of their methods (`sum`, `dot_add`, `shuffle`, `lookup`, `store`, …). +- `@cpu(...)` and the new built-ins (`select`, `concat`, `fma`, `muladd`, `cpu_has`, `f32_bits`, `f16_to_f32`, …) highlighted. ## 2.5.0 diff --git a/editors/vscode/syntaxes/finch.tmLanguage.json b/editors/vscode/syntaxes/finch.tmLanguage.json index e3c2dea..8ce8dd8 100644 --- a/editors/vscode/syntaxes/finch.tmLanguage.json +++ b/editors/vscode/syntaxes/finch.tmLanguage.json @@ -147,11 +147,15 @@ "match": "\\bself\\b" }, { - "name": "storage.type.primitive.finch", - "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|[iuf](?:8|16|32|64)x(?:2|4|8|16|32|64))\\b" + "name": "storage.modifier.attribute.finch", + "match": "@cpu\\b" }, { - "match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas)\\s*(?=\\()", + "name": "storage.type.primitive.finch", + "match": "\\b(int|float|bool|char|str|ptr|map|i8|i16|i32|i64|u8|u16|u32|u64|f32|f64|(?:[iuf](?:8|16|32|64)|bool)x(?:2|4|8|16|32|64))\\b" + }, + { + "match": "\\b(print|input|read_file|write_file|file_exists|delete_file|shell|exit|addr|new|free|error|min|max|abs|clamp|assert|atomic_add|atomic_sub|atomic_load|atomic_store|atomic_swap|atomic_cas|select|concat|fma|muladd|cpu_has|f32_bits|f32_from_bits|f64_bits|f64_from_bits|f16_to_f32|f32_to_f16|bf16_to_f32|f32_to_bf16)\\s*(?=\\()", "captures": { "1": { "name": "support.function.builtin.finch" diff --git a/editors/vscode/test/grammar.js b/editors/vscode/test/grammar.js index 0903ec5..ce951a9 100644 --- a/editors/vscode/test/grammar.js +++ b/editors/vscode/test/grammar.js @@ -30,6 +30,7 @@ const code = [ ' assert(clamp(x, 0, 9) == 3)', '}', 'fn dot(f32x8 a, i8x32 b) {', + '@cpu("avx2") fn k(boolx8 m) { select(m, a, b) }', ]; const expect = [ // [line, token text, scope that must be present] [0, 'import', 'keyword.control.import'], [0, '"stdio.h"', 'string.quoted.double'], @@ -51,6 +52,7 @@ const expect = [ // [line, token text, scope that must be present] [17, 'f', 'meta.embedded.line'], [17, 'left', 'string.quoted.double'], [18, 'assert', 'support.function.builtin'], [18, 'clamp', 'support.function.builtin'], [20, 'f32x8', 'storage.type.primitive'], [20, 'i8x32', 'storage.type.primitive'], + [21, '@cpu', 'storage.modifier.attribute'], [21, 'boolx8', 'storage.type.primitive'], [21, 'select', 'support.function.builtin'], ]; registry.loadGrammar('source.finch').then((g) => { let state = vsctm.INITIAL, toks = [];