Vectorized 4-bit dequantization: process 8 packed bytes = 16 output values Each byte contains two 4-bit values: high nibble first, low nibble second
| 87 | // Vectorized 4-bit dequantization: process 8 packed bytes = 16 output values |
| 88 | // Each byte contains two 4-bit values: high nibble first, low nibble second |
| 89 | static inline void |
| 90 | neon_dequant_4bit_16values(const uint8_t* packed, float scale, const float32x4_t lut[4], float* out) { |
| 91 | // Load 8 bytes = 16 x 4-bit values |
| 92 | uint8x8_t raw = vld1_u8(packed); |
| 93 | |
| 94 | // Extract high and low nibbles |
| 95 | uint8x8_t mask4 = vdup_n_u8(0x0F); |
| 96 | uint8x8_t lo_nibbles = vand_u8(raw, mask4); // low nibble (second value) |
| 97 | uint8x8_t hi_nibbles = vshr_n_u8(raw, 4); // high nibble (first value) |
| 98 | |
| 99 | // Interleave hi/lo into 16-element index array for output ordering |
| 100 | // output[2*i] = hi_nibble[i], output[2*i+1] = lo_nibble[i] |
| 101 | uint8x8x2_t interleaved = vzip_u8(hi_nibbles, lo_nibbles); |
| 102 | // interleaved.val[0] has elements 0-7, interleaved.val[1] has elements 8-15 |
| 103 | uint8x16_t indices = vcombine_u8(interleaved.val[0], interleaved.val[1]); |
| 104 | |
| 105 | // Reinterpret float LUT as 64-byte table for vqtbl4q_u8 lookup. |
| 106 | // Each 4-bit index i maps to bytes [i*4 .. i*4+3] in the table. |
| 107 | uint8x16x4_t lut_bytes = { |
| 108 | vreinterpretq_u8_f32(lut[0]), vreinterpretq_u8_f32(lut[1]), vreinterpretq_u8_f32(lut[2]), |
| 109 | vreinterpretq_u8_f32(lut[3]) |
| 110 | }; |
| 111 | // Multiply each index by 4 to get byte offset (max 15*4=60 < 64, safe) |
| 112 | uint8x16_t base = vshlq_n_u8(indices, 2); |
| 113 | // Expand each base offset to 4 consecutive bytes via zip |
| 114 | const uint8x16_t off = vreinterpretq_u8_u32(vdupq_n_u32(0x03020100)); |
| 115 | uint8x8_t lo = vget_low_u8(base), hi = vget_high_u8(base); |
| 116 | uint8x8x2_t z0 = vzip_u8(lo, lo); |
| 117 | uint8x8x2_t z1 = vzip_u8(hi, hi); |
| 118 | uint8x8x2_t zlo = vzip_u8(z0.val[0], z0.val[0]); |
| 119 | uint8x8x2_t zhi = vzip_u8(z0.val[1], z0.val[1]); |
| 120 | uint8x8x2_t zlo2 = vzip_u8(z1.val[0], z1.val[0]); |
| 121 | uint8x8x2_t zhi2 = vzip_u8(z1.val[1], z1.val[1]); |
| 122 | float32x4_t vscale = vdupq_n_f32(scale); |
| 123 | float32x4_t v0 = vreinterpretq_f32_u8(vqtbl4q_u8(lut_bytes, vaddq_u8(vcombine_u8(zlo.val[0], zlo.val[1]), off))); |
| 124 | float32x4_t v1 = vreinterpretq_f32_u8(vqtbl4q_u8(lut_bytes, vaddq_u8(vcombine_u8(zhi.val[0], zhi.val[1]), off))); |
| 125 | float32x4_t v2 = vreinterpretq_f32_u8(vqtbl4q_u8(lut_bytes, vaddq_u8(vcombine_u8(zlo2.val[0], zlo2.val[1]), off))); |
| 126 | float32x4_t v3 = vreinterpretq_f32_u8(vqtbl4q_u8(lut_bytes, vaddq_u8(vcombine_u8(zhi2.val[0], zhi2.val[1]), off))); |
| 127 | |
| 128 | vst1q_f32(out, vmulq_f32(v0, vscale)); |
| 129 | vst1q_f32(out + 4, vmulq_f32(v1, vscale)); |
| 130 | vst1q_f32(out + 8, vmulq_f32(v2, vscale)); |
| 131 | vst1q_f32(out + 12, vmulq_f32(v3, vscale)); |
| 132 | } |
| 133 | |
| 134 | // NEON-optimized BF16 to float conversion (4 values at a time) |
| 135 | static inline float32x4_t neon_bf16x4_to_f32(const bf16_t* src) { |
no outgoing calls
no test coverage detected