Hardware Pipelines & Hybrid Quantization are implemented

This commit is contained in:
Invisi
2026-03-22 23:36:58 +07:00
parent 5bfeb50726
commit 14d39ea9ce
5 changed files with 685 additions and 497 deletions
+52
View File
@@ -0,0 +1,52 @@
# Contributing to rk-llama.cpp
This document outlines the project's structure, current development priorities, and workflow guidelines.
## Project Structure
All backend-specific code is contained within the `ggml/src/ggml-rknpu2` directory. Here is a brief overview of the key files and their responsibilities:
* **`ggml-rknpu2.cpp`**
The core GGML backend implementation. It bridges the GGML framework with the Rockchip NPU.
* `ggml_backend_rknpu_buffer_type_alloc_buffer`: Handles the allocation of DMA-Heap memory blocks.
* `ggml_backend_rknpu_buffer_set_tensor`: Dequantizes incoming GGUF weights, requantizes them to the target NPU format, packs them into native RKNN layout, and writes them to the DMA buffer.
* `ggml_backend_rknpu_graph_compute`: Orchestrates the matrix multiplication, assigns workload segments to different NPU cores, and handles activation transformations.
* **`rknpu2-configuration.cpp`**
Defines chip-specific configurations, available hardware pipelines (data types, alignment rules, packing functions), and manages the hybrid quantization patterns.
* **`rknpu2-quantization.cpp`**
Contains mathematical utilities for scaling and symmetric quantization from FP32 to target NPU integer formats (INT8, INT4), as well as dequantization routines.
* **`rknpu2-calibration.cpp`**
Provides statistical methods for finding optimal quantization parameters (e.g., KL-Divergence, Min-MSE) and matrix transformations (like the Fast Walsh-Hadamard Transform).
* **`rknpu2-allocation.cpp`**
A lightweight wrapper for allocating and freeing physically contiguous, zero-copy memory via the Linux DMA-Heap subsystem.
## Future Directions
This section outlines the roadmap for future contributions and improvements.
### Ongoing Needs
Contributions in these areas are always welcome:
* **Bug Fixes:** Any reproducible issues confirmed by the community.
* **Optimizations:** Improvements to calculation accuracy, memory usage reduction, or inference speedups with experimental benchmark data.
### Hot Topics
Here are several "cutting-edge" ideas and active development goals:
* **Support for Other Chipsets:** Expanding configurations in `rknpu2-configuration.cpp` to support other Rockchip SoCs, including RISC-V variants.
* **Advanced Low-Bit Optimizations:** Currently, pure `INT4_STANDARD` produces garbage output. The Hadamard Transform solves the accuracy issue but introduces significant $O(K \log K)$ CPU overhead. Architectural optimizations, faster math routines or advanced algorithms are welcomed to solve the problem.
* **Split Quantization:** Implementing a system where a single weight matrix is split into two: a sparse matrix containing outliers (computed in high-bit pipeline) and a dense matrix for the rest (computed in low-bit pipeline). Orchestrating this efficiently on the NPU is a major milestone.
* **Smart Hybrid Quantization:** Currently, hybrid quantization applies a cyclical pattern across layers. Adding support for "smart" quantization-such as using Regex patterns to target specific, sensitive neural network layers with high-bit pipelines-would vastly improve the performance/accuracy ratio.
## Development Workflow
**Important:** This fork undergoes frequent `rebase` and `push --force` operations against the upstream `llama.cpp` repository to ensure compatibility with the newest architectural updates from the community.
To prevent merge conflicts:
* If you are working on a feature, **please open a Draft Pull Request as early as possible.** This avoids breaking your work during an upstream sync.
* Ensure your code matches the existing C/C++ style of the project.
* Keep your commits clean and descriptive.
+102 -63
View File
@@ -26,78 +26,19 @@ cd rk-llama.cpp
```sh
mkdir build && cd build
cmake .. -DLLAMA_RKNPU2=ON
make -j8
make -j4
```
3. Run inference
```sh
# For Dense models
./build/bin/llama-cli -m ~/Projects/gemma-3-1b-it-Q8_0.gguf
./build/bin/llama-cli -m ./gemma-3-1b-it-Q8_0.gguf
# For MoE models
./build/bin/llama-cli -m ~/Projects/LFM2-8B-A1B-Q4_0.gguf --cpu-moe
./build/bin/llama-cli -m ./LFM2-8B-A1B-Q4_0.gguf --cpu-moe
```
## Quantizations
### Weights
Weights are converted based on the input type. Implemented types:
`FP16`
Input **F16** weights are directly used in native **FP16** format.
`INT8`
Input **Q8_0** weights are dequantized to FP32, then re-quantized to a uniform per-tensor **INT8** format.
`INT4`
Input **Q4_0** weights are dequantized, rotated using a randomized Hadamard transform (see [2404.00456](https://arxiv.org/abs/2404.00456)), calibrated using a KL-Divergence (see [2411.02530](https://arxiv.org/abs/2411.02530)), and then re-quantized to per-tensor **INT4**.
### Activations
Activations are converted based on the operation type. Implemented types:
`FP16`
Input **F32** activations are converted to **FP16** format.
`INT8`
Input **F32** activations are quantized to **INT8** using per-channel scaling.
`INT4`
Input **F32** activations are rotated using a Hadamard transform and then quantized to **INT4** using per-channel scaling.
### Results
Results (of a matrix multiplication) are converted based on the output type. Implemented types:
`FP32`
**FP32** results from the NPU are already in **F32** and used directly.
`INT32`
**INT32** results from the NPU are dequantized to **F32** using combined weight and activation scales.
`INT16`
**INT16** results from the NPU are dequantized to **F32** with an additional normalization factor for rotated computations.
## Chipsets
### RK3588
The backend supports the following computation types:
* **W16A16**: FP16 weights & FP16 activations
* **W8A8**: INT8 weights & INT8 activations
* **W4A4**: INT4 weights & INT4 activations
## Benchmarks
The following benchmarks were conducted on an RK3588, comparing the performance, accuracy, and power consumption of the NPU backend against the standard CPU (NEON) backend.
@@ -108,21 +49,119 @@ The following benchmarks were conducted on an RK3588, comparing the performance,
| | | NPU | 🟢 20.74±0.74 | 🟢 432.3±0.6 | 🟡 20.2±0.4 | 🟢 3.2±0.2 |
| | Q8_0 | CPU | 🟢 20.71±0.74 | 🔴 163.4±0.2 | 🟢 40.6±0.1 | 🔴 6.4±0.4 |
| | | NPU | 🟡 22.68±0.82 | 🟢 311.8±2.1 | 🔴 25.4±0.4 | 🟢 3.6±0.2 |
| | Q6_K | CPU | 🟢 21.55±0.77 | 🔴 78.7±0.1 | 🟢 42.5±0.1 | 🔴 6.8±0.4 |
| | | NPU | 🔴 29.52±1.07 | 🟢 142.4±1.3 | 🔴 25.8±1.2 | 🟢 3.8±0.2 |
| | Q4_0 | CPU | 🟢 24.46±0.88 | 🟢 340.4±0.9 | 🟢 55.2±0.1 | 🔴 6.2±0.4 |
| | | NPU | 🔴 74.09±2.87 | 🔴 163.6±0.2 | 🔴 26.7±0.5 | 🟢 4.0±0.2 |
| **Gemma3 1B** | F16 | CPU | 🟢 26.20±1.08 | 🔴 68.5±0.1 | 🟢 11.1±0.1 | 🔴 6.8±0.4 |
| | | NPU | 🟢 26.18±1.07 | 🟢 249.6±0.2 | 🟢 10.8±0.2 | 🟢 2.8±0.2 |
| | Q8_0 | CPU | 🟢 26.08±1.07 | 🔴 73.3±0.1 | 🟢 19.5±0.1 | 🔴 7.4±0.4 |
| | | NPU | 🟡 29.15±1.22 | 🟢 378.6±0.4 | 🟡 16.5±0.3 | 🟢 3.0±0.2 |
| | Q6_K | CPU | 🟢 25.94±1.06 | 🔴 51.9±0.1 | 🟢 18.7±0.1 | 🔴 7.2±0.4 |
| | | NPU | 🟡 38.46±1.68 | 🟢 209.4±3.2 | 🟡 16.6±0.2 | 🟢 3.0±0.2 |
| | Q4_0 | CPU | 🟢 30.77±1.31 | 🟢 164.7±0.2 | 🟢 28.3±0.1 | 🔴 7.0±0.4 |
| | | NPU | 🔴 55.53±2.30 | 🔴 51.4±0.1 | 🔴 16.7±0.3 | 🟢 3.0±0.2 |
| **LFM2 8B A1B** | F16 | CPU | 🟢 15.79±0.58 | 🟡 31.1±2.9 | 🟢 6.8±0.2 | 🟡 7.0±0.6 |
| | | NPU | 🟢 15.82±0.58 | 🟢 38.3±3.2 | 🟢 6.3±0.4 | 🟢 5.8±0.4 |
| | Q8_0 | CPU | 🟢 15.92±0.59 | 🟡 31.7±0.1 | 🟢 12.9±0.1 | 🟡 7.4±0.6 |
| | | NPU | 🟡 16.76±0.62 | 🟢 40.7±0.7 | 🟢 12.5±0.3 | 🟢 5.8±0.4 |
| | Q6_K | CPU | 🟢 15.91±0.58 | 🟡 16.4±0.1 | 🟡 12.9±0.1 | 🟡 7.4±0.6 |
| | | NPU | 🟡 21.16±0.83 | 🟢 21.4±0.1 | 🟢 13.7±0.1 | 🟢 5.8±0.4 |
| | Q4_0 | CPU | 🟢 18.24±0.53 | 🟢 62.2±0.1 | 🟢 22.7±0.1 | 🟡 7.4±0.6 |
| | | NPU | 🟡 26.09±1.06 | 🟡 47.5±0.1 | 🟢 19.0±0.1 | 🟢 5.8±0.4 |
**Legend**: 🟢 Excellent | 🟡 Acceptable | 🔴 Poor
### Methodology
* **Perplexity:** Evaluates accuracy. Measured over 32 chunks of 512 tokens using `wiki.test.raw`. Lower is better.
```sh
taskset -c 4-7 ./build/bin/llama-perplexity -m ./model.gguf -f ./wiki.test.raw -t 4 -b 512 --chunks 32
```
* **PP / TG (tok/s):** Prompt Processing and Text Generation speeds. Evaluated using standard pp512 and tg128. Higher is better.
```sh
taskset -c 4-7 ./build/bin/llama-bench -m ./model.gguf -t 4
```
* **Power (W):** Represents relative active power consumption. Calculated as `Power(TextGeneration) - Power(Idle)`. Lower is better.
## Hybrid Quantization
Hybrid quantization is a technique designed to strike an optimal balance between memory consumption, inference speed, and generation quality.
Instead of forcing the entire model into a single format, the backend processes the model layer by layer, cyclically applying a predefined sequence of hardware pipelines. This approach is entirely transparent and works with any input GGUF weight type. You can find the default pipeline sequences for each chip in the **Chipsets** section below.
### Custom Patterns
Building on this layer-by-layer approach, you can easily override the default behavior to experiment with your own quantization strategies.
By setting the `HYBRID_PATTERN` environment variable, you define a custom sequence of hardware pipelines. The backend will cyclically iterate through this sequence as it loads the model's layers. For example, if you specify two pipelines, Layer 1 will use the first, Layer 2 will use the second, Layer 3 will revert to the first, and so on.
```sh
# Alternates model layers between FP16, INT8 and INT4 NPU pipelines
# This pipeline results in (16 + 8 + 4) / 3 ≈ 9.3 BPW
HYBRID_PATTERN="FP16_STANDARD,INT8_STANDARD,INT4_HADAMARD" ./build/bin/llama-cli -m ./model.gguf
# When the backend encounters a wrong pipeline name, it offloads layer to the default CPU backend
# This pipeline will offload to the NPU around half of the model's layers
HYBRID_PATTERN="CPU_STANDARD,INT8_STANDARD" ./build/bin/llama-cli -m ./model.gguf
```
### Weights Requantizations
It is important to understand how weights are loaded into the NPU. The backend does not execute GGUF formats (like `Q4_0` or `Q8_0`) natively. Instead, input weights are first dequantized to `FP32` on the CPU, calibrated, and then requantized into the NPU's native hardware formats.
Because of this double conversion process, you must carefully pair your input GGUF model with your target NPU pipelines:
1. **Ideal Case (GGUF Precision > NPU Precision)**<br>
Using an `F16` or `Q6_K` model to run `INT8` or `INT4` NPU pipelines. The NPU quantization algorithm receives highly accurate FP32 reference data, allowing it to calculate optimal scales and minimize the final quantization error.
2. **Recommended Case (GGUF Precision ≈ NPU Precision)**<br>
Using `Q8_0` for an `INT8` pipeline, or `Q4_0` for an `INT4` pipeline. The precision levels match closely, keeping the initial GGUF file size small on disk while minimizing further information loss during NPU requantization.
3. **Terrible Case (GGUF Precision < NPU Precision)**<br>
Upscaling a `Q4_0` model to run on an `FP16` or `INT8` NPU pipeline. Information was irreversibly lost when the model was originally compressed to `Q4_0`. Requantizing it to a higher bit-depth cannot restore the lost accuracy, meaning you will get the poor quality of a 4-bit model while wasting the memory bandwidth and compute resources of an 8-bit or 16-bit model.
## Chipsets
The backend configures operations based on hardware pipelines-specific hardware-accelerated paths mapping mathematical operations to native NPU types. Each supported chipset defines its own set of pipelines and default quantization behaviors.
### RK3588
#### Available Pipelines
Below is a comparison of all available hardware pipelines on the RK3588.
The **Perplexity** metrics were measured on the `Granite-4.0-350M-F16` model to isolate the accuracy impact of the NPU's internal quantization algorithms.
| Name | Operation | Perplexity | Notes |
| :--- | :--- | :--- | :--- |
| `FP16_HADAMARD` | FP16xFP16 | 20.74 ± 0.74 | Hadamard Transform\* |
| `FP16_STANDARD` | FP16xFP16 | 20.74 ± 0.74 | - |
| `INT8_HADAMARD` | INT8xINT8 | 20.85 ± 0.74 | Hadamard Transform\* |
| `INT8_STANDARD` | INT8xINT8 | 22.91 ± 0.83 | - |
| `INT4_HADAMARD` | INT4xINT4 | 109.15 ± 4.46 | Hadamard\*, KL-Div\*\* |
| `INT4_STANDARD` | INT4xINT4 | 240048.97 ± 9261.03 | KL-Divergence\*\* |
\* **Hadamard Transform:** Applies a randomized Fast Walsh-Hadamard Transform to smooth out activation outliers before quantization (see [2404.00456](https://arxiv.org/abs/2404.00456)).<br>
\*\* **KL-Divergence:** Uses entropy-based calibration to find the optimal scaling factor for 4-bit weights by minimizing information loss (see [2411.02530](https://arxiv.org/abs/2411.02530)).
> **Note:** As seen in the table, pure `INT4_STANDARD` produces completely broken outputs (extremely high perplexity). This is due to massive outliers being heavily clipped in the limited 4-bit range. The `INT4_HADAMARD` pipeline mitigates this by mathematically distributing the outliers across all channels, making 4-bit inference actually usable, though it introduces some CPU overhead.
#### Default Mappings
If no custom `HYBRID_PATTERN` is provided, the RK3588 backend will automatically map your input GGUF model types to the following default NPU pipelines to provide the best out-of-the-box balance of speed and accuracy:
| Input Weight Type | Default Hardware Pipeline | Bits Per Weight |
| :--- | :--- | :--- |
| `F16` | [`FP16_STANDARD`] | 16 |
| `Q8_0` | [`INT8_STANDARD`] | 8 |
| `Q6_K` | [`INT8_STANDARD`, `INT4_HADAMARD`] | 6 |
| `Q4_0` | [`INT4_HADAMARD`] | 4 |
## Contributing
Feel free to open an issue to discuss a bug or feature, or submit a pull request with your improvements.
Feel free to open an issue to discuss a bug or feature, or submit a pull request with your improvements. Please refer to CONTRIBUTING.md for a breakdown of the project structure, current development goals, and workflow guidelines.
+341 -384
View File
@@ -35,7 +35,7 @@
do { \
int ret = (stmt); \
if (ret < 0) { \
fprintf(stderr,"RKNN error %d at %s:%d: %s\n", ret, \
fprintf(stderr,"RKNN error %d at %s:%d: %s\n", ret, \
__FILE__, __LINE__, msg); \
assert(false); \
} \
@@ -163,7 +163,7 @@ struct ggml_backend_rknpu_context {
// A- and C-matrices cache (from create_mem)
std::unordered_map<std::tuple<int, int, int>, std::shared_ptr<rknn_tensor_mem>, TupleHasher> a_buffer_cache;
std::unordered_map<std::tuple<int, int, int>, std::shared_ptr<rknn_tensor_mem>, TupleHasher> c_buffer_cache;
std::unordered_map<std::tuple<int, int, int, int>, std::shared_ptr<rknn_tensor_mem>, TupleHasher> c_buffer_cache;
std::shared_ptr<rknpu_matmul_context> get_matmul_ctx(int M, int K, int N, int core_id, rknn_matmul_type type) {
std::lock_guard<std::mutex> lock(mutex);
@@ -247,17 +247,20 @@ static void ggml_backend_rknpu_free(ggml_backend_t backend) {
}
// Function for getting buffer from cache or creating new one
template <typename CacheKeyType>
static std::shared_ptr<rknn_tensor_mem> get_or_create_npu_buffer(
ggml_backend_rknpu_context* backend_ctx,
rknn_matmul_ctx matmul_ctx,
size_t size,
const std::tuple<int, int, int>& key,
std::unordered_map<std::tuple<int, int, int>, std::shared_ptr<rknn_tensor_mem>, TupleHasher>& cache
const CacheKeyType& key,
std::unordered_map<CacheKeyType, std::shared_ptr<rknn_tensor_mem>, TupleHasher>& cache
) {
std::lock_guard<std::mutex> lock(backend_ctx->mutex);
auto it = cache.find(key);
if (it != cache.end()) {
return it->second;
if (it->second->size >= size) {
return it->second;
}
}
rknn_tensor_mem* mem = rknn_create_mem(matmul_ctx, size);
@@ -277,36 +280,35 @@ static std::shared_ptr<rknn_tensor_mem> get_or_create_npu_buffer(
static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend, struct ggml_cgraph* cgraph) {
auto* backend_ctx = (ggml_backend_rknpu_context*)backend->context;
// Getting the current device configuration once
const auto& config = rknpu2_configuration::Rknpu2ConfigManager::get_instance().get_current_config();
for (int i = 0; i < cgraph->n_nodes; i++) {
struct ggml_tensor* node = cgraph->nodes[i];
for (int node_i = 0; node_i < cgraph->n_nodes; node_i++) {
struct ggml_tensor* node = cgraph->nodes[node_i];
if (node->op != GGML_OP_MUL_MAT) continue;
const struct ggml_tensor* src0 = node->src[0]; // Weights : (K x N)
const struct ggml_tensor* src1 = node->src[1]; // Activations : (M x K)
struct ggml_tensor* dst = node;
const ggml_type w_type = src0->type;
const int M = (int)src1->ne[1];
const int K = (int)src0->ne[0];
const int N = (int)src0->ne[1];
// Skip zero-dimension matmuls
// Skipping zero-dimension matmuls
if (M == 0 || K == 0 || N == 0) {
continue;
}
const bool is_q4_hadamard = (src0->type == GGML_TYPE_Q4_0);
const int K_op = is_q4_hadamard ? rknpu2_calibration::next_power_of_two(K) : K;
const auto* pipeline = config.resolve_op_support(src0);
if (!pipeline) continue;
const auto* op_support = config.find_op_support(src0->type);
if (!op_support) return GGML_STATUS_FAILED;
const bool is_hadamard = (pipeline->use_hadamard);
const int K_op = is_hadamard ? rknpu2_calibration::next_power_of_two(K) : K;
const int alignment = op_support->n_align;
const rknn_matmul_type matmul_type = pipeline->mm_type;
const int alignment = pipeline->n_align;
auto all_segments = compute_matrix_segments(N, config.core_count, alignment);
@@ -328,18 +330,14 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
// ===========================================
// ========== 1. Preparing contexts ==========
// ===========================================
for (size_t i = 0; i < num_active_segments; ++i) {
const auto& seg = active_segments[i];
matmul_ctxs[i] = backend_ctx->get_matmul_ctx(M, K_op, seg.size_n, seg.core_id, op_support->mm_type);
if (!matmul_ctxs[i] || matmul_ctxs[i]->ctx == 0) return GGML_STATUS_FAILED;
// Skip RKNN matmuls with zero segments
if (matmul_ctxs[i]->info.M == 0 || matmul_ctxs[i]->info.K == 0 || matmul_ctxs[i]->info.N == 0) {
matmul_ctxs[i] = nullptr;
{
for (size_t idx = 0; idx < num_active_segments; ++idx) {
const auto& seg = active_segments[idx];
matmul_ctxs[idx] = backend_ctx->get_matmul_ctx(M, K_op, seg.size_n, seg.core_id, matmul_type);
if (!matmul_ctxs[idx] || matmul_ctxs[idx]->ctx == 0) return GGML_STATUS_FAILED;
}
}
// ===========================================
// ========== 2. Preparing B-matrix ==========
// ===========================================
@@ -348,33 +346,38 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
auto* src0_buf_ctx = (ggml_backend_rknpu_buffer_context*)src0_buffer->context;
size_t src0_base_offset_in_dma = (uintptr_t)src0->data - (uintptr_t)ggml_backend_buffer_get_base(src0_buffer);
size_t type_size_packed;
if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_FP16) type_size_packed = 2;
else if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT8) type_size_packed = 1;
else type_size_packed = 0;
size_t current_offset_in_tensor = 0;
for (size_t i = 0; i < num_active_segments; ++i) {
// const auto& seg = active_segments[i];
size_t segment_size_bytes = matmul_ctxs[i]->io_attr.B.size;
size_t total_offset = src0_base_offset_in_dma + current_offset_in_tensor;
for (const auto& seg : all_segments) {
for (size_t idx = 0; idx < num_active_segments; ++idx) {
if (active_segments[idx].offset_n == seg.offset_n) {
auto& matmul_ctx = matmul_ctxs[idx];
size_t segment_size_bytes = matmul_ctx->io_attr.B.size;
size_t total_offset = src0_base_offset_in_dma + current_offset_in_tensor;
auto cache_key = std::make_pair(src0_buffer, total_offset);
std::lock_guard<std::mutex> lock(backend_ctx->mutex);
auto it = backend_ctx->b_mem_handle_cache.find(cache_key);
auto cache_key = std::make_pair(src0_buffer, total_offset);
{
std::lock_guard<std::mutex> lock(backend_ctx->mutex);
auto it = backend_ctx->b_mem_handle_cache.find(cache_key);
if (it != backend_ctx->b_mem_handle_cache.end()) {
mem_B_segments[i] = it->second;
} else {
rknn_tensor_mem* mem = rknn_create_mem_from_fd(
matmul_ctxs[i]->ctx, src0_buf_ctx->dma_buf.fd,
src0_buf_ctx->dma_buf.virt_addr, segment_size_bytes, total_offset
);
if (!mem) return GGML_STATUS_FAILED;
mem_B_segments[i] = std::shared_ptr<rknn_tensor_mem>(mem, [ctx=matmul_ctxs[i]->ctx](rknn_tensor_mem* m){ if (m) rknn_destroy_mem(ctx,m); });
backend_ctx->b_mem_handle_cache[cache_key] = mem_B_segments[i];
if (it != backend_ctx->b_mem_handle_cache.end()) {
mem_B_segments[idx] = it->second;
} else {
rknn_tensor_mem* mem = rknn_create_mem_from_fd(matmul_ctx->ctx, src0_buf_ctx->dma_buf.fd, src0_buf_ctx->dma_buf.virt_addr, segment_size_bytes, total_offset);
if (!mem) return GGML_STATUS_FAILED;
auto deleter = [ctx = matmul_ctx->ctx](rknn_tensor_mem* m) { if (m) rknn_destroy_mem(ctx, m); };
mem_B_segments[idx] = std::shared_ptr<rknn_tensor_mem>(mem, deleter);
backend_ctx->b_mem_handle_cache[cache_key] = mem_B_segments[idx];
}
RKNN_CHECK(rknn_matmul_set_io_mem(matmul_ctx->ctx, mem_B_segments[idx].get(), &matmul_ctx->io_attr.B), "set_io_mem B segment");
break;
}
RKNN_CHECK(rknn_matmul_set_io_mem(matmul_ctxs[i]->ctx, mem_B_segments[i].get(), &matmul_ctxs[i]->io_attr.B), "set_io_mem B segment");
}
current_offset_in_tensor += segment_size_bytes;
current_offset_in_tensor += type_size_packed > 0 ? (size_t)seg.size_n * K_op * type_size_packed : (size_t)seg.size_n * K_op / 2;
}
}
// ===========================================
@@ -383,7 +386,7 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
std::vector<float> scales_A(M);
float scale_B = 1.0f;
{
auto cache_key = std::make_tuple(M, K, (int)w_type);
auto cache_key = std::make_tuple(M, K_op, (int)pipeline->npu_type_a);
auto& matmul_ctx_0 = matmul_ctxs[0];
mem_A_shared = get_or_create_npu_buffer(backend_ctx, matmul_ctx_0->ctx, matmul_ctx_0->io_attr.A.size, cache_key, backend_ctx->a_buffer_cache);
@@ -393,69 +396,54 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
const int row_stride = (int)(src1->nb[1] / sizeof(float));
void* dst_base = mem_A_shared->virt_addr;
switch (op_support->npu_type_a) {
case rknpu2_configuration::NPU_TYPE_FP16: {
uint16_t* dst_ptr = (uint16_t*)dst_base;
#pragma omp parallel for
for (int m = 0; m < M; ++m) {
const float* src_row = x + (size_t)m * row_stride;
uint16_t* dst_row = dst_ptr + (size_t)m * K;
rknpu2_quantization::convert_fp32_to_fp16(src_row, dst_row, K);
}
break;
}
case rknpu2_configuration::NPU_TYPE_INT8: {
int8_t* dst_ptr = (int8_t*)dst_base;
#pragma omp parallel for
for (int m = 0; m < M; ++m) {
const float* src_row = x + (size_t)m * row_stride;
float amax_m = 0.0f;
for (int k = 0; k < K; ++k) {
amax_m = std::max(amax_m, std::abs(src_row[k]));
}
scales_A[m] = amax_m / 127.0f;
int8_t* dst_row = dst_ptr + (size_t)m * K;
rknpu2_quantization::quantize_fp32_to_int8(src_row, dst_row, K, scales_A[m]);
}
break;
}
case rknpu2_configuration::NPU_TYPE_INT4: {
auto* src0_buf_ctx = (ggml_backend_rknpu_buffer_context*)src0->buffer->context;
std::vector<float> s_vec;
{
std::lock_guard<std::mutex> lock(src0_buf_ctx->mutex);
auto it = src0_buf_ctx->hadamard_s_vectors.find(src0);
GGML_ASSERT(it != src0_buf_ctx->hadamard_s_vectors.end() && "Hadamard 's' vector not found");
s_vec = it->second;
}
uint8_t* dst_ptr = (uint8_t*)dst_base;
#pragma omp parallel for
for (int m = 0; m < M; ++m) {
const float* src_row = x + (size_t)m * row_stride;
std::vector<float> signed_row(K);
for(int k=0; k<K; ++k) signed_row[k] = src_row[k] * s_vec[k];
std::vector<float> rotated_row(K_op);
rknpu2_calibration::hadamard_transform(rotated_row.data(), signed_row.data(), K, K_op);
float amax_m = 0.0f;
for (int k = 0; k < K_op; ++k) amax_m = std::max(amax_m, std::abs(rotated_row[k]));
scales_A[m] = amax_m / 7.0f;
uint8_t* dst_row = dst_ptr + (size_t)m * (K_op / 2);
rknpu2_quantization::quantize_fp32_to_int4_packed(rotated_row.data(), dst_row, K_op, scales_A[m]);
}
break;
}
default:
// This should not be reached if config is correct
return GGML_STATUS_FAILED;
std::vector<float> s_vec;
if (is_hadamard) {
auto* src0_buf_ctx = (ggml_backend_rknpu_buffer_context*)src0->buffer->context;
std::lock_guard<std::mutex> lock(src0_buf_ctx->mutex);
auto it = src0_buf_ctx->hadamard_s_vectors.find(src0);
GGML_ASSERT(it != src0_buf_ctx->hadamard_s_vectors.end() && "Hadamard 's' vector not found");
s_vec = it->second;
}
if (op_support->npu_type_a == rknpu2_configuration::NPU_TYPE_INT8 || op_support->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
#pragma omp parallel for
for (int m = 0; m < M; ++m) {
const float* src_row = x + (size_t)m * row_stride;
std::vector<float> ready_row(K_op);
if (is_hadamard) {
std::vector<float> signed_row(K);
for(int k=0; k<K; ++k) signed_row[k] = src_row[k] * s_vec[k];
rknpu2_calibration::hadamard_transform(ready_row.data(), signed_row.data(), K, K_op);
} else {
memcpy(ready_row.data(), src_row, K * sizeof(float));
}
if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_FP16) {
uint16_t* dst_ptr = (uint16_t*)dst_base;
uint16_t* dst_row = dst_ptr + (size_t)m * K_op;
rknpu2_quantization::convert_fp32_to_fp16(ready_row.data(), dst_row, K_op);
}
else if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT8) {
float amax_m = 0.0f;
for (int k = 0; k < K_op; ++k) amax_m = std::max(amax_m, std::abs(ready_row[k]));
scales_A[m] = amax_m / 127.0f;
int8_t* dst_ptr = (int8_t*)dst_base;
int8_t* dst_row = dst_ptr + (size_t)m * K_op;
rknpu2_quantization::quantize_fp32_to_int8(ready_row.data(), dst_row, K_op, scales_A[m]);
}
else if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
float amax_m = 0.0f;
for (int k = 0; k < K_op; ++k) amax_m = std::max(amax_m, std::abs(ready_row[k]));
scales_A[m] = amax_m / 7.0f;
uint8_t* dst_ptr = (uint8_t*)dst_base;
uint8_t* dst_row = dst_ptr + (size_t)m * (K_op / 2);
rknpu2_quantization::quantize_fp32_to_int4_packed(ready_row.data(), dst_row, K_op, scales_A[m]);
}
}
if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT8 || pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
ggml_backend_buffer_t src0_buffer = src0->buffer;
auto* src0_buf_ctx = (ggml_backend_rknpu_buffer_context*)src0_buffer->context;
{
@@ -468,8 +456,8 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
RKNN_CHECK(rknn_mem_sync(matmul_ctx_0->ctx, mem_A_shared.get(), RKNN_MEMORY_SYNC_TO_DEVICE), "sync A TO_DEVICE");
for (size_t i = 0; i < num_active_segments; i++) {
RKNN_CHECK(rknn_matmul_set_io_mem(matmul_ctxs[i]->ctx, mem_A_shared.get(), &matmul_ctxs[i]->io_attr.A), "set_io_mem A for core");
for (size_t idx = 0; idx < num_active_segments; idx++) {
RKNN_CHECK(rknn_matmul_set_io_mem(matmul_ctxs[idx]->ctx, mem_A_shared.get(), &matmul_ctxs[idx]->io_attr.A), "set_io_mem A for core");
}
}
@@ -477,12 +465,12 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
// ========== 4. Preparing C-matrix ==========
// ===========================================
{
for (size_t i = 0; i < num_active_segments; i++) {
auto& matmul_ctx = matmul_ctxs[i];
auto cache_key = std::make_tuple(M, active_segments[i].size_n, active_segments[i].core_id);
mem_C_segments[i] = get_or_create_npu_buffer(backend_ctx, matmul_ctx->ctx, matmul_ctx->io_attr.C.size, cache_key, backend_ctx->c_buffer_cache);
if (!mem_C_segments[i]) return GGML_STATUS_FAILED;
RKNN_CHECK(rknn_matmul_set_io_mem(matmul_ctx->ctx, mem_C_segments[i].get(), &matmul_ctx->io_attr.C), "set_io_mem C");
for (size_t idx = 0; idx < num_active_segments; idx++) {
auto& matmul_ctx = matmul_ctxs[idx];
auto cache_key = std::make_tuple(M, active_segments[idx].size_n, active_segments[idx].core_id, (int)pipeline->npu_type_c);
mem_C_segments[idx] = get_or_create_npu_buffer(backend_ctx, matmul_ctx->ctx, matmul_ctx->io_attr.C.size, cache_key, backend_ctx->c_buffer_cache);
if (!mem_C_segments[idx]) return GGML_STATUS_FAILED;
RKNN_CHECK(rknn_matmul_set_io_mem(matmul_ctx->ctx, mem_C_segments[idx].get(), &matmul_ctx->io_attr.C), "set_io_mem C");
}
}
@@ -491,8 +479,8 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
// ==========================================
{
#pragma omp parallel for num_threads(num_active_segments)
for (size_t i = 0; i < num_active_segments; i++) {
int ret = rknn_matmul_run(matmul_ctxs[i]->ctx);
for (size_t idx = 0; idx < num_active_segments; idx++) {
int ret = rknn_matmul_run(matmul_ctxs[idx]->ctx);
if (ret != RKNN_SUCC) {
// Handle error
}
@@ -504,33 +492,41 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
// ===========================================
{
float* dst_data = (float*)dst->data;
for (size_t i = 0; i < num_active_segments; i++) {
RKNN_CHECK(rknn_mem_sync(matmul_ctxs[i]->ctx, mem_C_segments[i].get(), RKNN_MEMORY_SYNC_FROM_DEVICE), "sync C FROM_DEVICE");
for (size_t idx = 0; idx < num_active_segments; idx++) {
RKNN_CHECK(rknn_mem_sync(matmul_ctxs[idx]->ctx, mem_C_segments[idx].get(), RKNN_MEMORY_SYNC_FROM_DEVICE), "sync C FROM_DEVICE");
}
const float hadamard_divisor = pipeline->use_hadamard ? (float)K_op : 1.0f;
#pragma omp parallel for
for (int m = 0; m < M; m++) {
switch (op_support->npu_type_c) {
switch (pipeline->npu_type_c) {
case rknpu2_configuration::NPU_TYPE_FP32: {
for (size_t i = 0; i < num_active_segments; i++) {
int N_offset = active_segments[i].offset_n;
int N_segment = active_segments[i].size_n;
float* src_segment_base = (float*)mem_C_segments[i]->virt_addr;
memcpy(dst_data + (size_t)m * N + N_offset,
src_segment_base + (size_t)m * N_segment,
N_segment * sizeof(float));
for (size_t idx = 0; idx < num_active_segments; idx++) {
int N_offset = active_segments[idx].offset_n;
int N_segment = active_segments[idx].size_n;
float* src_segment_base = (float*)mem_C_segments[idx]->virt_addr;
float* dst_ptr = dst_data + (size_t)m * N + N_offset;
float* src_ptr = src_segment_base + (size_t)m * N_segment;
if (pipeline->use_hadamard) {
for(int n=0; n<N_segment; ++n) dst_ptr[n] = src_ptr[n] / hadamard_divisor;
} else {
memcpy(dst_ptr, src_ptr, N_segment * sizeof(float));
}
}
break;
}
case rknpu2_configuration::NPU_TYPE_INT32: {
float dequant_scale = scales_A[m] * scale_B;
for (size_t i = 0; i < num_active_segments; i++) {
int N_offset = active_segments[i].offset_n;
int N_segment = active_segments[i].size_n;
float dequant_scale = (scales_A[m] * scale_B) / hadamard_divisor;
for (size_t idx = 0; idx < num_active_segments; idx++) {
int N_offset = active_segments[idx].offset_n;
int N_segment = active_segments[idx].size_n;
float* dst_ptr = dst_data + (size_t)m * N + N_offset;
int32_t* src_segment_base = (int32_t*)mem_C_segments[i]->virt_addr;
int32_t* src_segment_base = (int32_t*)mem_C_segments[idx]->virt_addr;
int32_t* src_ptr = src_segment_base + (size_t)m * N_segment;
rknpu2_quantization::dequantize_int32_to_fp32(src_ptr, dst_ptr, N_segment, dequant_scale);
}
@@ -538,22 +534,19 @@ static enum ggml_status ggml_backend_rknpu_graph_compute(ggml_backend_t backend,
}
case rknpu2_configuration::NPU_TYPE_INT16: {
float dequant_scale = scales_A[m] * scale_B;
if (op_support->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
dequant_scale /= (float)K_op;
}
float dequant_scale = (scales_A[m] * scale_B) / hadamard_divisor;
for (size_t i = 0; i < num_active_segments; i++) {
int N_offset = active_segments[i].offset_n;
int N_segment = active_segments[i].size_n;
for (size_t idx = 0; idx < num_active_segments; idx++) {
int N_offset = active_segments[idx].offset_n;
int N_segment = active_segments[idx].size_n;
float* dst_ptr = dst_data + (size_t)m * N + N_offset;
int16_t* src_segment_base = (int16_t*)mem_C_segments[i]->virt_addr;
int16_t* src_segment_base = (int16_t*)mem_C_segments[idx]->virt_addr;
int16_t* src_ptr = src_segment_base + (size_t)m * N_segment;
rknpu2_quantization::dequantize_int16_to_fp32(src_ptr, dst_ptr, N_segment, dequant_scale);
}
break;
}
default:
// This should not be reached if config is correct
break;
@@ -587,229 +580,188 @@ static enum ggml_status ggml_backend_rknpu_buffer_init_tensor(ggml_backend_buffe
return GGML_STATUS_SUCCESS;
}
// Function for dequantizing GGUF format to FP32 and optionally applying Hadamard transform
static std::vector<float> dequantize_tensor(
const struct ggml_tensor * tensor,
ggml_backend_rknpu_buffer_context * ctx,
const void * raw_data,
int K, int N, int K_op, bool use_hadamard
) {
std::vector<float> fp32_matrix((size_t)N * K_op);
auto dequantize_row = [&](int n, float* row_out) {
if (tensor->type == GGML_TYPE_F32) {
const float* src = (const float*)raw_data;
memcpy(row_out, src + (size_t)n * K, K * sizeof(float));
} else if (tensor->type == GGML_TYPE_F16) {
const ggml_fp16_t* src = (const ggml_fp16_t*)raw_data;
const ggml_fp16_t* src_row = src + (size_t)n * K;
for (int k = 0; k < K; ++k) row_out[k] = ggml_fp16_to_fp32(src_row[k]);
} else if (tensor->type == GGML_TYPE_Q8_0) {
const block_q8_0* src = (const block_q8_0*)raw_data;
dequantize_row_q8_0(src + (size_t)n * (K / QK8_0), row_out, K);
} else if (tensor->type == GGML_TYPE_Q6_K) {
const block_q6_K* src = (const block_q6_K*)raw_data;
dequantize_row_q6_K(src + (size_t)n * (K / QK_K), row_out, K);
} else if (tensor->type == GGML_TYPE_Q4_0) {
const block_q4_0* src = (const block_q4_0*)raw_data;
dequantize_row_q4_0(src + (size_t)n * (K / QK4_0), row_out, K);
} else {
GGML_ASSERT(false && "Unsupported weight type for NPU pipeline");
}
};
if (use_hadamard) {
std::vector<float> s_vec(K_op, 1.0f);
std::mt19937 gen(reinterpret_cast<uintptr_t>(tensor));
std::uniform_int_distribution<int> distrib(0, 1);
for(int k = 0; k < K_op; ++k) {
s_vec[k] = (distrib(gen) == 0) ? -1.0f : 1.0f;
}
{
std::lock_guard<std::mutex> lock(ctx->mutex);
ctx->hadamard_s_vectors[tensor] = s_vec;
}
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
std::vector<float> raw_row(K);
dequantize_row(n, raw_row.data());
std::vector<float> signed_row(K);
for(int k=0; k<K; ++k) signed_row[k] = raw_row[k] * s_vec[k];
rknpu2_calibration::hadamard_transform(fp32_matrix.data() + (size_t)n * K_op, signed_row.data(), K, K_op);
}
} else {
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
dequantize_row(n, fp32_matrix.data() + (size_t)n * K);
}
}
return fp32_matrix;
}
// Function for quantizing FP32 matrix to target NPU format
static std::vector<uint8_t> quantize_tensor(
const struct ggml_tensor * tensor,
ggml_backend_rknpu_buffer_context * ctx,
const std::vector<float>& fp32_matrix,
int K_op, int N,
rknpu2_configuration::Rknpu2NpuType npu_type
) {
size_t n_elements = (size_t)N * K_op;
// FP16
if (npu_type == rknpu2_configuration::NPU_TYPE_FP16) {
std::vector<uint8_t> npu_bytes(n_elements * sizeof(uint16_t));
uint16_t* fp16_ptr = (uint16_t*)npu_bytes.data();
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
rknpu2_quantization::convert_fp32_to_fp16(
fp32_matrix.data() + (size_t)n * K_op,
fp16_ptr + (size_t)n * K_op,
K_op);
}
return npu_bytes;
}
float amax = 0.0f;
if (npu_type == rknpu2_configuration::NPU_TYPE_INT4) {
amax = rknpu2_calibration::calculate_entropy_amax(fp32_matrix.data(), n_elements);
} else {
#pragma omp parallel for reduction(max:amax)
for (size_t i = 0; i < n_elements; ++i) {
amax = std::max(amax, std::abs(fp32_matrix[i]));
}
}
float quant_divisor = (npu_type == rknpu2_configuration::NPU_TYPE_INT4) ? 7.0f : 127.0f;
float global_scale_b = amax / quant_divisor;
{
std::lock_guard<std::mutex> lock(ctx->mutex);
ctx->quantized_tensor_scales[tensor] = global_scale_b;
}
// INT8
if (npu_type == rknpu2_configuration::NPU_TYPE_INT8) {
std::vector<uint8_t> npu_bytes(n_elements);
int8_t* int8_ptr = (int8_t*)npu_bytes.data();
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
rknpu2_quantization::quantize_fp32_to_int8(
fp32_matrix.data() + (size_t)n * K_op,
int8_ptr + (size_t)n * K_op,
K_op, global_scale_b);
}
return npu_bytes;
}
// INT4
std::vector<uint8_t> npu_bytes(n_elements / 2);
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
rknpu2_quantization::quantize_fp32_to_int4_packed(
fp32_matrix.data() + (size_t)n * K_op,
npu_bytes.data() + (size_t)n * (K_op / 2),
K_op, global_scale_b);
}
return npu_bytes;
}
// Function for splitting into NPU-native layout segments and writing to DMA buffer
static void pack_tensor(
const uint8_t* src_data,
uint8_t* dst_dma_ptr,
int K_op, int N, int core_count,
const rknpu2_configuration::Rknpu2HardwarePipeline * pipeline
) {
auto segments = compute_matrix_segments(N, core_count, pipeline->n_align);
uint8_t* current_write_ptr = dst_dma_ptr;
std::vector<uint8_t> packed_temp;
for (const auto& seg : segments) {
if (seg.size_n == 0) continue;
size_t segment_packed_size = 0;
if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_FP16) {
segment_packed_size = (size_t)seg.size_n * K_op * 2;
} else if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT8) {
segment_packed_size = (size_t)seg.size_n * K_op;
} else if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
segment_packed_size = (size_t)seg.size_n * K_op / 2;
}
packed_temp.resize(segment_packed_size);
pipeline->pack_func(packed_temp.data(), src_data, K_op, N, seg.offset_n, seg.size_n);
memcpy(current_write_ptr, packed_temp.data(), segment_packed_size);
current_write_ptr += segment_packed_size;
}
}
static void ggml_backend_rknpu_buffer_set_tensor(ggml_backend_buffer_t buffer, struct ggml_tensor * tensor, const void * data, size_t offset, size_t size) {
auto * ctx = (ggml_backend_rknpu_buffer_context *) buffer->context;
uint8_t* dma_base = (uint8_t*)ctx->dma_buf.virt_addr;
uint8_t* tensor_dma_ptr = dma_base + ((uintptr_t)tensor->data - (uintptr_t)ggml_backend_buffer_get_base(buffer));
// Getting the current device configuration to drive the packing logic
const auto& config = rknpu2_configuration::Rknpu2ConfigManager::get_instance().get_current_config();
const auto* op_support = config.find_op_support(tensor->type);
const auto* pipeline = config.resolve_op_support(tensor);
// If there is a specific packing function defined for this tensor type, it's a weight matrix
if (op_support && op_support->pack_func) {
if (pipeline && pipeline->pack_func) {
const int K = (int)tensor->ne[0];
const int N = (int)tensor->ne[1];
if (tensor->type == GGML_TYPE_F16) {
auto segments = compute_matrix_segments(N, config.core_count, op_support->n_align);
const int K_op = pipeline->use_hadamard ? rknpu2_calibration::next_power_of_two(K) : K;
uint8_t* current_write_ptr = tensor_dma_ptr;
std::vector<uint8_t> packed_data_temp;
for (const auto& seg : segments) {
if (seg.size_n == 0) continue;
size_t segment_packed_size_bytes = (size_t)seg.size_n * K * sizeof(uint16_t);
packed_data_temp.resize(segment_packed_size_bytes);
op_support->pack_func(
packed_data_temp.data(),
(const uint8_t*)data,
K, N, seg.offset_n, seg.size_n
);
memcpy(current_write_ptr, packed_data_temp.data(), segment_packed_size_bytes);
current_write_ptr += segment_packed_size_bytes;
}
// GGML_TYPE_Q8_0
} else if (tensor->type == GGML_TYPE_Q8_0) {
const block_q8_0* src_blocks = (const block_q8_0*)data;
const size_t n_elements = (size_t)K * N;
// Finding the global scale without storing the full dequantized matrix.
float amax = 0.0f;
#pragma omp parallel
{
std::vector<float> tmp_row(K);
#pragma omp for reduction(max:amax)
for (int n = 0; n < N; ++n) {
dequantize_row_q8_0(src_blocks + (size_t)n * (K / QK8_0), tmp_row.data(), K);
for (int k = 0; k < K; ++k) amax = std::max(amax, std::abs(tmp_row[k]));
}
}
const float global_scale_b = amax / 127.0f;
// Storing it in the buffer context cache
{
std::lock_guard<std::mutex> lock(ctx->mutex);
ctx->quantized_tensor_scales[tensor] = global_scale_b;
}
// Dequantizing and re-quantizing directly row-by-row
std::vector<int8_t> requantized_data(n_elements);
#pragma omp parallel
{
std::vector<float> tmp_row(K);
#pragma omp for
for (int n = 0; n < N; ++n) {
dequantize_row_q8_0(src_blocks + (size_t)n * (K / QK8_0), tmp_row.data(), K);
rknpu2_quantization::quantize_fp32_to_int8(tmp_row.data(), requantized_data.data() + (size_t)n * K, K, global_scale_b);
}
}
// Packing into native format by segments
auto segments = compute_matrix_segments(N, config.core_count, op_support->n_align);
uint8_t* current_write_ptr = tensor_dma_ptr;
std::vector<uint8_t> packed_data_temp;
for (const auto& seg : segments) {
if (seg.size_n == 0) continue;
size_t segment_packed_size = (size_t)seg.size_n * K;
packed_data_temp.resize(segment_packed_size);
op_support->pack_func(
packed_data_temp.data(),
(const uint8_t*)requantized_data.data(),
K, N, seg.offset_n, seg.size_n
);
memcpy(current_write_ptr, packed_data_temp.data(), segment_packed_size);
current_write_ptr += segment_packed_size;
}
// GGML_TYPE_Q4_0
} else if (tensor->type == GGML_TYPE_Q4_0) {
if (op_support->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
const block_q4_0* src_blocks = (const block_q4_0*)data;
const int K = (int)tensor->ne[0];
const int N = (int)tensor->ne[1];
const int padded_K = rknpu2_calibration::next_power_of_two(K);
std::vector<float> s_vec(padded_K);
std::mt19937 gen(reinterpret_cast<uintptr_t>(tensor));
std::uniform_int_distribution<int> distrib(0, 1);
for(int k = 0; k < padded_K; ++k) {
s_vec[k] = (distrib(gen) == 0) ? -1.0f : 1.0f;
}
// Storing the vector for use during activation processing
{
std::lock_guard<std::mutex> lock(ctx->mutex);
ctx->hadamard_s_vectors[tensor] = s_vec;
}
// Dequantize original weights
std::vector<float> dequantized_data((size_t)N * K);
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
dequantize_row_q4_0(src_blocks + (size_t)n * (K / QK4_0), dequantized_data.data() + (size_t)n * K, K);
}
// Apply Hadamard transform using precomputed s vector
const size_t n_elements_padded = (size_t)N * padded_K;
std::vector<float> rotated_b_fp32(n_elements_padded);
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
const float* src_row = dequantized_data.data() + (size_t)n * K;
float* dst_row_rot = rotated_b_fp32.data() + (size_t)n * padded_K;
std::vector<float> signed_row(K);
for(int k=0; k<K; ++k) signed_row[k] = src_row[k] * s_vec[k];
rknpu2_calibration::hadamard_transform(dst_row_rot, signed_row.data(), K, padded_K);
}
// Finding scale on the rotated data
const float amax = rknpu2_calibration::calculate_entropy_amax(rotated_b_fp32.data(), n_elements_padded);
const float global_scale_b = amax / 7.0f;
{
std::lock_guard<std::mutex> lock(ctx->mutex);
ctx->quantized_tensor_scales[tensor] = global_scale_b;
}
// Quantizing data to INT4
std::vector<uint8_t> packed_int4_data(n_elements_padded / 2);
#pragma omp parallel for
for (int n = 0; n < N; ++n) {
rknpu2_quantization::quantize_fp32_to_int4_packed(
rotated_b_fp32.data() + (size_t)n * padded_K,
packed_int4_data.data() + (size_t)n * (padded_K/2),
padded_K, global_scale_b
);
}
// Packing into native RKNN format with padded_K
uint8_t* current_write_ptr = tensor_dma_ptr;
std::vector<uint8_t> packed_data_temp;
for (const auto& seg : compute_matrix_segments(N, config.core_count, op_support->n_align)) {
if (seg.size_n == 0) continue;
size_t segment_packed_size = (size_t)seg.size_n * padded_K / 2;
packed_data_temp.resize(segment_packed_size);
op_support->pack_func(
packed_data_temp.data(),
packed_int4_data.data(),
padded_K, N, seg.offset_n, seg.size_n
);
memcpy(current_write_ptr, packed_data_temp.data(), segment_packed_size);
current_write_ptr += segment_packed_size;
}
} else {
const block_q4_0* src_blocks = (const block_q4_0*)data;
const size_t n_elements = (size_t)K * N;
// Finding the global scale without storing the full dequantized matrix.
float amax = 0.0f;
#pragma omp parallel
{
std::vector<float> tmp_row(K);
#pragma omp for reduction(max:amax)
for (int n = 0; n < N; ++n) {
dequantize_row_q4_0(src_blocks + (size_t)n * (K / QK4_0), tmp_row.data(), K);
for (int k = 0; k < K; ++k) amax = std::max(amax, std::abs(tmp_row[k]));
}
}
const float global_scale_b = amax / 7.0f;
// Storing it in the buffer context cache
{
std::lock_guard<std::mutex> lock(ctx->mutex);
ctx->quantized_tensor_scales[tensor] = global_scale_b;
}
// Dequantizing and re-quantizing directly row-by-row
std::vector<uint8_t> requantized_data(n_elements / 2);
#pragma omp parallel
{
std::vector<float> tmp_row(K);
#pragma omp for
for (int n = 0; n < N; ++n) {
dequantize_row_q4_0(src_blocks + (size_t)n * (K / QK4_0), tmp_row.data(), K);
rknpu2_quantization::quantize_fp32_to_int4_packed(tmp_row.data(), requantized_data.data() + (size_t)n * (K/2), K, global_scale_b);
}
}
// Packing into native format by segments
auto segments = compute_matrix_segments(N, config.core_count, op_support->n_align);
uint8_t* current_write_ptr = tensor_dma_ptr;
std::vector<uint8_t> packed_data_temp;
for (const auto& seg : segments) {
if (seg.size_n == 0) continue;
size_t segment_packed_size = (size_t)seg.size_n * K / 2;
packed_data_temp.resize(segment_packed_size);
op_support->pack_func(
packed_data_temp.data(), requantized_data.data(),
K, N, seg.offset_n, seg.size_n
);
memcpy(current_write_ptr, packed_data_temp.data(), segment_packed_size);
current_write_ptr += segment_packed_size;
}
}
}
// Other tensor types
std::vector<float> fp32_matrix = dequantize_tensor(tensor, ctx, data, K, N, K_op, pipeline->use_hadamard);
std::vector<uint8_t> npu_matrix = quantize_tensor(tensor, ctx, fp32_matrix, K_op, N, pipeline->npu_type_a);
pack_tensor(npu_matrix.data(), tensor_dma_ptr, K_op, N, config.core_count, pipeline);
} else {
memcpy(tensor_dma_ptr + offset, data, size);
}
@@ -874,25 +826,30 @@ static size_t ggml_backend_rknpu_buffer_type_get_alloc_size(ggml_backend_buffer_
// Getting the current device configuration
const auto& config = rknpu2_configuration::Rknpu2ConfigManager::get_instance().get_current_config();
const auto* op_support = config.find_op_support(tensor->type);
// Padding Q4_0 weights to the next power of two for Hadamard Transform.
if (tensor->type == GGML_TYPE_Q4_0) {
if (op_support && op_support->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
const int K = (int)tensor->ne[0];
const int N = (int)tensor->ne[1];
// Defining hardware pipeline for the tensor
const auto* pipeline = config.resolve_op_support(tensor);
const int padded_K = rknpu2_calibration::next_power_of_two(K);
if (pipeline) {
const int K = (int)tensor->ne[0];
const int N = (int)tensor->ne[1];
auto segments = compute_matrix_segments(N, config.core_count, pipeline->n_align);
auto segments = compute_matrix_segments(N, config.core_count, op_support->n_align);
size_t total_size = 0;
for (const auto& seg : segments) {
if (seg.size_n > 0) {
total_size += (size_t)seg.size_n * padded_K / 2;
const int K_op = pipeline->use_hadamard ? rknpu2_calibration::next_power_of_two(K) : K;
size_t total_size = 0;
for (const auto& seg : segments) {
if (seg.size_n > 0) {
if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT4) {
total_size += (size_t)seg.size_n * K_op / 2;
} else if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_INT8) {
total_size += (size_t)seg.size_n * K_op;
} else if (pipeline->npu_type_a == rknpu2_configuration::NPU_TYPE_FP16) {
total_size += (size_t)seg.size_n * K_op * 2;
}
}
return total_size;
}
return total_size;
}
// Fallback to default size calculation for other types.
@@ -951,31 +908,31 @@ static bool ggml_backend_rknpu_device_supports_op(ggml_backend_dev_t dev, const
case GGML_OP_MUL_MAT: {
const struct ggml_tensor * src0 = op->src[0]; // Weights
const struct ggml_tensor * src1 = op->src[1]; // Activations
// Reject zero-dimension ops
// Searching for available hardware pipeline for this tensor
const auto* pipeline = config.resolve_op_support(src0);
if (!pipeline) {
return false;
}
// Rejecting zero-dimension ops
if (src0->ne[0] == 0 || src0->ne[1] == 0 ||
src1->ne[0] == 0 || src1->ne[1] == 0) {
return false;
}
// Finding if there is a supported operation for the given weight type
const auto* op_support = config.find_op_support(src0->type);
if (!op_support) {
return false;
}
// Checking if activation type matches the supported operation
if (src1->type != op_support->type_a) {
if (src1->type != GGML_TYPE_F32) {
return false;
}
// Checking for K alignment
if (src0->ne[0] % op_support->k_align != 0) {
if (src0->ne[0] % pipeline->k_align != 0) {
return false;
}
// Checking for N alignment
if (src0->ne[1] % op_support->n_align != 0) {
if (src0->ne[1] % pipeline->n_align != 0) {
return false;
}
@@ -1004,7 +961,7 @@ static ggml_backend_t ggml_backend_rknpu_device_init_backend(ggml_backend_dev_t
if (!rknpu2_configuration::Rknpu2ConfigManager::get_instance().select_device("RK3588")) return NULL;
ggml_backend_rknpu_context * ctx = new ggml_backend_rknpu_context();
static const struct ggml_backend_i rknpu_backend_interface = {
/* .get_name = */ ggml_backend_rknpu_name,
/* .free = */ ggml_backend_rknpu_free,
@@ -1052,7 +1009,7 @@ static ggml_backend_dev_t ggml_backend_rknpu_reg_get_device(ggml_backend_reg_t r
if (index != 0) {
return NULL;
}
static const struct ggml_backend_buffer_type_i rknpu_buffer_type_interface = {
/* .get_name = */ ggml_backend_rknpu_buffer_type_get_name,
/* .alloc_buffer = */ ggml_backend_rknpu_buffer_type_alloc_buffer,
@@ -1091,7 +1048,7 @@ static ggml_backend_dev_t ggml_backend_rknpu_reg_get_device(ggml_backend_reg_t r
/* .reg = */ reg,
/* .context = */ NULL,
};
if (rknpu_buffer_type.device == NULL) {
rknpu_buffer_type.device = &rknpu_device;
}
+152 -25
View File
@@ -3,6 +3,8 @@
#include "rknpu2-configuration.h"
#include <arm_neon.h>
#include <cstdlib>
#include <sstream>
// --- Anonymous namespace for chip-specific packing functions ---
@@ -112,6 +114,18 @@ void pack_B_rk3588_int4(
} // anonymous namespace
namespace {
// Function for parsing ENV variable
std::vector<std::string> split_string(const std::string& str, char delimiter) {
std::vector<std::string> tokens;
std::string token;
std::istringstream tokenStream(str);
while (std::getline(tokenStream, token, delimiter)) {
if(!token.empty()) tokens.push_back(token);
}
return tokens;
}
} // anonymous namespace
namespace rknpu2_configuration {
@@ -120,43 +134,156 @@ Rknpu2ConfigManager& Rknpu2ConfigManager::get_instance() {
return instance;
}
const std::vector<std::string>* Rknpu2DeviceConfig::get_active_pattern(int tensor_type) const {
auto it = default_patterns.find(tensor_type);
if (it == default_patterns.end()) {
return nullptr;
}
if (use_custom_pattern && !custom_hybrid_pattern.empty()) {
return &custom_hybrid_pattern;
}
return &it->second;
}
const Rknpu2HardwarePipeline* Rknpu2DeviceConfig::resolve_op_support(const struct ggml_tensor* w_tensor) const {
if (!w_tensor) return nullptr;
auto find_pipeline = [this](const std::string& name) -> const Rknpu2HardwarePipeline* {
for (const auto& pipe : hardware_pipelines) {
if (pipe.pipeline_name == name) return &pipe;
}
return nullptr;
};
// Retrieve active quantization pattern based on tensor type (or custom ENV variable)
const std::vector<std::string>* pattern_ptr = get_active_pattern((int)w_tensor->type);
// If no pattern is registered for this type and no global override exists, reject operation
if (!pattern_ptr || pattern_ptr->empty()) {
return nullptr;
}
const auto& pattern = *pattern_ptr;
// Acquiring the lock on the pattern mutex for thread-safe tensor tracking
std::lock_guard<std::mutex> lock(*pattern_mutex);
// Retrieving the unique tensor name
std::string name = w_tensor->name;
if (name.empty()) {
name = "ptr_" + std::to_string(reinterpret_cast<uintptr_t>(w_tensor));
}
// Assigning the next sequence number if this tensor is seen for the first time
if (tensor_sequence_map.find(name) == tensor_sequence_map.end()) {
tensor_sequence_map[name] = global_tensor_counter++;
}
// Selecting the pipeline cyclically based on the defined pattern
int seq_id = tensor_sequence_map[name];
size_t pattern_idx = seq_id % pattern.size();
const std::string& selected_pipeline = pattern[pattern_idx];
const auto* pipeline = find_pipeline(selected_pipeline);
// If no hardware pipeline exists with this name, reject operation
if (!pipeline) {
return nullptr;
}
return pipeline;
}
Rknpu2ConfigManager::Rknpu2ConfigManager() {
// Reading custom hybrid pattern ENV variable
const char* env_pattern = std::getenv("HYBRID_PATTERN");
bool use_custom_pattern = false;
std::vector<std::string> custom_pattern;
if (env_pattern != nullptr) {
custom_pattern = split_string(env_pattern, ',');
use_custom_pattern = true;
}
// --- Define RK3588 Configuration ---
Rknpu2DeviceConfig rk3588_config;
rk3588_config.device_name = "RK3588";
rk3588_config.core_count = 3;
rk3588_config.supported_ops = {
rk3588_config.hardware_pipelines = {
{
/* .type_w = */ GGML_TYPE_F16, // Weights must be converted from F16
/* .type_a = */ GGML_TYPE_F32,
/* .npu_type_a = */ NPU_TYPE_FP16, // Activations must be converted to FP16
/* .npu_type_c = */ NPU_TYPE_FP32, // Result is already in FP32
/* .mm_type = */ RKNN_FLOAT16_MM_FLOAT16_TO_FLOAT32,
/* .k_align = */ 32,
/* .n_align = */ 16,
/* .pack_func = */ pack_B_rk3588_fp16
/* .pipeline_name = */ "FP16_STANDARD",
/* .npu_type_a = */ NPU_TYPE_FP16,
/* .npu_type_c = */ NPU_TYPE_FP32,
/* .mm_type = */ RKNN_FLOAT16_MM_FLOAT16_TO_FLOAT32,
/* .k_align = */ 32,
/* .n_align = */ 16,
/* .pack_func = */ pack_B_rk3588_fp16,
/* .use_hadamard = */ false
},
{
/* .type_w = */ GGML_TYPE_Q8_0, // Weights must be converted from Q8_0
/* .type_a = */ GGML_TYPE_F32,
/* .npu_type_a = */ NPU_TYPE_INT8, // Activations must be converted to INT8
/* .npu_type_c = */ NPU_TYPE_INT32, // Result must be converted from INT32
/* .mm_type = */ RKNN_INT8_MM_INT8_TO_INT32,
/* .k_align = */ 32,
/* .n_align = */ 32,
/* .pack_func = */ pack_B_rk3588_int8
/* .pipeline_name = */ "FP16_HADAMARD",
/* .npu_type_a = */ NPU_TYPE_FP16,
/* .npu_type_c = */ NPU_TYPE_FP32,
/* .mm_type = */ RKNN_FLOAT16_MM_FLOAT16_TO_FLOAT32,
/* .k_align = */ 32,
/* .n_align = */ 16,
/* .pack_func = */ pack_B_rk3588_fp16,
/* .use_hadamard = */ true
},
{
/* .type_w = */ GGML_TYPE_Q4_0, // Weights must be converted from Q4_0
/* .type_a = */ GGML_TYPE_F32,
/* .npu_type_a = */ NPU_TYPE_INT4, // Activations must be converted to INT4
/* .npu_type_c = */ NPU_TYPE_INT16, // Result must be converted from INT16
/* .mm_type = */ RKNN_INT4_MM_INT4_TO_INT16,
/* .k_align = */ 32,
/* .n_align = */ 64,
/* .pack_func = */ pack_B_rk3588_int4
/* .pipeline_name = */ "INT8_STANDARD",
/* .npu_type_a = */ NPU_TYPE_INT8,
/* .npu_type_c = */ NPU_TYPE_INT32,
/* .mm_type = */ RKNN_INT8_MM_INT8_TO_INT32,
/* .k_align = */ 32,
/* .n_align = */ 32,
/* .pack_func = */ pack_B_rk3588_int8,
/* .use_hadamard = */ false
},
{
/* .pipeline_name = */ "INT8_HADAMARD",
/* .npu_type_a = */ NPU_TYPE_INT8,
/* .npu_type_c = */ NPU_TYPE_INT32,
/* .mm_type = */ RKNN_INT8_MM_INT8_TO_INT32,
/* .k_align = */ 32,
/* .n_align = */ 32,
/* .pack_func = */ pack_B_rk3588_int8,
/* .use_hadamard = */ true
},
{
/* .pipeline_name = */ "INT4_STANDARD",
/* .npu_type_a = */ NPU_TYPE_INT4,
/* .npu_type_c = */ NPU_TYPE_INT16,
/* .mm_type = */ RKNN_INT4_MM_INT4_TO_INT16,
/* .k_align = */ 32,
/* .n_align = */ 64,
/* .pack_func = */ pack_B_rk3588_int4,
/* .use_hadamard = */ false
},
{
/* .pipeline_name = */ "INT4_HADAMARD",
/* .npu_type_a = */ NPU_TYPE_INT4,
/* .npu_type_c = */ NPU_TYPE_INT16,
/* .mm_type = */ RKNN_INT4_MM_INT4_TO_INT16,
/* .k_align = */ 32,
/* .n_align = */ 64,
/* .pack_func = */ pack_B_rk3588_int4,
/* .use_hadamard = */ true
}
};
// Assigning custom variables
rk3588_config.use_custom_pattern = use_custom_pattern;
rk3588_config.custom_hybrid_pattern = custom_pattern;
// Defining default quantization sequences for each supported ggml_type
rk3588_config.default_patterns[(int)GGML_TYPE_F16] = {"FP16_STANDARD"};
rk3588_config.default_patterns[(int)GGML_TYPE_Q8_0] = {"INT8_STANDARD"};
rk3588_config.default_patterns[(int)GGML_TYPE_Q6_K] = {"INT8_STANDARD", "INT4_HADAMARD"};
rk3588_config.default_patterns[(int)GGML_TYPE_Q4_0] = {"INT4_HADAMARD"};
device_configs["RK3588"] = rk3588_config;
// --- Define RK3576 Configuration (Placeholder) ---
+38 -25
View File
@@ -7,14 +7,14 @@
#include <string>
#include <vector>
#include <map>
#include <unordered_map>
#include <functional>
#include <cstdint>
#include <mutex>
#include <memory>
namespace rknpu2_configuration {
// Forward declaration for packing function pointer
struct ggml_tensor;
/**
* @brief Defines a function signature for packing a segment of the weight matrix (B)
* from a standard row-major layout into the device-specific native layout.
@@ -49,49 +49,62 @@ enum Rknpu2NpuType {
};
/**
* @brief Describes a single supported matrix multiplication operation on the NPU.
* @brief Describes a single supported hardware pipeline available on the NPU.
*
* This struct maps a combination of ggml tensor types for weights (src0) and
* activations (src1) to a specific RKNN matmul type. It also specifies the
* hardware alignment requirements for the K and N dimensions of the matrices.
* This struct encapsulates the specific constraints and capabilities of a
* hardware execution path, including required data types, memory alignment
* rules, and the specific packing function needed to prepare tensor for
* the underlying matrix multiplication operation.
*/
struct Rknpu2Operation {
ggml_type type_w; // Weight tensor type
ggml_type type_a; // Activation tensor type
struct Rknpu2HardwarePipeline {
std::string pipeline_name; // The unique hardware pipeline name
Rknpu2NpuType npu_type_a; // The target data type for activations on the NPU
Rknpu2NpuType npu_type_c; // The data type of the result from the NPU
rknn_matmul_type mm_type; // Corresponding RKNN operation type
int k_align; // Required alignment for the K dimension
int n_align; // Required alignment for the N dimension
PackingFunction pack_func; // Function to pack the weight matrix for this op
bool use_hadamard; // Flag for using Hadamard Transform
};
/**
* @brief Holds the complete hardware configuration for a specific Rockchip NPU.
*
* This includes the device name, number of available NPU cores, and a list of
* all matrix multiplication operations supported by the hardware.
* all available hardware pipelines. It also manages layer-by-layer quantization strategies.
*/
struct Rknpu2DeviceConfig {
std::string device_name;
int core_count;
std::vector<Rknpu2Operation> supported_ops;
std::vector<Rknpu2HardwarePipeline> hardware_pipelines;
// Type-specific default patterns mapping
std::map<int, std::vector<std::string>> default_patterns;
// Custom global pattern
bool use_custom_pattern = false;
std::vector<std::string> custom_hybrid_pattern;
// Tensor-Quantization mapping
mutable std::shared_ptr<std::mutex> pattern_mutex = std::make_shared<std::mutex>();
mutable std::unordered_map<std::string, int> tensor_sequence_map;
mutable int global_tensor_counter = 0;
/**
* @brief Finds the corresponding operation configuration for the given weight type.
* @param w_type The ggml_type of the weight tensor.
* @return A pointer to the Rknpu2Operation if found, otherwise nullptr.
* @brief Resolves the appropriate hardware pipeline for a given tensor.
* Applies layer-by-layer cyclical selection if the active pattern has multiple entries.
*/
const Rknpu2Operation* find_op_support(ggml_type w_type) const {
for (const auto& op : supported_ops) {
if (op.type_w == w_type) {
return &op;
}
}
return nullptr;
}
const Rknpu2HardwarePipeline* resolve_op_support(const struct ggml_tensor* w_tensor) const;
private:
/**
* @brief Retrieves the active pattern for a given tensor type.
* Prioritizes custom ENV variable pattern over default mapping.
*/
const std::vector<std::string>* get_active_pattern(int tensor_type) const;
};
/**