Skip to content

RISC-V: fix Inf in nrm2_rvv and use it for RISCV64_ZVL256B - #6172

Open
HiderX wants to merge 2 commits into
OpenMathLib:developfrom
HiderX:riscv-nrm2-rvv-inf
Open

HiderX wants to merge 2 commits into
OpenMathLib:developfrom
HiderX:riscv-nrm2-rvv-inf

Conversation

@HiderX

@HiderX HiderX commented Oct 25, 2026

Copy link
Copy Markdown

Summary

This PR has two commits.

  1. nrm2_rvv.c returns +Inf for an infinite element.
    • The kernel computes a scaled sum of squares and divides by the running
      maximum. Once an infinite element becomes that maximum, inf/inf = NaN
      enters the sum, so the result is NaN instead of +Inf.
    • OpenBLAS's dnrm2_inf utest places the infinity among zeros, where the
      division is skipped, so it does not catch this.
    • Fix: when the result is NaN, the input is scanned again. The kernel
      returns a NaN element if there is one, and +Inf otherwise. Finite inputs
      never take this path.
    • Affects RISCV64_ZVL128B and x280, which already use nrm2_rvv.c.
  2. RISCV64_ZVL256B uses nrm2_rvv.c for SNRM2 and DNRM2, instead of
    nrm2_vector.c.
    • It is faster on the X100, see the table below.
    • It also propagates NaN inputs; nrm2_vector.c can return a finite value
      when the input contains a NaN.

Performance

Platform: SpacemiT K3, one X100 core (2.2 GHz), single thread, GCC 15.2. Each
number is the median ns per call for unit stride.

RISCV64_ZVL256B, develop vs this PR:

routine n develop patched speed-up
SNRM2 1024 3373.3 2738.0 +23.2%
SNRM2 32768 92679 85177 +8.8%
SNRM2 4194304 11743369 10910944 +7.6%
DNRM2 1024 9733.1 8853.6 +9.9%
DNRM2 32768 295859 279052 +6.0%
DNRM2 4194304 37827897 35804011 +5.7%

At every size, all patched processes were faster than all develop processes.
An exploratory job and a separate confirmation job (4 processes per build,
alternating) gave the same speed-ups within 0.1 percentage points.

The fix in commit 1 costs nothing. On RISCV64_ZVL128B, SNRM2 and DNRM2 with
and without it differ by at most 0.1% at every size.

Testing

  • OpenBLAS tests. The default make goal's tests give identical results
    before and after the patch, on both RISCV64_ZVL256B and RISCV64_ZVL128B:
    utest 141/141, extension tests 1479/1479, and all BLAS/CBLAS tests PASSED.

  • Edge-case check against a long-double reference, 1224 cases. It covers
    SNRM2/DNRM2/DSDOT with:

    • tail-heavy lengths from 1 to 32775;
    • strides 1, 2 and 3;
    • zero vectors;
    • huge (1e300 / 1e30) and tiny (1e-300 / 1e-30) magnitudes, and mixtures;
    • an infinity among non-zero values;
    • a NaN.
    build failures
    ZVL256B develop 114: Inf returns NaN, or a NaN input is lost
    ZVL128B develop 96: Inf returns NaN
    ZVL256B, this PR 0
    ZVL128B, this PR 0

Disclosure: an AI coding agent (Anthropic Claude) prepared this patch, its
validation runs and this description on behalf of the account owner. All
numbers above come from archived runs on the board.

The scaled sum of squares divides by the running maximum. Once an infinite
element becomes that maximum, inf/inf = NaN enters the sum and the result is
NaN instead of +Inf (OpenBLAS's dnrm2_inf utest only covers an infinity among
zeros, where the division is skipped). When the result is NaN, rescan the
input: return a NaN element if there is one, +Inf otherwise. Finite inputs do
not take this path.
nrm2_rvv.c (already used by RISCV64_ZVL128B and x280) is 23%/9%/8% faster for
SNRM2 and 10%/6%/6% faster for DNRM2 than nrm2_vector.c on the SpacemiT X100
(n = 1024 / 32768 / 4M), and it propagates NaN inputs, which nrm2_vector.c can
drop.
@HiderX

HiderX commented Oct 26, 2026 •

Copy link
Copy Markdown
Author

An additional data point before you review this. I ran the netlib LAPACK tests (make lapack-test) on the same
board (SpacemiT K3, X100 cores, GCC and gfortran 15.2, NO_LAPACKE=1 USE_OPENMP=0).

build REAL DOUBLE COMPLEX COMPLEX16
RISCV64_ZVL256B, develop 0 0 0 0
RISCV64_ZVL256B, this PR 0 1 0 0
RISCV64_ZVL128B, develop 0 1 1 0

The DOUBLE failure is the same test in both builds. In xeigtstd < nep.in, DHS fails at order 16, type 18, with
result 11 (the inverse-iteration residual) = 1.787e8. On develop, RISCV64_ZVL128B already uses nrm2_rvv.c for
DNRM2. This PR therefore carries that existing failure over to ZVL256B together with the kernel. No routine returns
INFO ≠ 0. The ZVL128B COMPLEX failure is a CHS case in xeigtstc, unrelated to this PR.

The cause looks like sensitivity to rounding in the Householder norms, rather than a wrong norm:

  • I also tried a rewrite of nrm2_rvv.c that is much faster and more accurate against a binary128 reference. With
    it, this DHS case passes on both targets.
  • A different marginal case then appears on ZVL256B: SBD test 9 = 54.8, against a threshold of 50.

Options: I can reduce this PR to the Inf fix in nrm2_rvv.c and leave ZVL256B on nrm2_vector.c, or keep it as
it is. Which would you prefer?

Update. I checked a DNRM2-only version of that rewrite, which leaves SNRM2 as in this PR, on top of this PR.

  • LAPACK testing on RISCV64_ZVL256B then gives 0 failures in all four precisions, the same as develop.
  • On RISCV64_ZVL128B the DHS case above passes, leaving only develop's existing CHS case.
  • DNRM2 becomes 4.5–42× faster than this PR's kernel.

I can push it to this PR or open it separately after this one, whichever you prefer.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant