perf: HVX vectorise bnProc and bnProcPc — 5.7× per-iteration speedup

Replace scalar sat8 loops in bnProc and bnProcPc with HVX:
  widen int8→int16 (Q6_Wh_vsxt_Vb), add/sub in 16-bit, clamp to
  [-128,127] with vmin/vmax, pack low bytes (Q6_Vb_vpacke_VhVh).
  Both functions have a scalar tail for Z not a multiple of 128
  (for Z=384=3×128 the tail is never taken).

Simulator cycle counts (BG1, Z=384, 8-iter avg):
  bnProcPc: 668 616 → 19 551 cycles  (34× faster)
  bnProc:   949 458 → 15 705 cycles  (60× faster)
  per-iteration total: 1 920 877 → 338 029 cycles (5.7× overall)

Remaining bottleneck: cn2bn + bn2cn permutations now 82% of cycles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Raymond Knopp <raymond.knopp@eurecom.fr>
This commit is contained in:
Raymond Knopp
2026-05-26 20:42:38 +00:00
parent acefc9294b
commit 3fc07f2445
2 changed files with 97 additions and 29 deletions

View File

@@ -78,8 +78,8 @@ int main(void)
for (int w = 0; w < N_WARMUP; w++) {
cnProc(&lut, cnProcBuf, cnProcBufRes, 384, 1);
nrLDPC_cn2bnProcBuf_BG1(&lut, cnProcBufRes, bnProcBuf, 384);
scalar_bnProcPc(&lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, 384);
scalar_bnProc(&lut, bnProcBuf, bnProcBufRes, llrRes, 384);
hvx_bnProcPc(&lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, 384);
hvx_bnProc(&lut, bnProcBuf, bnProcBufRes, llrRes, 384);
nrLDPC_bn2cnProcBuf_BG1(&lut, bnProcBufRes, cnProcBuf, 384);
}
@@ -100,12 +100,12 @@ int main(void)
cyc_cn2bn += t1 - t0;
t0 = hexagon_sim_read_pcycles();
scalar_bnProcPc(&lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, 384);
hvx_bnProcPc(&lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, 384);
t1 = hexagon_sim_read_pcycles();
cyc_bnProcPc += t1 - t0;
t0 = hexagon_sim_read_pcycles();
scalar_bnProc(&lut, bnProcBuf, bnProcBufRes, llrRes, 384);
hvx_bnProc(&lut, bnProcBuf, bnProcBufRes, llrRes, 384);
t1 = hexagon_sim_read_pcycles();
cyc_bnProc += t1 - t0;

View File

@@ -253,7 +253,40 @@ static int32_t scalar_cnProcPc(const int8_t *cnProcBuf, t_nrLDPC_lut *p_lut,
}
// =============================================================================
// BN Processing — parity-check variant (scalar bnProcPc)
// HVX helpers — saturating int8 arithmetic via int16 widening
// =============================================================================
// HVX has no single-instruction signed-byte saturating add/sub.
// Strategy: widen to int16 (Q6_Wh_vsxt_Vb), add/sub in 16-bit, clamp to
// [-128,127] with vmin/vmax, pack low bytes back (Q6_Vb_vpacke_VhVh).
//
// vpacke(Vu.h, Vv.h): output[0..63] = low bytes of Vv halfwords,
// output[64..127] = low bytes of Vu halfwords.
// After clamping, the low byte of each int16 is the correct int8 value.
static inline HVX_Vector hvx_sat8_add(HVX_Vector va, HVX_Vector vb,
HVX_Vector v127, HVX_Vector vn128)
{
HVX_VectorPair wa = Q6_Wh_vsxt_Vb(va), wb = Q6_Wh_vsxt_Vb(vb);
HVX_Vector lo = Q6_Vh_vadd_VhVh(Q6_V_lo_W(wa), Q6_V_lo_W(wb));
HVX_Vector hi = Q6_Vh_vadd_VhVh(Q6_V_hi_W(wa), Q6_V_hi_W(wb));
lo = Q6_Vh_vmax_VhVh(Q6_Vh_vmin_VhVh(lo, v127), vn128);
hi = Q6_Vh_vmax_VhVh(Q6_Vh_vmin_VhVh(hi, v127), vn128);
return Q6_Vb_vpacke_VhVh(hi, lo);
}
static inline HVX_Vector hvx_sat8_sub(HVX_Vector va, HVX_Vector vb,
HVX_Vector v127, HVX_Vector vn128)
{
HVX_VectorPair wa = Q6_Wh_vsxt_Vb(va), wb = Q6_Wh_vsxt_Vb(vb);
HVX_Vector lo = Q6_Vh_vsub_VhVh(Q6_V_lo_W(wa), Q6_V_lo_W(wb));
HVX_Vector hi = Q6_Vh_vsub_VhVh(Q6_V_hi_W(wa), Q6_V_hi_W(wb));
lo = Q6_Vh_vmax_VhVh(Q6_Vh_vmin_VhVh(lo, v127), vn128);
hi = Q6_Vh_vmax_VhVh(Q6_Vh_vmin_VhVh(hi, v127), vn128);
return Q6_Vb_vpacke_VhVh(hi, lo);
}
// =============================================================================
// BN Processing — parity-check variant (HVX bnProcPc)
// =============================================================================
// For each BN b in group g (cnidx+1 connected CNs):
// llrRes[b] = sat8( llrProcBuf[b] + Σ_k bnProcBuf[k, b] )
@@ -263,68 +296,98 @@ static int32_t scalar_cnProcPc(const int8_t *cnProcBuf, t_nrLDPC_lut *p_lut,
// CN layer k: byte range [startBn + k * numBn * ZMAX .. + numBn * ZMAX - 1]
// BN b within layer k: bytes [k*cnOff + b] (b in 0..numBn*Z-1)
//
// Groups iterate by connectivity degree (cnidx = CNs-per-BN minus 1).
// lut_numBnInBnGroups has NR_LDPC_NUM_BN_GROUPS_BG1_R13=30 entries; many are
// zero for lower rates and for BG2 — the loop naturally skips them.
// HVX path: outer loop steps 128 bytes at a time; inner k-loop accumulates
// int16 sums in two register-pair halves, then saturates and packs to int8.
// All accesses within a group are sequential — no cache-miss-latency issue.
static void scalar_bnProcPc(t_nrLDPC_lut *p_lut,
const int8_t *bnProcBuf, int8_t *bnProcBufRes,
const int8_t *llrProcBuf, int8_t *llrRes,
uint16_t Z)
static void hvx_bnProcPc(t_nrLDPC_lut *p_lut,
const int8_t *bnProcBuf, int8_t *bnProcBufRes,
const int8_t *llrProcBuf, int8_t *llrRes,
uint16_t Z)
{
const uint8_t *numBn = p_lut->numBnInBnGroups;
const uint32_t *sBn = p_lut->startAddrBnGroups;
const uint16_t *sLlr = p_lut->startAddrBnGroupsLlr;
// Group 0: BNs connected to exactly 1 CN (no accumulation loop needed)
HVX_Vector v127 = Q6_Vh_vsplat_R(127);
HVX_Vector vn128 = Q6_Vh_vsplat_R(-128);
// Group 0: BNs connected to exactly 1 CN
{
uint32_t nb = (uint32_t)numBn[0];
uint32_t total = nb * (uint32_t)Z;
uint32_t sa = sBn[0];
uint32_t sl = sLlr[0];
// Store channel LLR for later bnProc subtraction (extrinsic = channel for 1-CN BN)
memcpy(&bnProcBufRes[sa], &llrProcBuf[sl], total);
for (uint32_t b = 0; b < total; b++)
uint32_t full = (total / HVX_VLEN) * HVX_VLEN;
for (uint32_t b = 0; b < full; b += HVX_VLEN)
*(HVX_UVector *)(llrRes + sl + b) = hvx_sat8_add(
*(HVX_UVector *)(llrProcBuf + sl + b),
*(HVX_UVector *)(bnProcBuf + sa + b), v127, vn128);
for (uint32_t b = full; b < total; b++)
llrRes[sl + b] = sat8_add(llrProcBuf[sl + b], bnProcBuf[sa + b]);
}
// Groups 1+: cnidx = number of connected CNs minus 1
// Groups 1+: accumulate cnidx+1 CN messages per BN
uint8_t idxBnGroup = 0;
for (int cnidx = 1; cnidx < NR_LDPC_NUM_BN_GROUPS_BG1_R13; cnidx++) {
if (numBn[cnidx] == 0) continue;
idxBnGroup++;
uint32_t nb = (uint32_t)numBn[cnidx];
uint32_t cnOff = nb * NR_LDPC_ZMAX; // byte stride between CN layers
uint32_t cnOff = nb * NR_LDPC_ZMAX;
uint32_t total = nb * (uint32_t)Z;
uint32_t sa = sBn[idxBnGroup];
uint32_t sl = sLlr[idxBnGroup];
uint32_t full = (total / HVX_VLEN) * HVX_VLEN;
for (uint32_t b = 0; b < total; b++) {
for (uint32_t b = 0; b < full; b += HVX_VLEN) {
// Init accumulator with channel LLR (widened to int16)
HVX_VectorPair wsum = Q6_Wh_vsxt_Vb(*(HVX_UVector *)(llrProcBuf + sl + b));
HVX_Vector sum_lo = Q6_V_lo_W(wsum);
HVX_Vector sum_hi = Q6_V_hi_W(wsum);
for (int k = 0; k <= cnidx; k++) {
HVX_VectorPair wbn = Q6_Wh_vsxt_Vb(
*(HVX_UVector *)(bnProcBuf + sa + (uint32_t)k * cnOff + b));
sum_lo = Q6_Vh_vadd_VhVh(sum_lo, Q6_V_lo_W(wbn));
sum_hi = Q6_Vh_vadd_VhVh(sum_hi, Q6_V_hi_W(wbn));
}
sum_lo = Q6_Vh_vmax_VhVh(Q6_Vh_vmin_VhVh(sum_lo, v127), vn128);
sum_hi = Q6_Vh_vmax_VhVh(Q6_Vh_vmin_VhVh(sum_hi, v127), vn128);
*(HVX_UVector *)(llrRes + sl + b) = Q6_Vb_vpacke_VhVh(sum_hi, sum_lo);
}
for (uint32_t b = full; b < total; b++) {
int16_t s = (int16_t)llrProcBuf[sl + b];
for (int k = 0; k <= cnidx; k++)
s += (int16_t)bnProcBuf[sa + (uint32_t)k * cnOff + b];
llrRes[sl + b] = s > 127 ? 127 :
s < -128 ? -128 : (int8_t)s;
llrRes[sl + b] = s > 127 ? 127 : s < -128 ? -128 : (int8_t)s;
}
}
}
// =============================================================================
// BN Processing — extrinsic message (scalar bnProc)
// BN Processing — extrinsic message (HVX bnProc)
// =============================================================================
// For each BN-to-CN edge (group g, CN layer k, BN b):
// bnProcBufRes[k, b] = sat8( llrRes[b] - bnProcBuf[k, b] )
// Group 0 was already handled by bnProcPc (1-CN BNs: extrinsic = channel LLR).
//
// Inner loop over b is sequential in all buffers — ideal for HVX.
static void scalar_bnProc(t_nrLDPC_lut *p_lut,
const int8_t *bnProcBuf, int8_t *bnProcBufRes,
const int8_t *llrRes, uint16_t Z)
static void hvx_bnProc(t_nrLDPC_lut *p_lut,
const int8_t *bnProcBuf, int8_t *bnProcBufRes,
const int8_t *llrRes, uint16_t Z)
{
const uint8_t *numBn = p_lut->numBnInBnGroups;
const uint32_t *sBn = p_lut->startAddrBnGroups;
const uint16_t *sLlr = p_lut->startAddrBnGroupsLlr;
HVX_Vector v127 = Q6_Vh_vsplat_R(127);
HVX_Vector vn128 = Q6_Vh_vsplat_R(-128);
uint8_t idxBnGroup = 0;
for (int cnidx = 1; cnidx < NR_LDPC_NUM_BN_GROUPS_BG1_R13; cnidx++) {
if (numBn[cnidx] == 0) continue;
@@ -335,10 +398,15 @@ static void scalar_bnProc(t_nrLDPC_lut *p_lut,
uint32_t total = nb * (uint32_t)Z;
uint32_t sa = sBn[idxBnGroup];
uint32_t sl = sLlr[idxBnGroup];
uint32_t full = (total / HVX_VLEN) * HVX_VLEN;
for (int k = 0; k <= cnidx; k++) {
uint32_t off = (uint32_t)k * cnOff;
for (uint32_t b = 0; b < total; b++)
for (uint32_t b = 0; b < full; b += HVX_VLEN)
*(HVX_UVector *)(bnProcBufRes + sa + off + b) = hvx_sat8_sub(
*(HVX_UVector *)(llrRes + sl + b),
*(HVX_UVector *)(bnProcBuf + sa + off + b), v127, vn128);
for (uint32_t b = full; b < total; b++)
bnProcBufRes[sa + off + b] =
sat8_sub(llrRes[sl + b], bnProcBuf[sa + off + b]);
}
@@ -394,8 +462,8 @@ static int32_t ldpc_scalar_core(
else
nrLDPC_cn2bnProcBuf_BG2(p_lut, cnProcBufRes, bnProcBuf, Z);
scalar_bnProcPc(p_lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, Z);
scalar_bnProc (p_lut, bnProcBuf, bnProcBufRes, llrRes, Z);
hvx_bnProcPc(p_lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, Z);
hvx_bnProc (p_lut, bnProcBuf, bnProcBufRes, llrRes, Z);
if (BG == 1)
nrLDPC_bn2cnProcBuf_BG1(p_lut, bnProcBufRes, cnProcBuf, Z);
@@ -412,8 +480,8 @@ static int32_t ldpc_scalar_core(
else
nrLDPC_cn2bnProcBuf_BG2(p_lut, cnProcBufRes, bnProcBuf, Z);
scalar_bnProcPc(p_lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, Z);
scalar_bnProc (p_lut, bnProcBuf, bnProcBufRes, llrRes, Z);
hvx_bnProcPc(p_lut, bnProcBuf, bnProcBufRes, llrProcBuf, llrRes, Z);
hvx_bnProc (p_lut, bnProcBuf, bnProcBufRes, llrRes, Z);
if (BG == 1)
nrLDPC_bn2cnProcBuf_BG1(p_lut, bnProcBufRes, cnProcBuf, Z);