👏

A64FX SVE での bitnet b1.58 実装

に公開

なんで 1.58 って中途半端なビット数なんや...

-1.0, 0, 1.0 の重みで 3 パターンで log2(3) ~= 1.58 でした.
重みが 1.58bit で, activation(input) も 1.58 というわけではありませんでした
(ただ基本はこちらも量子化前提の模様)

sve だと predicate で 0 を扱えるので便利そ?

A64FX 向けの BitNet b1.58 ternary GEMV ベンチマークカーネルを書きます。A64FX 向けの BitNet b1.58 ternary GEMV ベンチマークを作成しました。

核心部分の解説

FMA 方式のカーネル

// 重みを2つの predicate に展開
svbool_t p_nonzero;  // 0以外の位置
svbool_t p_neg;      // -1 の位置

// sign vector: -1.0 or +1.0
svfloat32_t sign_vec = svsel(p_neg, neg_one, one);

// 1回の masked FMA で完結!
acc = svmla_f32_m(p_nonzero, acc, x, sign_vec);

パッキング形式

2bit/weight:  00=0, 01=+1, 11=-1
A64FX 512bit = 16 lanes → 4 bytes で 16 weights

比較用に Add/Sub 方式も実装

svbool_t p_pos = svbic_z(pg, p_nonzero, p_neg);  // +1 の位置
acc = svadd_f32_m(p_pos, acc, x);  // +1 → 加算
acc = svsub_f32_m(p_neg, acc, x);  // -1 → 減算

使い方

tar xzf bitnet_sve_benchmark.tar.gz
cd bitnet_sve

# Fugaku で
make fugaku
./bitnet_benchmark

# または QEMU で試す
make
qemu-aarch64 -cpu max,sve512=on ./bitnet_benchmark

期待される結果

  • FP16 比でメモリ帯域 8x 削減 (16bit → 2bit)
  • FMA パイプラインを活用できれば演算律速になる可能性
  • 展開オーバーヘッドがどの程度かは実測次第

Fugaku で試してみてね!

#include "ternary_gemv.h"
#include <arm_sve.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>

// ============================================
// Packing/Unpacking utilities
// ============================================

void pack_ternary_weights(const int8_t *weights, uint8_t *packed, size_t n) {
    // Pack 4 ternary weights into 1 byte (2 bits each)
    // Encoding: 00 = 0, 01 = +1, 11 = -1
    size_t packed_idx = 0;
    for (size_t i = 0; i < n; i += 4) {
        uint8_t byte = 0;
        for (int j = 0; j < 4 && (i + j) < n; j++) {
            int8_t w = weights[i + j];
            uint8_t encoded;
            if (w == 0) {
                encoded = 0b00;
            } else if (w == 1) {
                encoded = 0b01;
            } else { // w == -1
                encoded = 0b11;
            }
            byte |= (encoded << (j * 2));
        }
        packed[packed_idx++] = byte;
    }
}

void unpack_ternary_weights(const uint8_t *packed, int8_t *weights, size_t n) {
    size_t packed_idx = 0;
    for (size_t i = 0; i < n; i += 4) {
        uint8_t byte = packed[packed_idx++];
        for (int j = 0; j < 4 && (i + j) < n; j++) {
            uint8_t encoded = (byte >> (j * 2)) & 0b11;
            if (encoded == 0b00) {
                weights[i + j] = 0;
            } else if (encoded == 0b01) {
                weights[i + j] = 1;
            } else { // 0b11
                weights[i + j] = -1;
            }
        }
    }
}

// ============================================
// Scalar baseline
// ============================================

void ternary_gemv_scalar(const uint8_t *W_packed,
                         const float *X,
                         float *Y,
                         int M, int N) {
    int8_t *w_unpacked = (int8_t *)malloc(N * sizeof(int8_t));
    
    for (int i = 0; i < M; i++) {
        unpack_ternary_weights(&W_packed[(i * N) / 4], w_unpacked, N);
        
        float acc = 0.0f;
        for (int j = 0; j < N; j++) {
            acc += (float)w_unpacked[j] * X[j];
        }
        Y[i] = acc;
    }
    
    free(w_unpacked);
}

// ============================================
// SVE helper: unpack 2-bit weights to predicates
// Returns predicates for 16 weights (A64FX: 512-bit = 16 x float32)
// ============================================

static inline void unpack_to_predicates_16(const uint8_t *packed_4bytes,
                                            svbool_t pg,
                                            svbool_t *p_nonzero,
                                            svbool_t *p_neg) {
    // packed_4bytes contains 16 weights (4 bytes = 32 bits = 16 x 2-bit)
    uint32_t bits = *(const uint32_t *)packed_4bytes;
    
    // Create index vector: 0, 1, 2, ..., 15
    svuint32_t indices = svindex_u32(0, 1);
    
    // Extract 2-bit encoding for each position
    // shift = indices * 2
    svuint32_t shift = svlsl_x(pg, indices, 1);
    svuint32_t bits_vec = svdup_u32(bits);
    
    // shifted = bits >> (indices * 2)
    svuint32_t shifted = svlsr_x(pg, bits_vec, shift);
    
    // Extract 2-bit value: masked = shifted & 0b11
    svuint32_t masked = svand_x(pg, shifted, 0x3);
    
    // non_zero: bit 0 is set (values 01 or 11)
    *p_nonzero = svcmpne(pg, svand_x(pg, masked, 0x1), 0);
    
    // negative: both bits set (value 11)
    *p_neg = svcmpeq(pg, masked, 0x3);
}

// ============================================
// SVE FMA-based implementation
// Key idea: acc = fma(acc, x, sign) where sign = ±1.0
// Use predicate to mask zero weights
// ============================================

void ternary_gemv_sve_fma(const uint8_t *W_packed,
                          const float *X,
                          float *Y,
                          int M, int N) {
    
    const svfloat32_t one = svdup_f32(1.0f);
    const svfloat32_t neg_one = svdup_f32(-1.0f);
    
    // A64FX: svcntw() = 16 (512-bit / 32-bit = 16 lanes)
    const int vlen = svcntw();
    
    for (int i = 0; i < M; i++) {
        svfloat32_t acc = svdup_f32(0.0f);
        const uint8_t *row_packed = &W_packed[(i * N) / 4];
        
        int j = 0;
        for (; j + vlen <= N; j += vlen) {
            svbool_t pg = svptrue_b32();
            
            // Unpack 16 weights (4 bytes) to predicates
            svbool_t p_nonzero, p_neg;
            unpack_to_predicates_16(&row_packed[j / 4], pg, &p_nonzero, &p_neg);
            
            // Load activation vector
            svfloat32_t x = svld1(pg, &X[j]);
            
            // Build sign vector: -1.0 where p_neg, else +1.0
            svfloat32_t sign_vec = svsel(p_neg, neg_one, one);
            
            // FMA: acc += x * sign (masked by p_nonzero)
            // svmla_f32_m: acc = p_nonzero ? (acc + x * sign) : acc
            acc = svmla_f32_m(p_nonzero, acc, x, sign_vec);
        }
        
        // Handle remainder
        if (j < N) {
            svbool_t pg = svwhilelt_b32(j, N);
            
            svbool_t p_nonzero, p_neg;
            unpack_to_predicates_16(&row_packed[j / 4], pg, &p_nonzero, &p_neg);
            
            // Combine with tail predicate
            p_nonzero = svand_z(pg, p_nonzero, pg);
            p_neg = svand_z(pg, p_neg, pg);
            
            svfloat32_t x = svld1(pg, &X[j]);
            svfloat32_t sign_vec = svsel(p_neg, neg_one, one);
            
            acc = svmla_f32_m(p_nonzero, acc, x, sign_vec);
        }
        
        // Horizontal reduction
        Y[i] = svaddv(svptrue_b32(), acc);
    }
}

// ============================================
// SVE Add/Sub implementation (no multiply)
// Use svadd_m / svsub_m with predicates
// ============================================

void ternary_gemv_sve_addsub(const uint8_t *W_packed,
                              const float *X,
                              float *Y,
                              int M, int N) {
    
    const int vlen = svcntw();
    
    for (int i = 0; i < M; i++) {
        svfloat32_t acc = svdup_f32(0.0f);
        const uint8_t *row_packed = &W_packed[(i * N) / 4];
        
        int j = 0;
        for (; j + vlen <= N; j += vlen) {
            svbool_t pg = svptrue_b32();
            
            svbool_t p_nonzero, p_neg;
            unpack_to_predicates_16(&row_packed[j / 4], pg, &p_nonzero, &p_neg);
            
            // p_pos = nonzero AND NOT neg
            svbool_t p_pos = svbic_z(pg, p_nonzero, p_neg);
            
            svfloat32_t x = svld1(pg, &X[j]);
            
            // acc += x where positive
            acc = svadd_f32_m(p_pos, acc, x);
            // acc -= x where negative
            acc = svsub_f32_m(p_neg, acc, x);
        }
        
        // Handle remainder
        if (j < N) {
            svbool_t pg = svwhilelt_b32(j, N);
            
            svbool_t p_nonzero, p_neg;
            unpack_to_predicates_16(&row_packed[j / 4], pg, &p_nonzero, &p_neg);
            
            p_nonzero = svand_z(pg, p_nonzero, pg);
            p_neg = svand_z(pg, p_neg, pg);
            svbool_t p_pos = svbic_z(pg, p_nonzero, p_neg);
            
            svfloat32_t x = svld1(pg, &X[j]);
            
            acc = svadd_f32_m(p_pos, acc, x);
            acc = svsub_f32_m(p_neg, acc, x);
        }
        
        Y[i] = svaddv(svptrue_b32(), acc);
    }
}

// ============================================
// FP16 baseline using SVE
// ============================================

void fp16_gemv_sve(const uint16_t *W_fp16,
                   const float *X,
                   float *Y,
                   int M, int N) {
    
    const int vlen = svcntw();
    
    for (int i = 0; i < M; i++) {
        svfloat32_t acc = svdup_f32(0.0f);
        const uint16_t *row = &W_fp16[i * N];
        
        int j = 0;
        for (; j + vlen <= N; j += vlen) {
            svbool_t pg = svptrue_b32();
            
            // Load FP16 weights and convert to FP32
            svfloat16_t w_f16 = svld1_f16(svptrue_b16(), 
                                          (const float16_t *)&row[j]);
            // Convert lower half to FP32
            svfloat32_t w = svcvt_f32_f16_x(pg, w_f16);
            
            svfloat32_t x = svld1(pg, &X[j]);
            
            acc = svmla_f32_x(pg, acc, x, w);
        }
        
        // Handle remainder
        if (j < N) {
            svbool_t pg = svwhilelt_b32(j, N);
            svbool_t pg16 = svwhilelt_b16(j, N);
            
            svfloat16_t w_f16 = svld1_f16(pg16, (const float16_t *)&row[j]);
            svfloat32_t w = svcvt_f32_f16_x(pg, w_f16);
            
            svfloat32_t x = svld1(pg, &X[j]);
            
            acc = svmla_f32_m(pg, acc, x, w);
        }
        
        Y[i] = svaddv(svptrue_b32(), acc);
    }
}

// ============================================
// Utility functions
// ============================================

void init_random_ternary(int8_t *weights, size_t n, unsigned int seed) {
    srand(seed);
    for (size_t i = 0; i < n; i++) {
        int r = rand() % 3;
        weights[i] = (int8_t)(r - 1);  // -1, 0, or +1
    }
}

void init_random_float(float *vec, size_t n, unsigned int seed) {
    srand(seed);
    for (size_t i = 0; i < n; i++) {
        vec[i] = ((float)rand() / RAND_MAX) * 2.0f - 1.0f;  // [-1, 1]
    }
}

void ternary_to_fp16(const int8_t *ternary, uint16_t *fp16, size_t n) {
    // Simple conversion using union trick for FP16
    for (size_t i = 0; i < n; i++) {
        float f = (float)ternary[i];
        // Use ARM FP16 conversion
        __fp16 h = (__fp16)f;
        fp16[i] = *(uint16_t *)&h;
    }
}

float verify_results(const float *ref, const float *test, size_t n) {
    float max_err = 0.0f;
    for (size_t i = 0; i < n; i++) {
        float err = fabsf(ref[i] - test[i]);
        if (err > max_err) {
            max_err = err;
        }
    }
    return max_err;
}

double get_time_sec(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec + ts.tv_nsec * 1e-9;
}
#ifndef TERNARY_GEMV_H
#define TERNARY_GEMV_H

#include <stdint.h>
#include <stddef.h>

#ifdef __cplusplus
extern "C" {
#endif

// Weight encoding (2 bits per weight):
// bit 0: non_zero flag (1 = weight is ±1, 0 = weight is 0)
// bit 1: sign flag (0 = +1, 1 = -1, only valid if non_zero=1)
//
// Encoding: 00 = 0, 01 = +1, 11 = -1 (10 is unused)

// Pack ternary weights into 2-bit format
// weights: array of int8_t with values {-1, 0, +1}
// packed: output buffer (N/4 bytes for N weights)
void pack_ternary_weights(const int8_t *weights, uint8_t *packed, size_t n);

// Unpack for verification
void unpack_ternary_weights(const uint8_t *packed, int8_t *weights, size_t n);

// ============================================
// GEMV implementations: Y = W * X
// W: M x N ternary weights (packed)
// X: N-dim float32 vector
// Y: M-dim float32 output
// ============================================

// Baseline: scalar implementation
void ternary_gemv_scalar(const uint8_t *W_packed,
                         const float *X,
                         float *Y,
                         int M, int N);

// SVE FMA-based implementation (main focus)
void ternary_gemv_sve_fma(const uint8_t *W_packed,
                          const float *X,
                          float *Y,
                          int M, int N);

// SVE with predicate-only (add/sub, no multiply)
void ternary_gemv_sve_addsub(const uint8_t *W_packed,
                              const float *X,
                              float *Y,
                              int M, int N);

// FP16 baseline for comparison
void fp16_gemv_sve(const uint16_t *W_fp16,
                   const float *X,
                   float *Y,
                   int M, int N);

// ============================================
// Utility functions
// ============================================

// Initialize random ternary weights
void init_random_ternary(int8_t *weights, size_t n, unsigned int seed);

// Initialize random float vector
void init_random_float(float *vec, size_t n, unsigned int seed);

// Convert ternary to FP16 for baseline comparison
void ternary_to_fp16(const int8_t *ternary, uint16_t *fp16, size_t n);

// Verify results (returns max absolute error)
float verify_results(const float *ref, const float *test, size_t n);

// Get current time in seconds (for benchmarking)
double get_time_sec(void);

#ifdef __cplusplus
}
#endif

#endif // TERNARY_GEMV_H

Discussion