Buckets:
| # Hexagon backend developer details | |
| ## Backend libraries | |
| The Hexagon backend consists of two parts: | |
| - `libggml-hexagon` | |
| This is the regular CPU-side GGML backend library, either shared or statically linked. | |
| - `libggml-htp-vNN` | |
| This is the NPU-side (HTP stands for Hexagon Tensor Processor) shared library that contains the Op dispatcher and kernels. | |
| The correct library is selected automatically at runtime based on the HW version. | |
| Here is an example of the build artifacts: | |
| ``` | |
| ~/src/llama.cpp$ ls -l pkg-adb/llama.cpp/lib/libggml* | |
| pkg-adb/llama.cpp/lib/libggml-base.so | |
| pkg-adb/llama.cpp/lib/libggml-cpu.so | |
| pkg-adb/llama.cpp/lib/libggml-hexagon.so <<< CPU library | |
| pkg-adb/llama.cpp/lib/libggml-htp-v73.so <<< HTP op/kernels for Hexagon v73 | |
| pkg-adb/llama.cpp/lib/libggml-htp-v75.so | |
| pkg-adb/llama.cpp/lib/libggml-htp-v79.so | |
| pkg-adb/llama.cpp/lib/libggml-htp-v81.so | |
| ``` | |
| ## Memory buffers | |
| The Hexagon NPU backend takes advantage of Snapdragon unified memory where all DDR buffers are accessible by CPU, GPU, and NPU. | |
| The NPU has dedicated tightly-coupled memory called VTCM (Vector Tightly-Coupled Memory). VTCM is used for intermediate data (such as | |
| dynamically quantized activations) and streaming buffers (chunks of weight and activation tensors fetched via DMA). | |
| ## Large model handling | |
| Hexagon NPU sessions have a 32-bit virtual address space window of around 3.5GB. | |
| In llama.cpp/GGML, each Hexagon session is mapped to a single GGML backend device (e.g., `HTP0:0`, `HTP0:1`, etc. when using | |
| `GGML_HEXAGON_DEVICES`, or `HTP0`, `HTP1` in legacy mode). | |
| To support running models larger than 3.5GB on a single device, the Hexagon backend dynamically maps and unmaps buffers: | |
| - Buffers are allocated in shared DDR (RPCMEM) and mapped through FastRPC file descriptors. Non-pinned buffers use delayed | |
| mappings (`FASTRPC_MAP_FD_DELAYED` or `FASTRPC_MAP_FD_DELAYED_EXTENDED`). | |
| - Pinned buffers (such as KV cache and active compute buffers) remain mapped throughout execution. | |
| - Inactive weight buffers are dynamically mapped into the NPU session during batch buffer preparation | |
| (`prep_op_bufs()` in `htp/main.c`) and unmapped via `htp_iface_munmap()` when no longer needed by the active batch. | |
| - This dynamic sliding window allows a single NPU session to execute models that exceed the 3.5GB window. | |
| Alternatively, users can partition and split the model across multiple virtual sessions or physical NPUs using layer-splitting, | |
| tensor-splitting, or row-splitting modes. For user-facing execution modes and examples, see the | |
| [Snapdragon user guide](README.md#multi-device-execution-modes). | |
| ## Tiled Weight Formats | |
| Weight tensors in the Hexagon backend are repacked from flat row-major GGML layouts into 2D tiles in memory. | |
| Tiling groups 2D matrix blocks contiguously: | |
| - Eliminates strided row reads when fetching 2D tiles into VTCM via DMA. | |
| - Matches native HVX vector width (128 bytes) and HMX systolic array dimensions (32x32). | |
| - Repacking happens on the host during model load (`set_tensor()`) or via runtime repacking (`rt-repack`). | |
| Implementation: [`htp-repack.cpp`](../../../ggml/src/ggml-hexagon/htp-repack.cpp). | |
| ### Tile Dimensions and Grid Layout | |
| Each tile represents a submatrix of `N_rows = 32` rows and `K_cols` columns: | |
| - Standard types (`Q4_0`, `Q4_1`, `Q8_0`, `IQ4_NL`, `MXFP4`, and K-quants): `K_cols = 32` (`HTP_TENSOR_TILE_N_COLS = 32`). | |
| - `Q2_0`: `K_cols = 64` (`HTP_TENSOR_TILE_K_Q2_0 = 64`). | |
| - `Q1_0`: `K_cols = 128` (`HTP_TENSOR_TILE_K_Q1_0 = 128`). | |
| For a weight matrix of logical shape `[ne0, ne1]` (where `ne0 = K`, `ne1 = N`): | |
| - `n_k_tiles = ne0 / K_cols` | |
| - `n_col_tiles = (ne1 + 31) / 32` | |
| - Tile index for column-tile `kt` and row-tile `ct`: `tile_idx = ct * n_k_tiles + kt` | |
| - Byte offset in matrix buffer: `tile_offset = tile_idx * tile_size` | |
| ### Block-32 Quantized Formats | |
| Block-32 formats use a tile size of 32 rows x 32 columns (`K_cols = 32`): | |
| - **Q4_0** (576 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q4_0`): | |
| - **Quants** (bytes 0..511, 512 bytes): | |
| Contains 32 rows x 32 columns of 4-bit quants. | |
| Divided into 16 column pairs `cp = 0..15` covering columns `2*cp` and `2*cp + 1`. | |
| For each column pair `cp`, 32 bytes are stored consecutively for rows `row = 0..31`: | |
| `tile[cp * 32 + row]` holds `(quant[row][2*cp + 1] << 4) | quant[row][2*cp]`. | |
| Low nibble holds the even column; high nibble holds the odd column. | |
| - **Scales** (bytes 512..575, 64 bytes): | |
| 32 FP16 values (`ggml_half`), storing scale `d` for each row `row = 0..31`. | |
| - **Q8_0** (1088 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q8_0`): | |
| - **Quants** (bytes 0..1023, 1024 bytes): | |
| Contains 32 rows x 32 columns of signed 8-bit quants (`int8_t`). | |
| Divided into 16 column pairs `cp = 0..15` covering columns `col0 = 2*cp` and `col1 = 2*cp + 1`. | |
| For each column pair `cp`, 64 bytes store interleaved column elements for each row `row = 0..31`: | |
| `tile[cp * 64 + 2 * row + 0] = quant[row][col0]` | |
| `tile[cp * 64 + 2 * row + 1] = quant[row][col1]` | |
| - **Scales** (bytes 1024..1087, 64 bytes): | |
| 32 FP16 values (`ggml_half`), storing scale `d` for each row `row = 0..31`. | |
| - **IQ4_NL** (576 bytes per tile, `HTP_TENSOR_TILE_SIZE_IQ4_NL`): | |
| - **Quants** (bytes 0..511, 512 bytes): | |
| Same nibble transposition and storage layout as `Q4_0`. | |
| Contains 32 rows x 32 columns of non-linear 4-bit indices (values 0..15). | |
| Divided into 16 column pairs `cp = 0..15`: | |
| `tile[cp * 32 + row]` holds `(quant[row][2*cp + 1] << 4) | quant[row][2*cp]`. | |
| - **Scales** (bytes 512..575, 64 bytes): | |
| 32 FP16 values (`ggml_half`), storing scale `d` for each row `row = 0..31`. | |
| - **Dequantization**: | |
| In HVX/HMX kernels, each 4-bit index is looked up in the 16-entry non-linear table (`iq4_nl_to_fp16_lut`) to produce an FP16 value, then multiplied by the row scale `d`. | |
| - **MXFP4** (544 bytes per tile, `HTP_TENSOR_TILE_SIZE_MXFP4`): | |
| - **Quants** (bytes 0..511, 512 bytes): | |
| Same nibble transposition and storage layout as `Q4_0`. | |
| Contains 32 rows x 32 columns of 4-bit OCP Microscaling FP4 (E2M1) values. | |
| Divided into 16 column pairs `cp = 0..15`: | |
| `tile[cp * 32 + row]` holds `(quant[row][2*cp + 1] << 4) | quant[row][2*cp]`. | |
| - **Scales** (bytes 512..543, 32 bytes): | |
| 32 unsigned 8-bit values (`uint8_t`), storing one 8-bit biased exponent (E8M0, bias 127) scale `e` per row `row = 0..31`. | |
| - **Dequantization**: | |
| Kernel converts E8M0 bias to IEEE FP16 bias via `vh = clamp(e - 112, 0, 30) << 10` to form a power-of-two FP16 scale. | |
| Each 4-bit FP4 nibble is unpacked via `mxfp4_to_fp16_lut` and multiplied by this scale. | |
| - **Q4_1** (640 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q4_1`): | |
| - **Quants** (bytes 0..511, 512 bytes): | |
| Same 4-bit nibble transposition and storage layout as `Q4_0`. | |
| - **Scales** (bytes 512..639, 128 bytes): | |
| 32 pairs of FP16 values (`ggml_half`): `scale[2 * row + 0] = d` (scale) and `scale[2 * row + 1] = m` (min) for each row `row = 0..31`. | |
| ### Q1_0 Format (1-Bit Quantization) | |
| `Q1_0` packs 128 columns per tile (`K_cols = 128`, `HTP_TENSOR_TILE_K_Q1_0 = 128`): | |
| - **Tile Layout** (576 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q1_0`): | |
| - **Quants** (bytes 0..511, 512 bytes): | |
| Contains 32 rows x 128 columns of 1-bit quants (-1 or +1). | |
| Divided into 4 quarters (`quarter = 0..3`), where each quarter covers 32 columns in K. | |
| Each quarter occupies 128 bytes (1 HVX vector) in `vrmpy` byte order. | |
| Within a quarter, columns `kq = 0..31` map into 8 groups `g = kq / 4` (0..7) of 4 consecutive columns: | |
| `byte_offset = quarter * 128 + 4 * row + (kq % 4)` | |
| `bit = g` (0..7) | |
| - **Scales** (bytes 512..575, 64 bytes): | |
| 32 FP16 values (`ggml_half`), storing scale `d` for each row `row = 0..31`. | |
| One scale per row applies to all 128 columns (all 4 quarters) of the tile. | |
| - **HMX Mapping (1-to-4 Tile Expansion)**: | |
| - HMX hardware operates strictly on 32x32 FP16 tiles (`HTP_TENSOR_TILE_N_ROWS = 32`, `HTP_TENSOR_TILE_N_COLS = 32`). | |
| - During kernel execution, HVX dequantization (`dequantize_tiled_weight_to_fp16_task_q1_0`) expands each 32x128 Q1_0 tile (576 bytes) into 4 contiguous 32x32 FP16 HMX tiles (4 x 2048 bytes = 8192 bytes total) in VTCM. | |
| - HMX systolic array processes the 4 resulting 32x32 FP16 tiles over 4 compute steps along K. | |
| ### Q2_0 Format (2-Bit Quantization) | |
| `Q2_0` packs 64 columns per tile (`K_cols = 64`, `HTP_TENSOR_TILE_K_Q2_0 = 64`): | |
| - **Tile Layout** (576 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q2_0`): | |
| - **Quants** (bytes 0..511, 512 bytes): | |
| Contains 32 rows x 64 columns of 2-bit quants (codes 0..3 representing signed values -1..2). | |
| Divided into 16 column groups `group = 0..15` of 4 columns each. | |
| Stored in `vrmpy` byte order across 4 HVX vectors (vec 0..3): | |
| Vector `group >> 2` (0..3) holds `group & 3` at bit offset `2 * (group & 3)`. | |
| Byte `4 * row + byte` holds row `row = 0..31` and column byte `0..3`. | |
| Vec 0..1 holds the first 32 columns (half 0), vec 2..3 holds the next 32 columns (half 1). | |
| - **Scales** (bytes 512..575, 64 bytes): | |
| 32 FP16 values (`ggml_half`), storing scale `d` for each row `row = 0..31` (at offset `512 + 2 * row`). | |
| One scale per row applies to all 64 columns of the tile. | |
| - **HMX Mapping (1-to-2 Tile Expansion)**: | |
| - HMX hardware operates strictly on 32x32 FP16 tiles (`HTP_TENSOR_TILE_N_ROWS = 32`, `HTP_TENSOR_TILE_N_COLS = 32`). | |
| - During kernel execution, HVX dequantization (`dequantize_tiled_weight_to_fp16_task_q2_0`) expands each 32x64 Q2_0 tile (576 bytes) into 2 contiguous 32x32 FP16 HMX tiles (2 x 2048 bytes = 4096 bytes total) in VTCM. | |
| - HMX systolic array processes the 2 resulting 32x32 FP16 tiles over 2 compute steps along K. | |
| - **HVX Vector Dot-Product**: | |
| - `tiled_vec_dot_q2_0_32x1` and `tiled_vec_dot_q2_0_32x2` unpack 2-bit quants using `unpack_q3_k_low2`, subtract 1, and accumulate against Q8_0 activation tiles using `Q6_Vw_vrmpyacc_VwVbVb`. | |
| ### K-Quants Formats | |
| In GGML, K-quants pack 256 elements per super-block. | |
| On Hexagon, each 256-element super-block splits along K into 8 tiles of 32 columns (`K_cols = 32`). | |
| Byte ordering: `Q2_K`, `Q3_K`, and `Q6_K` use `vrmpy` byte order within each 128B HVX vector: | |
| `k = 0..31` is split into 8 groups `g = k/4` of 4 `k` each; byte `4*r + (k%4)` holds row `r`. | |
| - **Q2_K** (336 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q2_K`, lossless tiling with same size as `block_q2_K`): | |
| - `bytes 0..255` (vec 0..1): unsigned 2-bit quants; vec `m` holds groups `4m..4m+3` at bit offsets 0, 2, 4, 6. | |
| - `bytes 256..319`: raw `scales[]` bytes (`min << 4 | sc`): `[0..31]` for k 0..15, `[32..63]` for k 16..31. | |
| - `bytes 320..335`: 8 FP16 in tile `j` (0..7) of super-block: `d` (`j < 4`) or `-dmin` (`j >= 4`) for rows `8*(j%4)..8*(j%4)+7`. | |
| - **Q3_K** (512 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q3_K`): | |
| - `bytes 0..255` (vec 0..1): low 2 bits; vec `m` holds groups `4m..4m+3` at bit offsets 0, 2, 4, 6. | |
| - `bytes 256..383` (vec 2): sign mask, bit `g` set when `hmask` is 0 (quant = `low2 - 4`). | |
| - `bytes 384..511` (vec 3): FP16 scales `d * (scales[] - 32)`: `fp16[0..31]` for k 0..15, `fp16[32..63]` for k 16..31. | |
| - **Q4_K** (640 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q4_K`, same layout as Q4_1 tile; each 256-k super-block splits into 8 tiles of 32 k): | |
| - `bytes 0..511` (vec 0..3): 4-bit quants; byte `32*cp + r` holds `k = 2*cp` (low nibble) and `2*cp + 1` (high nibble). | |
| - `bytes 512..639` (vec 4): 32x2 FP16 per row: `(d * sc, -dmin * m)`. | |
| - **Q5_K** (768 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q5_K`): | |
| - `bytes 0..639` (vec 0..4): Q4_K tile (low 4 bits + FP16 scale/min pairs). | |
| - `bytes 640..767` (vec 5): 5th-bit plane; byte `l` holds the 8 high bits for lane `l` across vec `i = 0..3` (`bit 2i = lo, 2i+1 = hi`). | |
| - **Q6_K** (896 bytes per tile, `HTP_TENSOR_TILE_SIZE_Q6_K`): | |
| - `bytes 0..511` (vec 0..3): low 4 bits; vec `i` holds group `2i` (low nibble) and `2i+1` (high nibble). | |
| - `bytes 512..767` (vec 4..5): high 2 bits; vec `4+m` holds groups `4m..4m+3` at bit offsets 0, 2, 4, 6. | |
| - `bytes 768..895` (vec 6): FP16 scales `d * scales[]`: `fp16[0..31]` for k 0..15, `fp16[32..63]` for k 16..31. | |
| ## Op and Kernel Development Guidelines | |
| Writing high-performance operators for Hexagon requires following specific guidelines. | |
| ### DDR -> DMA -> VTCM Execution Pipeline | |
| - Strongly prefer the `DDR -> DMA -> VTCM -> compute (HVX/HMX) -> VTCM -> DMA -> DDR` data flow. | |
| - Direct HVX reads/writes from/to DDR are less efficient and should only be used as a fallback. | |
| - Use `dma_addr_t` only for DMA base and final addresses. Form a final address by adding a 32-bit byte offset to a | |
| `dma_addr_t` tensor base address. This permits a 64-bit mapped base address on newer platforms while retaining 32-bit | |
| relative addressing. | |
| - The DMA queue is a strict FIFO where operations must be pushed and popped in strict order. | |
| - Follow the pipelined multi-buffering sequence properly (typically 2x to 16x buffering) so every push has a corresponding pop: | |
| 1. In the prologue, push initial DDR -> VTCM transfers to prime the pipeline. | |
| 2. In the loop body, wait for buffer N via DMA pop, launch HVX/HMX compute on buffer N, push VTCM -> DDR writeback of result N, | |
| and push DDR -> VTCM prefetch of buffer N+2. | |
| 3. In the epilogue, pop all remaining in-flight transfers to drain the pipeline. | |
| - Because every push must be matched by a pop, `dma_queue_flush()` is not required when the pipeline sequence is followed | |
| properly. Flushing is only used in rare exceptions where a batch of operations is pushed without individual pops. | |
| - Use the DMA queue interface from [`dma-queue.h`](../../../ggml/src/ggml-hexagon/htp/dma-queue.h) | |
| (`dma_queue_push()`, `dma_queue_pop()`, and `dma_queue_flush()`). | |
| See [`cumsum-ops.c`](../../../ggml/src/ggml-hexagon/htp/cumsum-ops.c) and | |
| [`act-ops.c`](../../../ggml/src/ggml-hexagon/htp/act-ops.c) for reference implementations. | |
| ### Avoid Scalar Reads and Writes to VTCM | |
| - Access VTCM data using DMA transfers or HVX/HMX vector instructions rather than scalar reads and writes. | |
| ### Avoid Scalar Division in Inner Loops | |
| - Hexagon cores do not have hardware division instructions. | |
| - For recurring divisions across iterations or threads, use `fastdiv` from | |
| [`hex-fastdiv.h`](../../../ggml/src/ggml-hexagon/htp/hex-fastdiv.h) with precomputed divisors (such as | |
| `octx->ctx->mdev.count_div` or `octx->n_threads_div`). | |
| - Do not call `init_fastdiv_values()` for single-use divisions; use standard compiler division (`/`) instead. | |
| ### Host-Side Precomputation via `kernel_params` | |
| - Precompute tensor shapes, strides, scale conversions, tiling layouts, and validation checks on the host CPU during graph | |
| preparation in [`ggml-hexagon.cpp`](../../../ggml/src/ggml-hexagon/ggml-hexagon.cpp). | |
| - Pack precomputed parameters into the operator's fixed `kernel_params` structure in `htp_op_node` (such as | |
| `htp_mm_kernel_params`, `htp_unary_kernel_params`, `htp_fa_kernel_params`, `htp_get_rows_kernel_params`). | |
| - The NPU executes directly using `octx->kernel_params` without redundant runtime metadata extraction or validation. | |
| - **Strict Host-Kernel Alignment**: | |
| - Verify that parameters calculated by the host CPU are strictly honored by the NPU kernel. | |
| - Ensure the kernel does not ignore host-computed fields (for example, falling back to `octx->n_threads` instead of | |
| using `kparams->n_threads`, or ignoring precomputed `tasks_per_thread` and chunk counts). | |
| - Both human developers and coding agents must audit both sides of the interface: ensure fields populated in `kernel_params` | |
| in [`ggml-hexagon.cpp`](../../../ggml/src/ggml-hexagon/ggml-hexagon.cpp) are actively and consistently utilized by the | |
| corresponding operator entry point and worker threads in `htp/*-ops.c`. | |
| ### Tracing Instrumentation | |
| - All kernels must include trace events for performance profiling and timeline visualization in Perfetto | |
| ([`hex-profile.h`](../../../ggml/src/ggml-hexagon/htp/hex-profile.h)). | |
| - Surround compute sections with `htp_trace_event_start(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) info)` and | |
| `htp_trace_event_stop(tr, HTP_TRACE_EVT_HVX_COMP, (uint16_t) info)`. | |
| - Use specific event types for major phases: | |
| - `HTP_TRACE_EVT_HVX_COMP`: Vector compute execution. | |
| - `HTP_TRACE_EVT_DMA`: DMA transfer wait or poll cycles. | |
| - `HTP_TRACE_EVT_FENCE`: Multi-device fence barrier synchronization. | |
| - `HTP_TRACE_EVT_L2FLUSH`: L2 cache cleaning operations. | |
| - Pass meaningful progress metrics (such as row index, chunk index, or token index) in the 16-bit `info` parameter. | |
| ### Work Queue and Threading | |
| - Distribute parallel work across NPU worker threads using the thread pool work queue: | |
| ```c | |
| work_queue_run(ctx->work_queue, worker_func, &op_ctx, n_threads); | |
| ``` | |
| - Keep worker functions independent and re-entrant. Worker threads should only operate on their designated chunk of rows or elements. | |
| ### Avoid Redundant Defensive NULL Checks | |
| - Do not add defensive NULL checks or assertions for internal framework pointers or required graph operands and outputs. | |
| Internal pointers include `ctx`, `octx`, local context structs like `*ctx`, `kparams`, and worker callback `data`. | |
| - These pointers are architectural invariants during kernel execution and host-side graph preparation. | |
| - Do not turn an invariant violation into an unsupported operation or missed fusion. | |
| Checks such as `if (!octx || !octx->ctx)` clutter the code, obscure intent, and hide upstream errors. | |
| - **Distinction**: `octx->src[N]` pointers *can* be NULL by design and must be checked when optional. | |
| Examples include attention masks, optional bias or weights in fused kernels, and frequency factors. | |
| ### `supports_op` Independence from Buffer and Repack State | |
| - `supports_op` (and op-specific checkers such as `supported_mul_mat()`, `supported_get_rows()`) runs during GGML graph scheduling **before** buffers are allocated and before tensor data is populated. | |
| - **Never check buffer usage or repacking state in `supports_op`**: | |
| - `t->buffer` may be NULL or uninitialized. | |
| - `t->data` may be NULL. | |
| - `buffer->usage` is unknown at scheduling time. | |
| - `is_tiled` or `extra->flags & GGML_HEXAGON_TENSOR_TILED` is not set yet. | |
| - Operator support decisions must depend strictly on static tensor properties: operand types (`t->type`), shapes (`t->ne`), strides (`t->nb`), and operation parameters. | |
| - Repacking (converting weights to tiled layout) is an internal optimization applied during `set_tensor()` or rt-repack. Non-weight tensors or test harness tensors with the same type remain in flat layout. | |
| - Therefore, any supported type (such as BF16, F16, quantized types) must be supported both when repacked into tiles and when stored flat in memory. | |
| ### Multiline Macro Formatting | |
| - Keep trailing backslashes in multiline `#define` macros cleanly aligned to a consistent column. | |
| - Avoid trailing whitespace after macro backslashes. | |
| - Use [`scripts/snapdragon/ggml-hexagon-align-macros.py`](../../../scripts/snapdragon/ggml-hexagon-align-macros.py) to inspect, diff, | |
| or automatically align macro definitions across Hexagon kernel sources: | |
| ```bash | |
| # Check for misaligned macros | |
| python3 scripts/snapdragon/ggml-hexagon-align-macros.py ggml/src/ggml-hexagon/htp/ | |
| # Fix misaligned macros in-place | |
| python3 scripts/snapdragon/ggml-hexagon-align-macros.py --fix ggml/src/ggml-hexagon/htp/ | |
| ``` | |
| ### Binary Inspection and Spill Analysis | |
| Use [`scripts/snapdragon/ggml-hexagon-inspect.py`](../../../scripts/snapdragon/ggml-hexagon-inspect.py) to audit Hexagon binaries for register | |
| spills, unexpected float promotions, or disassembly: | |
| - Always verify that compute kernels have zero in-loop vector spills (`--spills --strict`) and no float promotions (`--promotions`). | |
| - Avoid excessive loop unrolling (`#pragma unroll`), which increases register pressure and causes spills. | |
| ```bash | |
| # Check for vector and scalar register spills | |
| python3 scripts/snapdragon/ggml-hexagon-inspect.py --spills --strict --func "^compute_" | |
| # Check for float promotions | |
| python3 scripts/snapdragon/ggml-hexagon-inspect.py --promotions --func "^compute_" | |
| # Disassemble with annotated loops and spill markers | |
| python3 scripts/snapdragon/ggml-hexagon-inspect.py --disasm compute_same_shape_div_f32 | |
| # Resolve crash addresses to function symbols and lines | |
| python3 scripts/snapdragon/ggml-hexagon-inspect.py --addr2line 0x51a30 0x5ba54 | |
| ``` | |
| ## Multi-Device Partitioning (mdev) | |
| Multi-device (mdev) mode enables row-level tensor parallel execution across multiple physical NPU cores or virtual NPU | |
| sessions. | |
| ### 128-Byte Cache Line Alignment | |
| - Shared tensor buffers reside in DDR (RPCMEM) with a 128-byte cache line granularity | |
| (`HEX_L2_LINE_SIZE` = 128 bytes, `HTP_TENSOR_MDEV_LINE_SIZE`). | |
| - **Rule**: Multi-device work partitions that write directly to DDR through HVX/L2 must align destination write regions to | |
| 128-byte cache line boundaries so distinct devices never share or overwrite the same cache line. | |
| - DMA writes to DDR are not subject to this cache-line ownership rule. They may use smaller non-overlapping destination | |
| ranges when the operator only writes through DMA. | |
| ### Partitioning Helpers in `htp-tensor.h` | |
| Common partitioning logic is factored into reusable inline helpers in | |
| [`htp-tensor.h`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h): | |
| 1. [`htp_tensor_mdev_rows_per_chunk`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L104): | |
| Determines the minimum number of rows per chunk so that the chunk byte size is a multiple of 128 bytes: | |
| ``` | |
| rows_per_chunk = 128 / hex_gcd_u32(row_size, 128) | |
| ``` | |
| If the active row and outer strides are already multiples of 128 bytes, `rows_per_chunk = 1`. | |
| Returns `false` if the tensor cannot be safely row-partitioned (such as unaligned base pointer, permuted layout, | |
| or non-128-byte aligned outer strides). | |
| 2. [`htp_tensor_mdev_partition`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L131): | |
| Calculates the per-device work range `struct htp_tensor_mdev_range { uint32_t start; uint32_t count; }` given | |
| `total_units`, `units_per_chunk`, `mdev_idx`, `mdev_count`, and the precomputed `mdev_count_div`. | |
| Handles chunk distribution across devices, assigns remainder units to the last device, and automatically triggers | |
| single-device fallback when partitioning is unsafe. | |
| ### Row-Partitioned Operators | |
| For row-wise operators that write directly to DDR | |
| (such as activations in [`act-ops.c`](../../../ggml/src/ggml-hexagon/htp/act-ops.c), | |
| binary ops in [`binary-ops.c`](../../../ggml/src/ggml-hexagon/htp/binary-ops.c), | |
| and unary ops in [`unary-ops.c`](../../../ggml/src/ggml-hexagon/htp/unary-ops.c)): | |
| ```c | |
| const uint32_t total_rows = ne01 * ne02 * ne03; | |
| const size_t dst_row_size = dst->ne[0] * elem_size; | |
| uint32_t row_start = 0; | |
| uint32_t nrows = total_rows; | |
| if (octx->ctx->mdev.count > 1) { | |
| uint32_t rows_per_chunk = 0; | |
| htp_tensor_mdev_rows_per_chunk(dst, elem_size, (uint32_t) dst_row_size, &rows_per_chunk); | |
| const struct htp_tensor_mdev_range range = htp_tensor_mdev_partition( | |
| total_rows, rows_per_chunk, octx->ctx->mdev.idx, octx->ctx->mdev.count, &octx->ctx->mdev.count_div); | |
| row_start = range.start; | |
| nrows = range.count; | |
| } | |
| if (nrows == 0) { | |
| return HTP_STATUS_OK; | |
| } | |
| ``` | |
| ### Element-Partitioned Operators | |
| For flat element-wise operations that write directly to DDR: | |
| - Partition total linear elements N = ne0 * ne1 * ne2 * ne3 in 128-byte cache line chunks (`elems_per_line = (elem_size == 4) ? 32 : 64`). | |
| - Requires strict 1D contiguity: | |
| [`htp_tensor_is_contiguous(dst, elem_size)`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L65) | |
| and 128-byte aligned destination pointer | |
| [`htp_tensor_mdev_data_aligned(dst)`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L84). | |
| - If contiguous and aligned, pass `elems_per_line` to | |
| [`htp_tensor_mdev_partition`](../../../ggml/src/ggml-hexagon/htp/htp-tensor.h#L131); | |
| otherwise pass 0 to trigger Device 0 fallback. | |
| ### Single-Device Fallback (Device 0) | |
| - Fallback to Device 0 (`mdev.idx == 0`) when partitioning would cause cache line tearing or there are too few aligned chunks. | |
| - Triggers: | |
| 1. Destination tensor cannot be safely partitioned (`rows_per_chunk == 0` or non-contiguous/unaligned buffer). | |
| 2. Total aligned chunks < `mdev_count`. | |
| - Device 0 processes the entire tensor `[0, total_units)`. | |
| - Devices 1 ... N-1 receive `count = 0` and return `HTP_STATUS_OK` immediately. | |
| ### Flatten Outer Dimensions Globally | |
| - **Never partition solely on `ne01` (dimension 1).** | |
| - Partitioning only on `ne01` repeats the device boundary across every 2D slice (`ne02`, `ne03`). If each 2D slice is small, | |
| false sharing occurs repeatedly throughout the tensor. | |
| - Always flatten outer dimensions globally: `total_rows = ne01 * ne02 * ne03` and partition once across the combined row space. | |
| ### Stateless Starting Coordinates | |
| - Do not use incremental state variables across slices that assume the thread or device starts at index 0. | |
| - Precompute starting multidimensional coordinates at `r = row_start` (or `e = elem_start`) once using `fastdiv`. | |
| - In inner loops, step base pointers directly (`ptr += stride`) or reset/wrap coordinates explicitly (`if (++i01 == ne01) { ... }`). | |
| ### Clean Range Encapsulation | |
| - Initialize single-device default ranges at declaration: | |
| ```c | |
| uint32_t row_start = 0; | |
| uint32_t nrows = total_rows; | |
| ``` | |
| - Encapsulate all multi-device logic inside `if (octx->ctx->mdev.count > 1)`. If the block is omitted or compiled out, | |
| the operator runs standard single-device execution untouched. | |
| - Do not propagate `mdev_` prefixes to worker functions or context structs. Worker threads are device-agnostic and | |
| should only receive standard range parameters (`ctx.row_start`, `ctx.nrows`). | |
| - In worker threads, calculate row intervals using standard arithmetic: | |
| ```c | |
| const uint32_t ir0 = ctx->row_start + dr * ith; | |
| const uint32_t ir1 = MIN(ir0 + dr, ctx->row_start + ctx->nrows); | |
| ``` | |
| In single-device mode (`row_start == 0`), this naturally simplifies to `dr * ith` and `MIN(ir0 + dr, ctx->nrows)` with zero overhead. | |
| ## Multi-Device Synchronization | |
| Multi-device execution synchronizes worker sessions across devices using explicit barriers and tensor cache flushing. | |
| ### Synchronization Fence Protocol | |
| Multi-device execution synchronizes worker sessions through atomic fence slots and barriers defined in | |
| [`htp-fence.h`](../../../ggml/src/ggml-hexagon/htp/htp-fence.h): | |
| ``` | |
| [NPU Session 0] [NPU Session 1] | |
| | | | |
| (Input Prep) (Input Prep) | |
| | | | |
| Pre-Op Barrier ----------------------------- Pre-Op Barrier | |
| (htp_mdev_group_barrier) (htp_mdev_group_barrier) | |
| | | | |
| Kernel Execution Kernel Execution | |
| (Output Slice 0) (Output Slice 1) | |
| | | | |
| Tensor Cache Flush Tensor Cache Flush | |
| (htp_dcache_flush_dirty_tensors) (htp_dcache_flush_dirty_tensors) | |
| | | | |
| Post-Op/Batch Barrier ---------------------- Post-Op/Batch Barrier | |
| (htp_mdev_group_barrier) (htp_mdev_group_barrier) | |
| | | | |
| Return Response to Host Return Response to Host | |
| ``` | |
| ### Atomic Fence Slots and Cache Invalidation | |
| - Fence synchronization operates on dedicated RPCMEM shared memory mapped across all participating sessions (`ctx->mdev.fence_base`). | |
| - Each device owns a dedicated 128-byte cache-line aligned fence slot: | |
| ```c | |
| atomic_uint * my_fence = htp_mdev_fence_slot(fence_base, mdev_idx); | |
| ``` | |
| - **Writing to fence ([`htp_fence_write`](../../../ggml/src/ggml-hexagon/htp/htp-fence.h#L17))**: | |
| Stores `seq` and `status`, issues a `syncht` thread synchronization barrier, and flushes/invalidates the line | |
| using `Q6_dccleaninva_A(fence)`. | |
| - **Reading from peer fence ([`htp_fence_read`](../../../ggml/src/ggml-hexagon/htp/htp-fence.h#L25))**: | |
| Executes `Q6_dccleaninva_A(fence)` and `syncht` before reading atomic values to ensure fresh data from DDR. | |
| ### Deterministic Monotonic Sequence Numbers | |
| - Barrier fences use monotonically increasing sequence numbers: | |
| ```c | |
| const uint32_t seq = ++ctx->mdev.fence_seq; | |
| ``` | |
| - Comparing sequence numbers with signed arithmetic `(int32_t)(peer_seq - seq) >= 0` prevents race conditions or | |
| misaligned barrier arrivals across iterations. | |
| - If any peer reports an error status (`peer_status > HTP_STATUS_OK`), the barrier propagates the error and unblocks immediately. | |
| ### Tensor Cache Flush and Pipeline Completion | |
| - In the kernel, ensure all pushed DMA operations have been popped in strict FIFO order to drain the queue. | |
| - Use [`htp_dcache_flush_dirty_tensors()`](../../../ggml/src/ggml-hexagon/htp/htp-dcache.h) to flush specific dirty tensors back to DDR: | |
| - [`htp_dcache_flush_dirty_tensors()`](../../../ggml/src/ggml-hexagon/htp/htp-dcache.h) flushes modified tensor address ranges, or the | |
| full D-cache when their total size exceeds the flush threshold, ensuring peer devices and the host CPU observe consistent | |
| data in DDR. | |
| - Never signal completion before all DMA transfers are drained and dirty tensor flushes have completed. | |
Xet Storage Details
- Size:
- 29.5 kB
- Xet hash:
- cadd8dbdb09f49e1027f81d6647d4b115d9486bfa2e5bd9d6d52a92d0927b02b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.