|
| 1 | +# Native macOS SIMD acceleration via Apple Accelerate framework |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +The `skainet-backend-cpu` module on Kotlin/Native macOS (macosArm64) uses plain scalar loops |
| 6 | +for all tensor operations (`DefaultCpuOps`). On JVM, the same module uses the JDK Vector API |
| 7 | +for SIMD-accelerated matmul, elementwise ops, and reductions (`DefaultCpuOpsJvm`), which gives |
| 8 | +a significant performance advantage. |
| 9 | + |
| 10 | +When running LLM inference benchmarks via the `llm-performance` native binary, the CPU backend |
| 11 | +is 5-10x slower than it needs to be because every matmul is a triple-nested scalar loop |
| 12 | +(`DefaultCpuOps.kt:264-272`). |
| 13 | + |
| 14 | +## Proposed solution |
| 15 | + |
| 16 | +Add an Accelerate-backed `TensorOps` implementation for the macOS native target, mirroring |
| 17 | +how the JVM target has `DefaultCpuOpsJvm`. Apple's Accelerate framework provides |
| 18 | +hardware-optimized BLAS and vector DSP routines that leverage ARM NEON and AMX under the hood. |
| 19 | + |
| 20 | +### Architecture |
| 21 | + |
| 22 | +``` |
| 23 | +PlatformCpuOpsFactory |
| 24 | + ├── jvmMain → DefaultCpuOpsJvm (Vector API + optional BLAS) ← exists |
| 25 | + ├── nativeMain → DefaultCpuOps (scalar fallback) ← exists |
| 26 | + ├── macosMain → AccelerateCpuOps (Accelerate framework via cinterop) ← NEW |
| 27 | + └── linuxMain → DefaultCpuOps (scalar, or OpenBLAS in future) ← unchanged |
| 28 | +``` |
| 29 | + |
| 30 | +### Key changes |
| 31 | + |
| 32 | +**1. Cinterop definition** — `src/nativeInterop/cinterop/accelerate.def` |
| 33 | + |
| 34 | +```def |
| 35 | +package = platform.accelerate |
| 36 | +language = C |
| 37 | +headers = Accelerate/Accelerate.h |
| 38 | +compilerOpts = -framework Accelerate |
| 39 | +linkerOpts = -framework Accelerate |
| 40 | +``` |
| 41 | + |
| 42 | +**2. New class** — `src/macosMain/kotlin/.../AccelerateCpuOps.kt` |
| 43 | + |
| 44 | +Extends `DefaultCpuOps` and overrides hot-path operations with Accelerate calls: |
| 45 | + |
| 46 | +| Priority | Operation | Accelerate function | Impact | |
| 47 | +|----------|-----------|---------------------|--------| |
| 48 | +| P0 | `matmul` | `cblas_sgemm` | Dominant cost in LLM inference (~90% of forward pass) | |
| 49 | +| P1 | `add` | `vDSP_vadd` | Elementwise add (residual connections) | |
| 50 | +| P1 | `multiply` | `vDSP_vmul` | Elementwise multiply (gates, scaling) | |
| 51 | +| P1 | `subtract` | `vDSP_vsub` | Elementwise subtract | |
| 52 | +| P1 | `divide` | `vDSP_vdiv` | Elementwise divide | |
| 53 | +| P2 | `sum` (global) | `vDSP_sve` | Reduction for normalization | |
| 54 | +| P2 | `mean` (global) | `vDSP_meanv` | Reduction for normalization | |
| 55 | +| P2 | `softmax` | `vDSP_vse` + manual | Attention weights | |
| 56 | +| P3 | `relu` | `vDSP_vthres` / `vDSP_vthr` | Activation function | |
| 57 | +| P3 | `silu` | manual vectorized loop | Activation function (SiLU = x * sigmoid(x)) | |
| 58 | +| P3 | `transpose` | `vDSP_mtrans` | Matrix transpose | |
| 59 | + |
| 60 | +**3. Platform factory** — update `PlatformCpuOpsFactory` for macOS |
| 61 | + |
| 62 | +```kotlin |
| 63 | +// src/macosMain/kotlin/.../PlatformCpuOpsFactory.macos.kt |
| 64 | +internal actual fun platformDefaultCpuOpsFactory(): (TensorDataFactory) -> TensorOps { |
| 65 | + println("[SKaiNET] Using Accelerate-backed CPU operations (ARM NEON + AMX)") |
| 66 | + return { factory -> AccelerateCpuOps(factory) } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +This requires splitting the current `nativeMain` expect/actual into separate |
| 71 | +`macosMain` and `linuxMain` actuals (the `macosMain` source set already exists in |
| 72 | +`build.gradle.kts`). |
| 73 | + |
| 74 | +**4. Build changes** — `build.gradle.kts` |
| 75 | + |
| 76 | +Add cinterop configuration for macosArm64 (and optionally iosArm64/iosSimulatorArm64): |
| 77 | + |
| 78 | +```kotlin |
| 79 | +macosArm64 { |
| 80 | + compilations["main"].cinterops { |
| 81 | + val accelerate by creating { |
| 82 | + defFile("src/nativeInterop/cinterop/accelerate.def") |
| 83 | + } |
| 84 | + } |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +Add linker opts for the Accelerate framework to all macOS/iOS binaries. |
| 89 | + |
| 90 | +### Implementation notes |
| 91 | + |
| 92 | +- `AccelerateCpuOps` should extend `DefaultCpuOps` and override only the operations above. |
| 93 | + Non-accelerated operations fall through to the scalar implementation. |
| 94 | +- The `matmul` override should handle 2D FP32 tensors with `cblas_sgemm` and delegate |
| 95 | + batched/non-float cases to `super.matmul()`. |
| 96 | +- `vDSP_*` functions operate on contiguous `FloatArray` buffers. Tensors backed by |
| 97 | + `FloatArrayTensorData` can be passed directly; others need a `toFloatArray()` copy. |
| 98 | +- Broadcasting logic (e.g., bias add, scalar multiply) should remain in the Kotlin layer |
| 99 | + and only dispatch the contiguous inner loop to Accelerate. |
| 100 | +- The same approach works for iOS targets (`iosArm64`, `iosSimulatorArm64`) since |
| 101 | + Accelerate is available on all Apple platforms. |
| 102 | + |
| 103 | +### Testing |
| 104 | + |
| 105 | +- Existing `DefaultCpuOps` tests in `commonTest` should pass unchanged (numerical equivalence). |
| 106 | +- Add macOS-specific tests verifying Accelerate dispatch actually occurs (e.g., check log output |
| 107 | + or add a query method). |
| 108 | +- Benchmark comparison: run `llm-performance` native benchmark with the current scalar backend |
| 109 | + vs Accelerate backend on the same model. |
| 110 | + |
| 111 | +### Expected impact |
| 112 | + |
| 113 | +Based on JVM BLAS vs scalar measurements and Apple's published Accelerate performance data: |
| 114 | + |
| 115 | +- **matmul**: 10-50x speedup (NEON + AMX vs scalar loop) |
| 116 | +- **elementwise**: 4-8x speedup (NEON vectorization) |
| 117 | +- **reductions**: 4-8x speedup (NEON vectorization) |
| 118 | +- **overall LLM inference**: 5-20x speedup on native macOS CPU backend |
| 119 | + |
| 120 | +### Files to create/modify |
| 121 | + |
| 122 | +``` |
| 123 | +skainet-backends/skainet-backend-cpu/ |
| 124 | +├── build.gradle.kts # add cinterop |
| 125 | +├── src/nativeInterop/cinterop/accelerate.def # NEW |
| 126 | +├── src/macosMain/kotlin/.../AccelerateCpuOps.kt # NEW |
| 127 | +├── src/macosMain/kotlin/.../PlatformCpuOpsFactory.macos.kt # NEW |
| 128 | +├── src/linuxMain/kotlin/.../PlatformCpuOpsFactory.linux.kt # NEW (move from nativeMain) |
| 129 | +└── src/nativeMain/kotlin/.../PlatformCpuOpsFactory.native.kt # REMOVE (split to platform-specific) |
| 130 | +``` |
| 131 | + |
| 132 | +### References |
| 133 | + |
| 134 | +- JVM SIMD implementation: `src/jvmMain/kotlin/.../DefaultCpuOpsJvm.kt` |
| 135 | +- JVM BLAS integration: `src/jvmMain/kotlin/.../JvmBlas.kt` |
| 136 | +- Apple Accelerate docs: https://developer.apple.com/documentation/accelerate |
| 137 | +- CBLAS reference: https://developer.apple.com/documentation/accelerate/blas |
| 138 | +- vDSP reference: https://developer.apple.com/documentation/accelerate/vdsp |
0 commit comments