Commit 9f12cd4a4 for llama.cpp

commit 9f12cd4a4c52ea351b85cf78939db8991e1bc025
Author: Alan Tseng <alan.tseng.cs@gmail.com>
Date:   Mon Oct 5 15:34:15 2026 +0800

    ggml-cpu : add Q8_0 IME1 matrix kernel for SpacemiT X60 (#28479)

    * ggml-cpu : add Q8_0 IME1 matrix kernel for SpacemiT X60

    On the SpacemiT X60, IME matrix acceleration only covered Q4_0/Q4_1/Q4_K.
    Q8_0 had no IME1 kernel, and since the SpacemiT build sets
    GGML_CPU_REPACK=OFF there was no repack path compiled in either, so Q8_0
    had no accelerated path at all and ran roughly ten times slower than
    Q4_0 for prefill on the same board.

    - add make_block_q8_0x16 and the Q8_0 repack entry: interleave the
      weights into the 16-column layout the IME1 vmadot sequence expects
    - add ime1::gemm_kernel_i8i8, an int8 x int8 IME1 kernel with a
      single-row and a 4-row A path; the 4-row path loads each B panel once
      and reuses it across 4 rows of A
    - add quantize_a_4row_i8 for the 4-row activation quantization
    - wire both into forward_mul_mat and the repack factory for Q8_0
    - docs: mark Q8_0 as supported on X60

    Correctness was checked against a quant-exact integer reference for
    K = 32 up to 4096, with a max relative error of about 1e-6, and by
    checking that generation stays coherent across several prompts.

    Tested on Milk-V Jupiter (SpacemiT X60), Bianbu 2.1.1, gcc 14.2, with
    Qwen2.5-0.5B-Instruct Q8_0. llama-bench -t 4 under taskset -c 0-3, 5
    repetitions on an idle board: pp128 goes from 10.70 to 93.87 t/s. Q4_0
    is unchanged at 106.40 -> 107.51 t/s, as expected since this does not
    touch that path.

    * ggml-cpu : move q8_0_16x32 decl to IME1 section

    * ggml-cpu : align q8_0 IME1 kernel assignments

diff --git a/docs/build-riscv64-spacemit.md b/docs/build-riscv64-spacemit.md
index 7a9a1f3ad..c16054a81 100644
--- a/docs/build-riscv64-spacemit.md
+++ b/docs/build-riscv64-spacemit.md
@@ -63,7 +63,7 @@ ${QEMU_ROOT_PATH}/bin/qemu-riscv64 -L ${RISCV_ROOT_PATH_IME1}/sysroot -cpu max,v
 | Q5_1 |  | :heavy_check_mark: |
 | Q5_K |  | :heavy_check_mark: |
 | Q6_K |  | :heavy_check_mark: |
-| Q8_0 |  | :heavy_check_mark: |
+| Q8_0 | :heavy_check_mark: | :heavy_check_mark: |


 ## Performance
diff --git a/ggml/src/ggml-cpu/spacemit/ime.cpp b/ggml/src/ggml-cpu/spacemit/ime.cpp
index 450d2d1aa..81e86b9dd 100644
--- a/ggml/src/ggml-cpu/spacemit/ime.cpp
+++ b/ggml/src/ggml-cpu/spacemit/ime.cpp
@@ -321,6 +321,9 @@ template <typename BLOC_TYPE, int64_t INTER_SIZE, int64_t NB_COLS> class tensor_
                           std::is_same_v<BLOC_TYPE, block_q4_K>) {
                 gemm_kernel     = spacemit_kernels::ime1::gemm_kernel_i8i4;
                 set_kernel_impl = true;
+            } else if constexpr (std::is_same_v<BLOC_TYPE, block_q8_0>) {
+                gemm_kernel     = spacemit_kernels::ime1::gemm_kernel_i8i8;
+                set_kernel_impl = true;
             }
         }
 #endif
@@ -624,6 +627,9 @@ template <typename BLOC_TYPE, int64_t INTER_SIZE, int64_t NB_COLS> class tensor_
                           std::is_same_v<BLOC_TYPE, block_q4_K>) {
                 gemm_kernel     = spacemit_kernels::ime1::gemm_kernel_i8i4;
                 set_kernel_impl = true;
+            } else if constexpr (std::is_same_v<BLOC_TYPE, block_q8_0>) {
+                gemm_kernel     = spacemit_kernels::ime1::gemm_kernel_i8i8;
+                set_kernel_impl = true;
             }
         }
 #endif
@@ -1237,6 +1243,7 @@ class tensor_traits_common : public tensor_traits_base {
 static const tensor_traits<block_q4_0, 32, 16>  q4_0_16x32_q8_0;
 static const tensor_traits<block_q4_1, 32, 16>  q4_1_16x32_q8_0;
 static const tensor_traits<block_q4_K, 32, 16>  q4_k_16x32_q8_0;
+static const tensor_traits<block_q8_0, 32, 16>  q8_0_16x32_q8_0;
 // Impl By IME2
 static const tensor_traits<block_q2_K, 256, 32> q2_k_32x256_q8_0;
 static const tensor_traits<block_q3_K, 256, 32> q3_k_32x256_q8_0;
@@ -1348,6 +1355,12 @@ static const ggml::cpu::tensor_traits * ggml_riscv64_spacemit_get_optimal_repack
                     return &ggml::cpu::riscv64_spacemit::q8_0_32x32_q8_0;
                 }
 #endif
+
+#if defined(RISCV64_SPACEMIT_IME1)
+                if (cur->ne[1] % 16 == 0 && (ggml::cpu::riscv64_spacemit::global_spine_env_info.use_ime1)) {
+                    return &ggml::cpu::riscv64_spacemit::q8_0_16x32_q8_0;
+                }
+#endif
             }
             break;
         case GGML_TYPE_MXFP4:
diff --git a/ggml/src/ggml-cpu/spacemit/ime1_kernels.cpp b/ggml/src/ggml-cpu/spacemit/ime1_kernels.cpp
index 6acc6819d..6c974f6eb 100644
--- a/ggml/src/ggml-cpu/spacemit/ime1_kernels.cpp
+++ b/ggml/src/ggml-cpu/spacemit/ime1_kernels.cpp
@@ -991,6 +991,224 @@ void SQ4BitGemmM1Kernel_CompInt8_ScaleFp16_Impl(size_t          BlkLen,
         }
     }
 }
+
+
+// ---- Q8_0 IME1 int8xint8 M4 kernel ----------------------------------------
+// Handles 4 rows at once using the 4-row interleaved A produced by quantize_a_4row_i8:
+// per K-block (stride 144B) the first 16B are the four fp32 row scales and the following
+// 128B are four 32B chunks, each holding 8 K-values for each of the 4 rows. That maps
+// directly onto vmadot's 4-lane row dimension, so the A side is identical to Q4_0.
+// B is block_q8_0x16 as in the M1 kernel: 32B of fp16 scales then 512B of interleaved
+// int8, loaded with 8 plain vle8 into v2..v9 (no nibble unpack).
+static void SQ8BitGemmM4Kernel_CompInt8_ScaleFp16_Impl(size_t          BlkLen,
+                                                       const uint8_t * QuantA,
+                                                       const uint8_t * QuantBData,
+                                                       float *         C,
+                                                       size_t          CountN,
+                                                       size_t          BlockCountK,
+                                                       const size_t    ldc) {
+    // Same invariant as the Q4_0 M4 kernel: SAVE_RESULT_4x16 stores a full 4x16 tile with no tail
+    // handling. Q8_0 is only admitted to the IME1 path when ne[1] % 16 == 0 (ime.cpp),
+    // and the n-tiling step is NB_COLS == 16, so a partial tile never reaches here.
+    GGML_ASSERT(CountN % 16 == 0);
+
+    const size_t INNER = BlkLen / 16;
+    const size_t LDC   = ldc * sizeof(float);
+
+    for (size_t n = 0; n < CountN; n += 16) {
+        uint8_t * QuantBDataPtr =
+            (uint8_t *) QuantBData + (n / 16) * BlockCountK * (16 * sizeof(_Float16) + 512);
+        float * CPtr = C + n;
+
+        __asm__ volatile(
+            "vsetvli            t0, zero, e32, m8           \n\t"
+            "vxor.vv            v24, v24, v24               \n\t"
+            "addi               t3, %[BlockCountK], 0       \n\t"
+            "addi               a1, %[A], 0                 \n\t"
+            "addi               s1, %[B], 0                 \n\t"
+
+            "BLOCK_COUNTK_LOOP%=:                           \n\t"
+            "addi               s5, s1, 0                   \n\t"
+            "addi               s1, s5, 32                  \n\t"
+            "vsetvli            t0, zero, e32, m8           \n\t"
+            "vxor.vv            v16, v16, v16               \n\t"
+            "flw                f1, (a1)                    \n\t"
+            "flw                f2, 4(a1)                   \n\t"
+            "flw                f3, 8(a1)                   \n\t"
+            "flw                f4, 12(a1)                  \n\t"
+            "addi               a1, a1, 16                  \n\t"
+            "addi               t2, %[INNER], 0             \n\t"
+
+            "BLOCK_INNER_LOOP%=:                            \n\t"
+            "vsetvli            t0, zero, e8, m1            \n\t"
+            "vle8.v             v2, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v3, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v4, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v5, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v6, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v7, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v8, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v9, (s1)                    \n\t"
+            "addi               s1, s1, 32                  \n\t"
+            "vle8.v             v10, (a1)                   \n\t"
+            "addi               a1, a1, 32                  \n\t"
+            "vle8.v             v11, (a1)                   \n\t"
+            "addi               a1, a1, 32                  \n\t"
+
+            SQ4BIT_KERNEL_COMP_4x16x16
+
+            "addi               t2, t2, -1                  \n\t"
+            "bnez               t2, BLOCK_INNER_LOOP%=      \n\t"
+
+            LOAD_SCALE_4x16_FP16
+
+            "vsetvli            t0, zero, e32, m8           \n\t"
+            "vfcvt.f.x.v        v16, v16                    \n\t"
+            "vfmacc.vv          v24, v16, v8                \n\t"
+            "addi               t3, t3, -1                  \n\t"
+            "bnez               t3, BLOCK_COUNTK_LOOP%=     \n\t"
+
+            "RESULT_SAVE%=:                                 \n\t"
+
+            SAVE_RESULT_4x16
+
+            :
+            : [INNER] "r"(INNER), [A] "r"(QuantA), [B] "r"(QuantBDataPtr), [LDC] "r"(LDC),
+              [BlockCountK] "r"(BlockCountK), [C] "r"(CPtr)
+            : "cc", "t0", "t1", "t2", "t3", "a1", "a2", "a3", "a4", "f1", "f2", "f3", "f4",
+              "s1", "s2", "s3", "s4", "s5", "s6");
+    }
+}
+
+// ---- Q8_0 IME1 int8xint8 M1 kernel ----------------------------------------
+// B comes from block_q8_0x16 (make_block_q8_0x16): 16 fp16 scales (32B) then 512B of interleaved
+// int8 weights laid out as [INNER0: reg0..reg7][INNER1: reg0..reg7], each reg 32B = 4 columns x
+// (even=K-first-half / odd=K-second-half). A comes from quantize_a_row_i8 (same as Q4_0 path).
+// Reuses the vmadot COMP macro and the ACC_F16 dequant tail from the Q4_0 kernel.
+static void SQ8BitGemmM1Kernel_CompInt8_ScaleFp16_Impl(size_t          BlkLen,
+                                                       const uint8_t * QuantA,
+                                                       const uint8_t * QuantBData,
+                                                       float *         C,
+                                                       size_t          CountN,
+                                                       size_t          BlockCountK,
+                                                       const size_t    ldc) {
+    GGML_UNUSED(ldc);
+    const size_t INNER = BlkLen / 16;  // = 2 for QK8_0=32
+
+    for (size_t n = 0; n < CountN; n += 16) {
+        size_t    nblks = (CountN - n) > 16 ? 16 : CountN - n;
+        // Each x16 K-block is {16 fp16 scales (32B), 512B interleaved int8}; stride = 544B.
+        uint8_t * QuantBDataPtr = (uint8_t *) QuantBData + (n / 16) * BlockCountK * (16 * sizeof(_Float16) + 512);
+        float *   CPtr = C + n;
+        size_t    cnt  = BlockCountK;
+
+        __asm__ volatile(
+            "vsetvli      t0, zero, e32, m4       \n\t"
+            "vxor.vv      v28, v28, v28           \n\t"
+            // s7 = per-K-block base (scale@+0, data@+32, block stride 544)
+            "addi         s7, %[B], 0             \n\t"
+            "addi         s5, %[A], 0             \n\t"   // A scale (fp32)
+            "addi         s6, %[A], 12            \n\t"   // A data (int8), offset like Q4_0 M1
+            "LOOP_K%=:                            \n\t"
+            "addi         s1, s7, 32              \n\t"   // data base for this K-block
+            // B scales: d[0..15] fp16 at block start. Load in 4 groups of 4 (d[0-3]/[4-7]/[8-11]/[12-15])
+            // matching the 4 accumulators (each covers columns [g*4 .. g*4+3]).
+            "addi         s2, s7, 8               \n\t"
+            "addi         s3, s7, 16              \n\t"
+            "addi         s4, s7, 24              \n\t"
+            "vsetvli      t0, zero, e16, mf4      \n\t"
+            "vle16.v      v4, (s7)                \n\t"
+            "vle16.v      v5, (s2)                \n\t"
+            "vle16.v      v6, (s3)                \n\t"
+            "vle16.v      v7, (s4)                \n\t"
+            "addi         s7, s7, 544             \n\t"   // advance to next K-block (32 scale + 512 data)
+            "flw          f1, (s5)                \n\t"
+            "addi         s5, s5, 4               \n\t"
+            "vfwcvt.f.f.v v8, v4                  \n\t"
+            "vfwcvt.f.f.v v9, v5                  \n\t"
+            "vfwcvt.f.f.v v10, v6                 \n\t"
+            "vfwcvt.f.f.v v11, v7                 \n\t"
+            "vsetvli      t0, zero, e32, mf2      \n\t"
+            "addi         t5, %[INNER], 0         \n\t"
+            "vxor.vv      v16, v16, v16           \n\t"
+            "vxor.vv      v18, v18, v18           \n\t"
+            "vxor.vv      v20, v20, v20           \n\t"
+            "vxor.vv      v22, v22, v22           \n\t"
+            // combined scale (A_scale * B_scale) -> v24..v27 (one per accumulator)
+            "vfmul.vf     v24, v8, f1             \n\t"
+            "vfmul.vf     v25, v9, f1             \n\t"
+            "vfmul.vf     v26, v10, f1            \n\t"
+            "vfmul.vf     v27, v11, f1            \n\t"
+            "addi         %[CNT], %[CNT], -1      \n\t"
+            "vsetvli      t0, zero, e8, m1        \n\t"
+            "LOOP_INNER%=:                        \n\t"
+            // load 8 B data regs (v0..v7) directly (int8, no nibble unpack)
+            "vle8.v       v0, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            "vle8.v       v1, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            "vle8.v       v2, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            "vle8.v       v3, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            "vle8.v       v4, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            "vle8.v       v5, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            "vle8.v       v6, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            "vle8.v       v7, (s1)                \n\t"
+            "addi         s1, s1, 32              \n\t"
+            // load A (2 halves): v14 from s5, v15 from s6 (matches Q4_0 M1 A packing)
+            "vsetvli      t0, zero, e8, mf4       \n\t"
+            "vle8.v       v14, (s5)               \n\t"
+            "addi         s5, s5, 16              \n\t"
+            "vle8.v       v15, (s6)               \n\t"
+            "addi         s6, s6, 16              \n\t"
+            "addi         t5, t5, -1              \n\t"
+            "vsetvli      t0, zero, e8, m1        \n\t"
+            SQ4BIT_KERNEL_COMP_1x8x2_4X8X4
+            "bnez         t5, LOOP_INNER%=        \n\t"
+            "vsetvli      t0, zero, e32, mf2      \n\t"
+            SQ4BIT_KERNEL_ACC_F16_1X4X4
+            "bnez         %[CNT], LOOP_K%=        \n\t"
+            "addi         t3, zero, 16            \n\t"
+            "addi         s1, %[C], 16            \n\t"
+            "addi         s2, %[C], 32            \n\t"
+            "addi         s3, %[C], 48            \n\t"
+            "blt          %[NBLKS], t3, ST_TAIL%= \n\t"
+            "vse32.v      v28, (%[C])             \n\t"
+            "vse32.v      v29, (s1)               \n\t"
+            "vse32.v      v30, (s2)               \n\t"
+            "vse32.v      v31, (s3)               \n\t"
+            "jal          x0, END%=               \n\t"
+            "ST_TAIL%=:                           \n\t"
+            "vsetvli      t0, %[NBLKS], e32, mf2  \n\t"
+            "sub          %[NBLKS], %[NBLKS], t0  \n\t"
+            "vse32.v      v28, (%[C])             \n\t"
+            "vsetvli      t0, %[NBLKS], e32, mf2  \n\t"
+            "sub          %[NBLKS], %[NBLKS], t0  \n\t"
+            "vse32.v      v29, (s1)               \n\t"
+            "vsetvli      t0, %[NBLKS], e32, mf2  \n\t"
+            "sub          %[NBLKS], %[NBLKS], t0  \n\t"
+            "vse32.v      v30, (s2)               \n\t"
+            "vsetvli      t0, %[NBLKS], e32, mf2  \n\t"
+            "sub          %[NBLKS], %[NBLKS], t0  \n\t"
+            "vse32.v      v31, (s3)               \n\t"
+            "END%=:                               \n\t"
+            : [CNT] "+r"(cnt), [NBLKS] "+r"(nblks)
+            : [INNER] "r"(INNER), [A] "r"(QuantA), [B] "r"(QuantBDataPtr), [C] "r"(CPtr)
+            : "cc", "t0", "t3", "t5", "f1", "s1", "s2", "s3", "s4", "s5", "s6", "s7");
+    }
+}
+
 }  // namespace

 namespace ime1 {
@@ -1023,5 +1241,24 @@ size_t gemm_kernel_i8i4(size_t          blk_len,
         return 1;
     }
 }
+
+size_t gemm_kernel_i8i8(size_t          blk_len,
+                        const uint8_t * quant_a_ptr,
+                        const uint8_t * quant_b_data,
+                        const uint8_t * quant_b_zp,
+                        float *         c_ptr,
+                        size_t          count_m,
+                        size_t          count_n,
+                        size_t          k_blks,
+                        size_t          ldc) {
+    GGML_UNUSED(quant_b_zp);
+    if (count_m >= 4) {
+        SQ8BitGemmM4Kernel_CompInt8_ScaleFp16_Impl(blk_len, quant_a_ptr, quant_b_data, c_ptr, count_n, k_blks,
+                                                   ldc);
+        return 4;
+    }
+    SQ8BitGemmM1Kernel_CompInt8_ScaleFp16_Impl(blk_len, quant_a_ptr, quant_b_data, c_ptr, count_n, k_blks, ldc);
+    return 1;
+}
 }  // namespace ime1
 }  // namespace spacemit_kernels
diff --git a/ggml/src/ggml-cpu/spacemit/ime_kernels.h b/ggml/src/ggml-cpu/spacemit/ime_kernels.h
index 0a1fafffb..476a0e74b 100644
--- a/ggml/src/ggml-cpu/spacemit/ime_kernels.h
+++ b/ggml/src/ggml-cpu/spacemit/ime_kernels.h
@@ -79,6 +79,16 @@ size_t gemm_kernel_i8i4(size_t          blk_len,
                         size_t          k_blks,
                         size_t          ldc);

+size_t gemm_kernel_i8i8(size_t          blk_len,
+                        const uint8_t * quant_a_ptr,
+                        const uint8_t * quant_b_data,
+                        const uint8_t * quant_b_zp,
+                        float *         c_ptr,
+                        size_t          count_m,
+                        size_t          count_n,
+                        size_t          k_blks,
+                        size_t          ldc);
+
 void quantize_a_row_i8(size_t blk_len, const float * a_ptr, size_t count_k, uint8_t * quant_a_ptr);

 void quantize_a_4row_i8(size_t blk_len, const float * a_ptr, size_t count_k, uint8_t * quant_a_ptr);
diff --git a/ggml/src/ggml-cpu/spacemit/repack.cpp b/ggml/src/ggml-cpu/spacemit/repack.cpp
index 3c879c4b7..2039056e0 100644
--- a/ggml/src/ggml-cpu/spacemit/repack.cpp
+++ b/ggml/src/ggml-cpu/spacemit/repack.cpp
@@ -370,6 +370,74 @@ static block_q8_0x32 make_block_q8_0x32(block_q8_0 * in, unsigned int blck_size_
     return out;
 }

+// IME1: interleave 16 q8_0 rows so a plain vle8 sequence in the i8i8 kernel lands weights in the
+// vmadot group/parity layout. Mirrors make_block_q4_0x16 but stores full int8 (no nibble packing).
+// qs (512B) = [INNER step 0: reg0..reg7][INNER step 1: reg0..reg7], each reg 32B holding 4 columns
+// x (even=K-first-half / odd=K-second-half). Column col -> acc=col/4, cgrp=col%4.
+static block_q8_0x16 make_block_q8_0x16(block_q8_0 * in, unsigned int blck_size_interleave) {
+    block_q8_0x16 out;
+    GGML_ASSERT(QK8_0 / blck_size_interleave == 2);
+    GGML_UNUSED(blck_size_interleave);
+
+    for (int i = 0; i < 16; i++) {
+        out.d[i] = in[i].d;
+    }
+
+    memset(out.qs, 0, sizeof(out.qs));
+    for (int col = 0; col < 16; col++) {
+        const int      acc  = col / 4;
+        const int      cgrp = col % 4;
+        const int8_t * q    = in[col].qs;
+        for (int s = 0; s < 2; s++) {
+            const int base   = s * 16;
+            uint8_t * reg_lo = out.qs + (s * 8 + acc)     * 32;
+            uint8_t * reg_hi = out.qs + (s * 8 + acc + 4) * 32;
+            for (int i = 0; i < 4; i++) {
+                reg_lo[(2 * cgrp)     * 4 + i] = (uint8_t) q[base + 0  + i];
+                reg_lo[(2 * cgrp + 1) * 4 + i] = (uint8_t) q[base + 4  + i];
+                reg_hi[(2 * cgrp)     * 4 + i] = (uint8_t) q[base + 8  + i];
+                reg_hi[(2 * cgrp + 1) * 4 + i] = (uint8_t) q[base + 12 + i];
+            }
+        }
+    }
+    return out;
+}
+
+static int repack_q8_0_to_q8_0_16_bl_ref(ggml_tensor *              t,
+                                         int                        interleave_block,
+                                         const void * GGML_RESTRICT data,
+                                         size_t                     data_size) {
+    GGML_ASSERT(t->type == GGML_TYPE_Q8_0);
+    GGML_ASSERT(interleave_block == 16);
+
+    constexpr int nrows_interleaved = 16;
+
+    block_q8_0x16 *    dst = (block_q8_0x16 *) t->data;
+    const block_q8_0 * src = (const block_q8_0 *) data;
+    block_q8_0         dst_tmp[16];
+    int                nrow    = ggml_nrows(t);
+    int                nblocks = t->ne[0] / QK8_0;
+
+    GGML_ASSERT(data_size == nrow * nblocks * sizeof(block_q8_0));
+
+    if (t->ne[1] % nrows_interleaved != 0 || t->ne[0] % QK8_0 != 0) {
+        return -1;
+    }
+
+    for (int b = 0; b < nrow; b += nrows_interleaved) {
+        for (int64_t x = 0; x < nblocks; x++) {
+            for (int i = 0; i < nrows_interleaved; i++) {
+                dst_tmp[i] = src[x + i * nblocks];
+            }
+            *dst++ = make_block_q8_0x16(dst_tmp, interleave_block);
+        }
+        src += nrows_interleaved * nblocks;
+    }
+    return 0;
+
+    GGML_UNUSED(data_size);
+}
+
 static int repack_q2_k_to_q2_k_32_bl(ggml_tensor *              t,
                                      int                        interleave_block,
                                      const void * GGML_RESTRICT data,
@@ -1768,6 +1836,10 @@ template <> int repack<block_q6_K, 32, 32>(ggml_tensor * t, const void * data, s
 #endif
 }

+template <> int repack<block_q8_0, 32, 16>(ggml_tensor * t, const void * data, size_t data_size) {
+    return repack_q8_0_to_q8_0_16_bl_ref(t, 16, data, data_size);
+}
+
 template <> int repack<block_q8_0, 32, 32>(ggml_tensor * t, const void * data, size_t data_size) {
 #if 1
     return repack_q8_0_to_q8_0_32_bl_ref(t, 32, data, data_size);