Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ hardware for each kernel: Zen 3, Neoverse N1, SpacemiT X60, Loongson
go-asmgen pins against GNU as and llvm-mc. A sabotaged one (`xvsubsp`) failed
the bit-identity test on the POWER9.

**2026-10-18 (v0.17.0).** The s390x block kernel (`blocks8VXE`) follows the
same guards: `AddBlocks8F32`'s length check, the wrapper's touch of the last
element, and the `useVXE` gate, so a CPU without vector-enhancements facility
1 never reaches `vfasb`. On an IBM Z (LinuxONE) the PROT_NONE fence test and
the whole suite pass, and a sabotaged kernel (`vfssb`) failed the bit-identity
test. Its `vfasb` is `WORD`+`BYTE`+`BYTE`, pinned in go-asmgen against GNU as
and llvm-mc and executed bit-exact on that machine.

**2026-10-17 (after v0.12.0, tests only).** v0.10.0 added thirteen dtypes,
and `FuzzOps` builds only float64 arrays; `values.json` checks every
operation on operands of one dtype, so the values of a mixed-dtype operation
Expand Down
21 changes: 18 additions & 3 deletions docs/perf.md
Original file line number Diff line number Diff line change
Expand Up @@ -910,9 +910,24 @@ tests passed on the POWER9, and a sabotaged kernel (one `xvsubsp`) failed them.
|---|---:|---|
| POWER9 (cfarm29) | −36% / −48% | float64, int32 ~ |

Left: s390x runs the scalar block loop. Go's assembler lacks `vfasb`
(vector-enhancements facility 1, z14), which [CL 846286](https://go.dev/cl/846286)
adds, and go-asmgen's registry has no s390x entries yet to pin its encoding.
**s390x (2026-10-18).** Go's assembler lacks `vfasb` (vector-enhancements
facility 1, z14), which [CL 846286](https://go.dev/cl/846286) adds. go-asmgen
v0.19.0 emits it as `WORD`+`BYTE`+`BYTE`, the 6 bytes pinned against GNU as
(go-asmgen/asmgen#29). The eight accumulators sit in two vector registers;
`VL`/`VST` keep memory order on this big-endian machine. The kernel runs
behind the `useVXE` gate of max/min, so a z13 takes the scalar loop. The
kernel tests passed on the z machine, and a sabotaged kernel (one `vfssb`)
failed them. Same A/B protocol:

| float32 `Sum`, 2¹⁴ / 2²⁰ | change | controls |
|---|---:|---|
| IBM Z, LinuxONE (vxe, vxe2) | −73% / −78% | int32 ~; float64 ~ / +2.0% |

The float64 control at 2²⁰ moved +2.0% with its kernel byte-identical: the
change moved `sumVX` by 32 bytes (0x18f920 → 0x18f940). It is attributed to
layout, not proven.

Every 64-bit target now adds the float32 block with SIMD.

## SIMD coverage

Expand Down
2 changes: 1 addition & 1 deletion internal/kernels/asmgen/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ module github.com/go-ndarray/ndarray/internal/kernels/asmgen

go 1.27.2

require github.com/go-asmgen/asmgen v0.18.0
require github.com/go-asmgen/asmgen v0.19.0
4 changes: 2 additions & 2 deletions internal/kernels/asmgen/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/go-asmgen/asmgen v0.18.0 h1:fo2agcSeLGgS69guRwlL7V0kGN7FtkQAg/NhLI0fE+4=
github.com/go-asmgen/asmgen v0.18.0/go.mod h1:FHWXZt1MrejOwSShl4tPYjI2GPEfNAJuXJUX6Z+ruyQ=
github.com/go-asmgen/asmgen v0.19.0 h1:ESiyTkTS1KOi0uMsjmOo7pnBw+IOxvnGkTuVmipZmDo=
github.com/go-asmgen/asmgen v0.19.0/go.mod h1:FHWXZt1MrejOwSShl4tPYjI2GPEfNAJuXJUX6Z+ruyQ=
25 changes: 25 additions & 0 deletions internal/kernels/asmgen/s390x/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func main() {
f.Add(gemmKernel())
f.Add(extremumKernel("maxVXE", "VFMAXDB", "WFMAXDB"))
f.Add(extremumKernel("minVXE", "VFMINDB", "WFMINDB"))
f.Add(blocks8Kernel())
if err := os.WriteFile("sum_s390x.s", []byte(f.String()), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
Expand Down Expand Up @@ -216,3 +217,27 @@ func extremumKernel(name, vec, sca string) *emit.Function {
Ret()
return b.Func()
}

// blocks8Kernel builds blocks8VXE(r, a *float32, blocks int): for each of the
// blocks of eight float32 at a, in order, r[j] += a[8i+j] (NumPy's pairwise-sum
// block). The eight accumulators are split over two vector registers, V0 and
// V1 (two dependency chains, as on the other targets), and VFASB is one IEEE
// float32 addition per lane, so the result is the scalar loop's bit for bit.
// VL/VST keep memory order on this big-endian machine, so every float keeps
// its lane. VFASB is the vector-enhancements facility 1 (z14); Go's assembler
// lacks it, and go-asmgen emits it as WORD+BYTE+BYTE pinned against GNU as
// (the cmd/asm spelling is CL 846286). blocks >= 1.
func blocks8Kernel() *emit.Function {
sig := s390x.Layout([]string{"r", "a", "blocks"},
[]s390x.Type{s390x.Ptr, s390x.Ptr, s390x.Int64}, nil, nil)
b := s390x.NewFunc("blocks8VXE", sig, 0)
b.LoadArg("r", "R1").LoadArg("a", "R2").LoadArg("blocks", "R3").
Raw("VL 0(R1), V0").Raw("VL 16(R1), V1").
Raw("b8loop:").
Raw("VL 0(R2), V2").Raw("VL 16(R2), V3")
b.VFASB(0, 0, 2).VFASB(1, 1, 3)
b.Raw("ADD $32, R2").Raw("SUB $1, R3").Raw("CMPBNE R3, $0, b8loop").
Raw("VST V0, 0(R1)").Raw("VST V1, 16(R1)").
Ret()
return b.Func()
}
2 changes: 1 addition & 1 deletion internal/kernels/blocks8_generic.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !amd64 && !arm64 && !loong64 && !ppc64le && !riscv64
//go:build !amd64 && !arm64 && !loong64 && !ppc64le && !riscv64 && !s390x

package kernels

Expand Down
18 changes: 18 additions & 0 deletions internal/kernels/blocks8_s390x.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package kernels

// addBlocks8F32 runs the VXE kernel (sum_s390x.s), the eight accumulators in
// two vector registers, one VFASB per register per block, on a z14 or later
// (VFASB is vector-enhancements facility 1, the useVXE gate of max/min); the
// scalar loop otherwise. len(a) is a positive multiple of 8 (AddBlocks8F32
// checks).
func addBlocks8F32(r *[8]float32, a []float32) {
if !useVXE {
addBlocks8F32Go(r, a)
return
}
_ = a[len(a)-1] // the kernel reads len(a) of a
blocks8VXE(&r[0], &a[0], len(a)/8)
}

//go:noescape
func blocks8VXE(r, a *float32, blocks int)
18 changes: 18 additions & 0 deletions internal/kernels/sum_s390x.s
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,21 @@ TEXT ·minVXE(SB), NOSPLIT, $0-24
FMOVD F0, ret+16(FP)
RET

TEXT ·blocks8VXE(SB), NOSPLIT, $0-24
MOVD r+0(FP), R1
MOVD a+8(FP), R2
MOVD blocks+16(FP), R3
VL 0(R1), V0
VL 16(R1), V1
b8loop:
VL 0(R2), V2
VL 16(R2), V3
WORD $0xe7002000; BYTE $0x20; BYTE $0xe3 // VFASB V0, V2, V0
WORD $0xe7113000; BYTE $0x20; BYTE $0xe3 // VFASB V1, V3, V1
ADD $32, R2
SUB $1, R3
CMPBNE R3, $0, b8loop
VST V0, 0(R1)
VST V1, 16(R1)
RET

Loading