diff --git a/.github/workflows/build-pylance.yml b/.github/workflows/build-pylance.yml new file mode 100644 index 000000000..da8d5332d --- /dev/null +++ b/.github/workflows/build-pylance.yml @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: 2026 The RISE Project +# SPDX-License-Identifier: MIT +--- +# This workflow is based on the `linux` job of +# https://github.com/lancedb/lance/blob/v11.0.0/.github/workflows/pypi-publish.yml +name: Build pylance wheels (riscv64) + +on: + workflow_dispatch: + inputs: + version: + description: 'pylance version to build (git tag without the leading v, e.g. 11.0.0)' + required: true + default: '11.0.0' + pull_request: + paths: + - '.github/workflows/build-pylance.yml' + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version || '11.0.0' }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + # `inputs.version` is empty on pull_request events; default to 11.0.0 there. + PYLANCE_VERSION: ${{ inputs.version || '11.0.0' }} + MANYLINUX_RISCV64_IMAGE: quay.io/pypa/manylinux_2_39_riscv64 + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + +jobs: + setup: + uses: $/.github/workflows/_setup.yml + + build_wheels: + needs: [setup] + name: Build pylance ${{ inputs.version || '11.0.0' }} cp310-abi3-manylinux_riscv64 + runs-on: ubuntu-24.04-riscv + timeout-minutes: 1440 + + steps: + - name: Checkout lance v${{ env.PYLANCE_VERSION }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: lancedb/lance + ref: v${{ env.PYLANCE_VERSION }} + persist-credentials: false + + - name: Checkout python-wheels + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + path: python-wheels + sparse-checkout: patches/pylance + persist-credentials: false + + # lance-core's SIMD_SUPPORT already has a portable riscv64 fallback, but + # lance-linalg's f32x8/f32x16/i32x8/f64x4/f64x8 SIMD types still compile + # only for x86_64/aarch64/loongarch64 upstream (unlike u8x16/u8x32, which + # already have one); see patches/pylance/11.0.0/0001*.patch. + - name: Apply pylance patches + run: git apply -v python-wheels/patches/pylance/${{ env.PYLANCE_VERSION }}/0001*.patch + + - name: Free disk space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + + # Same OOM guard build-deltalake.yml/build-lancedb.yml need on these + # runners for a comparably sized Rust workspace (lance's own core, not + # just a downstream binding). + - name: Set swap space + uses: pierotofy/set-swap-space@fc79b3f67fa8a838184ce84a674ca12238d2c761 # master + with: + swap-size-gb: 16 + + - name: Build wheel + uses: pypa/cibuildwheel@1828c10ab37f080699c7b81cea34097c684a7074 # v4.2.0 + with: + package-dir: python + output-dir: wheelhouse/ + # pyo3 carries `abi3-py310` unconditionally in python/Cargo.toml, so + # upstream ships one cp310-abi3 wheel and no free-threaded variant + # (its own linux-wheel job only builds cp310 too): one build, on our + # minimum interpreter. musllinux is dropped: rustup.rs ships no + # riscv64 musl toolchain. + only: cp312-manylinux_riscv64 + env: + CIBW_MANYLINUX_RISCV64_IMAGE: ${{ env.MANYLINUX_RISCV64_IMAGE }} + # lance ships no [tool.cibuildwheel], so the Rust toolchain its + # maturin backend needs is installed in-container here. Rocky 10's + # CRB repo (enabled in the manylinux image) has protoc; PROTOC_INCLUDE + # is needed because it resolves google/protobuf/*.proto from disk + # rather than from the binary (same as build-lancedb.yml). + CIBW_BEFORE_ALL_LINUX: >- + yum install -y protobuf-compiler protobuf-devel && + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + # python/pyproject.toml leaves `[tool.maturin]` at the default `dev` + # profile, so a plain PEP 517 build ships an unoptimised wheel unless + # `--profile release` is passed explicitly (gotcha 141; the + # deltalake case). python/.cargo/config.toml already pins + # `[profile.release]` to thin LTO; only codegen-units is raised from + # 1 to 16 here, the same compile-time/runtime tradeoff upstream's own + # `release-with-debug`/`bench` profiles make, needed for this + # workspace's size on the riscv64 runner (same reasoning as + # build-deltalake.yml/build-lancedb.yml). + CIBW_ENVIRONMENT_LINUX: >- + PATH="$PATH:$HOME/.cargo/bin" + PROTOC=/usr/bin/protoc + PROTOC_INCLUDE=/usr/include + MATURIN_PEP517_ARGS="--profile release --strip" + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 + CARGO_BUILD_JOBS=2 + PIP_EXTRA_INDEX_URL=https://pypi.riseproject.dev/simple/ + # Without this pip prefers PyPI's releases, which have no riscv64 + # wheel for these, and source-builds them in the container. ml_dtypes + # has no riscv64 wheel anywhere and is left off this list so pip + # builds it from its (pure C++) sdist instead. + CIBW_TEST_ENVIRONMENT: >- + PIP_ONLY_BINARY=numpy,pandas,pyarrow,duckdb,psutil,pillow,torch + CIBW_TEST_REQUIRES: >- + pytest pytest-xdist numpy pandas pyarrow duckdb + polars[pyarrow,pandas] psutil pillow ml_dtypes torch lance_namespace + CIBW_TEST_SOURCES: python/pyproject.toml python/python/tests + # cibuildwheel runs CIBW_TEST_COMMAND from the parent of the copied + # CIBW_TEST_SOURCES tree, one level above python/pyproject.toml; + # pytest's rootdir/inifile search only looks upward from the cwd, so + # `cd python` first puts pyproject.toml back in the search path + # (same fix as build-lancedb.yml, gotcha discovered there). Mirrors + # upstream's own `make test` (Makefile: `pytest -vvv -s + # --durations=30 python/tests`, `-n auto --dist loadgroup` from + # run_tests/action.yml's default `pytest-workers: auto`). + CIBW_TEST_COMMAND: >- + cd python && + python -m pytest python/tests -vvv -s --durations=30 + -n auto --dist loadgroup + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pylance-${{ env.PYLANCE_VERSION }}-cp310-abi3-manylinux_riscv64 + path: wheelhouse/*.whl + if-no-files-found: error + + publish: + name: Publish pylance ${{ inputs.version || '11.0.0' }} + needs: [setup, build_wheels] + permissions: + contents: write + pull-requests: write + uses: $/.github/workflows/_publish-wheel.yml + with: + artifact-pattern: pylance-${{ inputs.version || '11.0.0' }}-*-manylinux_riscv64 diff --git a/patches/pylance/11.0.0/0001-lance-linalg-add-a-portable-riscv64-simd-fallback.patch b/patches/pylance/11.0.0/0001-lance-linalg-add-a-portable-riscv64-simd-fallback.patch new file mode 100644 index 000000000..0708da295 --- /dev/null +++ b/patches/pylance/11.0.0/0001-lance-linalg-add-a-portable-riscv64-simd-fallback.patch @@ -0,0 +1,1292 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Tue, 8 Sep 2026 00:00:00 +0000 +Subject: [PATCH] lance-linalg: add a portable riscv64 SIMD fallback + +lance-core's SIMD_SUPPORT static already carries a portable riscv64 +fallback (rust/lance-core/src/utils/cpu.rs), but lance-linalg's +f32x8/f32x16/i32x8/f64x4/f64x8 SIMD types still hardcode x86_64 +(AVX/AVX2/AVX-512), aarch64 (NEON) and loongarch64 (LSX/LASX) as the +only supported architectures, with no portable fallback -- unlike +u8x16/u8x32, which already carry one (rust/lance-linalg/src/simd/u8.rs). +This adds the same shape of fallback for the missing types: an +array-backed struct plus a scalar implementation of every method the +SIMD/FloatSimd traits require, gated +`#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64")))]` +beside the existing per-architecture arms, which are otherwise +untouched. Add/Sub/Mul on i32x8 use wrapping arithmetic to match the +silent-wraparound semantics of the SIMD instructions they replace. + +Verified with `cargo check --target riscv64gc-unknown-linux-gnu` (and, +as a regression check, `--target aarch64-apple-darwin` and +`--target x86_64-unknown-linux-gnu`) against an extracted copy of +lance-linalg's simd module. + +Upstream-Status: To upstream [not yet submitted to lancedb/lance] + +Signed-off-by: Ludovic Henry +--- + rust/lance-linalg/src/simd/f32.rs | 383 ++++++++++++++++++++++++++++++ + rust/lance-linalg/src/simd/f64.rs | 364 ++++++++++++++++++++++++++++ + rust/lance-linalg/src/simd/i32.rs | 196 +++++++++++++++ + 3 files changed, 943 insertions(+) + +diff --git a/rust/lance-linalg/src/simd/f32.rs b/rust/lance-linalg/src/simd/f32.rs +index 434a1ef..141185d 100644 +--- a/rust/lance-linalg/src/simd/f32.rs ++++ b/rust/lance-linalg/src/simd/f32.rs +@@ -35,6 +35,15 @@ pub struct f32x8(float32x4x2_t); + #[derive(Clone, Copy)] + pub struct f32x8(v8f32); + ++#[allow(non_camel_case_types)] ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++#[derive(Clone, Copy)] ++pub struct f32x8([f32; 8]); ++ + impl std::fmt::Debug for f32x8 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut arr = [0.0_f32; 8]; +@@ -45,6 +54,7 @@ impl std::fmt::Debug for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl f32x8 { + /// Gather 8 f32 values from `slice` at the offsets in `indices`. + /// +@@ -163,6 +173,7 @@ impl<'a> From<&'a [f32; 8]> for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SIMD for f32x8 { + fn splat(val: f32) -> Self { + #[cfg(target_arch = "x86_64")] +@@ -371,6 +382,7 @@ impl SIMD for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl FloatSimd for f32x8 { + fn multiply_add(&mut self, a: Self, b: Self) { + #[cfg(target_arch = "x86_64")] +@@ -389,6 +401,7 @@ impl FloatSimd for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Add for f32x8 { + type Output = Self; + +@@ -412,6 +425,7 @@ impl Add for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl AddAssign for f32x8 { + #[inline] + fn add_assign(&mut self, rhs: Self) { +@@ -431,6 +445,7 @@ impl AddAssign for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Sub for f32x8 { + type Output = Self; + +@@ -454,6 +469,7 @@ impl Sub for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SubAssign for f32x8 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { +@@ -473,6 +489,7 @@ impl SubAssign for f32x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Mul for f32x8 { + type Output = Self; + +@@ -521,6 +538,15 @@ pub struct f32x16(float32x4x4_t); + #[derive(Clone, Copy)] + pub struct f32x16(v8f32, v8f32); + ++#[allow(non_camel_case_types)] ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++#[derive(Clone, Copy)] ++pub struct f32x16([f32; 16]); ++ + impl std::fmt::Debug for f32x16 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut arr = [0.0_f32; 16]; +@@ -548,6 +574,7 @@ impl<'a> From<&'a [f32; 16]> for f32x16 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SIMD for f32x16 { + #[inline] + fn splat(val: f32) -> Self { +@@ -795,6 +822,7 @@ impl SIMD for f32x16 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl FloatSimd for f32x16 { + #[inline] + fn multiply_add(&mut self, a: Self, b: Self) { +@@ -818,6 +846,7 @@ impl FloatSimd for f32x16 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Add for f32x16 { + type Output = Self; + +@@ -843,6 +872,7 @@ impl Add for f32x16 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl AddAssign for f32x16 { + #[inline] + fn add_assign(&mut self, rhs: Self) { +@@ -866,6 +896,7 @@ impl AddAssign for f32x16 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Mul for f32x16 { + type Output = Self; + +@@ -891,6 +922,7 @@ impl Mul for f32x16 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Sub for f32x16 { + type Output = Self; + +@@ -916,6 +948,7 @@ impl Sub for f32x16 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SubAssign for f32x16 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { +@@ -939,6 +972,356 @@ impl SubAssign for f32x16 { + } + } + ++ ++// --------------------------------------------------------------------------- ++// Portable scalar fallback for architectures with no dedicated SIMD kernel ++// above (e.g. riscv64). Correctness-first, no intrinsics. ++// --------------------------------------------------------------------------- ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl f32x8 { ++ /// Gather 8 f32 values from `slice` at the offsets in `indices`. ++ /// ++ /// # Panics ++ /// ++ /// If any index is negative or lands outside `slice`. ++ #[inline] ++ pub fn gather(slice: &[f32], indices: &[i32; 8]) -> Self { ++ let values = indices.map(|i| slice[i as usize]); ++ unsafe { Self::load_unaligned(values.as_ptr()) } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SIMD for f32x8 { ++ #[inline] ++ fn splat(val: f32) -> Self { ++ Self([val; 8]) ++ } ++ ++ #[inline] ++ fn zeros() -> Self { ++ Self([0.0; 8]) ++ } ++ ++ #[inline] ++ unsafe fn load(ptr: *const f32) -> Self { ++ unsafe { Self::load_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn load_unaligned(ptr: *const f32) -> Self { ++ let mut arr = [0.0_f32; 8]; ++ unsafe { ++ std::ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 8); ++ } ++ Self(arr) ++ } ++ ++ #[inline] ++ unsafe fn store(&self, ptr: *mut f32) { ++ unsafe { self.store_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn store_unaligned(&self, ptr: *mut f32) { ++ unsafe { ++ std::ptr::copy_nonoverlapping(self.0.as_ptr(), ptr, 8); ++ } ++ } ++ ++ #[inline] ++ fn reduce_sum(&self) -> f32 { ++ self.0.iter().sum() ++ } ++ ++ #[inline] ++ fn reduce_min(&self) -> f32 { ++ self.0.iter().copied().fold(f32::INFINITY, f32::min) ++ } ++ ++ #[inline] ++ fn min(&self, rhs: &Self) -> Self { ++ let mut result = [0.0_f32; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i].min(rhs.0[i]); ++ } ++ Self(result) ++ } ++ ++ #[inline] ++ fn find(&self, val: f32) -> Option { ++ self.0.iter().position(|&x| x == val).map(|i| i as i32) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl FloatSimd for f32x8 { ++ #[inline] ++ fn multiply_add(&mut self, a: Self, b: Self) { ++ for i in 0..8 { ++ self.0[i] += a.0[i] * b.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Add for f32x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn add(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f32; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i] + rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl AddAssign for f32x8 { ++ #[inline] ++ fn add_assign(&mut self, rhs: Self) { ++ for i in 0..8 { ++ self.0[i] += rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Sub for f32x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn sub(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f32; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i] - rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SubAssign for f32x8 { ++ #[inline] ++ fn sub_assign(&mut self, rhs: Self) { ++ for i in 0..8 { ++ self.0[i] -= rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Mul for f32x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn mul(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f32; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i] * rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SIMD for f32x16 { ++ #[inline] ++ fn splat(val: f32) -> Self { ++ Self([val; 16]) ++ } ++ ++ #[inline] ++ fn zeros() -> Self { ++ Self([0.0; 16]) ++ } ++ ++ #[inline] ++ unsafe fn load(ptr: *const f32) -> Self { ++ unsafe { Self::load_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn load_unaligned(ptr: *const f32) -> Self { ++ let mut arr = [0.0_f32; 16]; ++ unsafe { ++ std::ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 16); ++ } ++ Self(arr) ++ } ++ ++ #[inline] ++ unsafe fn store(&self, ptr: *mut f32) { ++ unsafe { self.store_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn store_unaligned(&self, ptr: *mut f32) { ++ unsafe { ++ std::ptr::copy_nonoverlapping(self.0.as_ptr(), ptr, 16); ++ } ++ } ++ ++ #[inline] ++ fn reduce_sum(&self) -> f32 { ++ self.0.iter().sum() ++ } ++ ++ #[inline] ++ fn reduce_min(&self) -> f32 { ++ self.0.iter().copied().fold(f32::INFINITY, f32::min) ++ } ++ ++ #[inline] ++ fn min(&self, rhs: &Self) -> Self { ++ let mut result = [0.0_f32; 16]; ++ for i in 0..16 { ++ result[i] = self.0[i].min(rhs.0[i]); ++ } ++ Self(result) ++ } ++ ++ #[inline] ++ fn find(&self, val: f32) -> Option { ++ self.0.iter().position(|&x| x == val).map(|i| i as i32) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl FloatSimd for f32x16 { ++ #[inline] ++ fn multiply_add(&mut self, a: Self, b: Self) { ++ for i in 0..16 { ++ self.0[i] += a.0[i] * b.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Add for f32x16 { ++ type Output = Self; ++ ++ #[inline] ++ fn add(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f32; 16]; ++ for i in 0..16 { ++ result[i] = self.0[i] + rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl AddAssign for f32x16 { ++ #[inline] ++ fn add_assign(&mut self, rhs: Self) { ++ for i in 0..16 { ++ self.0[i] += rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Sub for f32x16 { ++ type Output = Self; ++ ++ #[inline] ++ fn sub(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f32; 16]; ++ for i in 0..16 { ++ result[i] = self.0[i] - rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SubAssign for f32x16 { ++ #[inline] ++ fn sub_assign(&mut self, rhs: Self) { ++ for i in 0..16 { ++ self.0[i] -= rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Mul for f32x16 { ++ type Output = Self; ++ ++ #[inline] ++ fn mul(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f32; 16]; ++ for i in 0..16 { ++ result[i] = self.0[i] * rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ + #[cfg(test)] + mod tests { + +diff --git a/rust/lance-linalg/src/simd/f64.rs b/rust/lance-linalg/src/simd/f64.rs +index 129b2f0..a2b5f02 100644 +--- a/rust/lance-linalg/src/simd/f64.rs ++++ b/rust/lance-linalg/src/simd/f64.rs +@@ -33,6 +33,15 @@ pub struct f64x4(float64x2x2_t); + #[derive(Clone, Copy)] + pub struct f64x4(v4f64); + ++#[allow(non_camel_case_types)] ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++#[derive(Clone, Copy)] ++pub struct f64x4([f64; 4]); ++ + impl std::fmt::Debug for f64x4 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut arr = [0.0_f64; 4]; +@@ -60,6 +69,7 @@ impl<'a> From<&'a [f64; 4]> for f64x4 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SIMD for f64x4 { + fn splat(val: f64) -> Self { + #[cfg(target_arch = "x86_64")] +@@ -231,6 +241,7 @@ impl SIMD for f64x4 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl FloatSimd for f64x4 { + fn multiply_add(&mut self, a: Self, b: Self) { + #[cfg(target_arch = "x86_64")] +@@ -249,6 +260,7 @@ impl FloatSimd for f64x4 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Add for f64x4 { + type Output = Self; + +@@ -272,6 +284,7 @@ impl Add for f64x4 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl AddAssign for f64x4 { + #[inline] + fn add_assign(&mut self, rhs: Self) { +@@ -291,6 +304,7 @@ impl AddAssign for f64x4 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Sub for f64x4 { + type Output = Self; + +@@ -314,6 +328,7 @@ impl Sub for f64x4 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SubAssign for f64x4 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { +@@ -333,6 +348,7 @@ impl SubAssign for f64x4 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Mul for f64x4 { + type Output = Self; + +@@ -382,6 +398,15 @@ pub struct f64x8(float64x2x2_t, float64x2x2_t); + #[derive(Clone, Copy)] + pub struct f64x8(v4f64, v4f64); + ++#[allow(non_camel_case_types)] ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++#[derive(Clone, Copy)] ++pub struct f64x8([f64; 8]); ++ + impl std::fmt::Debug for f64x8 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut arr = [0.0_f64; 8]; +@@ -409,6 +434,7 @@ impl<'a> From<&'a [f64; 8]> for f64x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SIMD for f64x8 { + #[inline] + fn splat(val: f64) -> Self { +@@ -602,6 +628,7 @@ impl SIMD for f64x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl FloatSimd for f64x8 { + #[inline] + fn multiply_add(&mut self, a: Self, b: Self) { +@@ -625,6 +652,7 @@ impl FloatSimd for f64x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Add for f64x8 { + type Output = Self; + +@@ -648,6 +676,7 @@ impl Add for f64x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl AddAssign for f64x8 { + #[inline] + fn add_assign(&mut self, rhs: Self) { +@@ -671,6 +700,7 @@ impl AddAssign for f64x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Mul for f64x8 { + type Output = Self; + +@@ -694,6 +724,7 @@ impl Mul for f64x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl Sub for f64x8 { + type Output = Self; + +@@ -717,6 +748,7 @@ impl Sub for f64x8 { + } + } + ++#[cfg(any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "loongarch64"))] + impl SubAssign for f64x8 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { +@@ -740,6 +772,338 @@ impl SubAssign for f64x8 { + } + } + ++ ++// --------------------------------------------------------------------------- ++// Portable scalar fallback for architectures with no dedicated SIMD kernel ++// above (e.g. riscv64). Correctness-first, no intrinsics. ++// --------------------------------------------------------------------------- ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SIMD for f64x4 { ++ #[inline] ++ fn splat(val: f64) -> Self { ++ Self([val; 4]) ++ } ++ ++ #[inline] ++ fn zeros() -> Self { ++ Self([0.0; 4]) ++ } ++ ++ #[inline] ++ unsafe fn load(ptr: *const f64) -> Self { ++ unsafe { Self::load_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn load_unaligned(ptr: *const f64) -> Self { ++ let mut arr = [0.0_f64; 4]; ++ unsafe { ++ std::ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 4); ++ } ++ Self(arr) ++ } ++ ++ #[inline] ++ unsafe fn store(&self, ptr: *mut f64) { ++ unsafe { self.store_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn store_unaligned(&self, ptr: *mut f64) { ++ unsafe { ++ std::ptr::copy_nonoverlapping(self.0.as_ptr(), ptr, 4); ++ } ++ } ++ ++ #[inline] ++ fn reduce_sum(&self) -> f64 { ++ self.0.iter().sum() ++ } ++ ++ #[inline] ++ fn reduce_min(&self) -> f64 { ++ self.0.iter().copied().fold(f64::INFINITY, f64::min) ++ } ++ ++ #[inline] ++ fn min(&self, rhs: &Self) -> Self { ++ let mut result = [0.0_f64; 4]; ++ for i in 0..4 { ++ result[i] = self.0[i].min(rhs.0[i]); ++ } ++ Self(result) ++ } ++ ++ #[inline] ++ fn find(&self, val: f64) -> Option { ++ self.0.iter().position(|&x| x == val).map(|i| i as i32) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl FloatSimd for f64x4 { ++ #[inline] ++ fn multiply_add(&mut self, a: Self, b: Self) { ++ for i in 0..4 { ++ self.0[i] += a.0[i] * b.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Add for f64x4 { ++ type Output = Self; ++ ++ #[inline] ++ fn add(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f64; 4]; ++ for i in 0..4 { ++ result[i] = self.0[i] + rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl AddAssign for f64x4 { ++ #[inline] ++ fn add_assign(&mut self, rhs: Self) { ++ for i in 0..4 { ++ self.0[i] += rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Sub for f64x4 { ++ type Output = Self; ++ ++ #[inline] ++ fn sub(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f64; 4]; ++ for i in 0..4 { ++ result[i] = self.0[i] - rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SubAssign for f64x4 { ++ #[inline] ++ fn sub_assign(&mut self, rhs: Self) { ++ for i in 0..4 { ++ self.0[i] -= rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Mul for f64x4 { ++ type Output = Self; ++ ++ #[inline] ++ fn mul(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f64; 4]; ++ for i in 0..4 { ++ result[i] = self.0[i] * rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SIMD for f64x8 { ++ #[inline] ++ fn splat(val: f64) -> Self { ++ Self([val; 8]) ++ } ++ ++ #[inline] ++ fn zeros() -> Self { ++ Self([0.0; 8]) ++ } ++ ++ #[inline] ++ unsafe fn load(ptr: *const f64) -> Self { ++ unsafe { Self::load_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn load_unaligned(ptr: *const f64) -> Self { ++ let mut arr = [0.0_f64; 8]; ++ unsafe { ++ std::ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 8); ++ } ++ Self(arr) ++ } ++ ++ #[inline] ++ unsafe fn store(&self, ptr: *mut f64) { ++ unsafe { self.store_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn store_unaligned(&self, ptr: *mut f64) { ++ unsafe { ++ std::ptr::copy_nonoverlapping(self.0.as_ptr(), ptr, 8); ++ } ++ } ++ ++ #[inline] ++ fn reduce_sum(&self) -> f64 { ++ self.0.iter().sum() ++ } ++ ++ #[inline] ++ fn reduce_min(&self) -> f64 { ++ self.0.iter().copied().fold(f64::INFINITY, f64::min) ++ } ++ ++ #[inline] ++ fn min(&self, rhs: &Self) -> Self { ++ let mut result = [0.0_f64; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i].min(rhs.0[i]); ++ } ++ Self(result) ++ } ++ ++ #[inline] ++ fn find(&self, val: f64) -> Option { ++ self.0.iter().position(|&x| x == val).map(|i| i as i32) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl FloatSimd for f64x8 { ++ #[inline] ++ fn multiply_add(&mut self, a: Self, b: Self) { ++ for i in 0..8 { ++ self.0[i] += a.0[i] * b.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Add for f64x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn add(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f64; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i] + rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl AddAssign for f64x8 { ++ #[inline] ++ fn add_assign(&mut self, rhs: Self) { ++ for i in 0..8 { ++ self.0[i] += rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Sub for f64x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn sub(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f64; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i] - rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SubAssign for f64x8 { ++ #[inline] ++ fn sub_assign(&mut self, rhs: Self) { ++ for i in 0..8 { ++ self.0[i] -= rhs.0[i]; ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Mul for f64x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn mul(self, rhs: Self) -> Self::Output { ++ let mut result = [0.0_f64; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i] * rhs.0[i]; ++ } ++ Self(result) ++ } ++} ++ + #[cfg(test)] + mod tests { + use super::*; +diff --git a/rust/lance-linalg/src/simd/i32.rs b/rust/lance-linalg/src/simd/i32.rs +index 6e08129..0b6dc14 100644 +--- a/rust/lance-linalg/src/simd/i32.rs ++++ b/rust/lance-linalg/src/simd/i32.rs +@@ -38,6 +38,17 @@ pub struct i32x8(int32x4x2_t); + #[derive(Clone, Copy)] + pub struct i32x8(v8i32); + ++/// Portable scalar fallback for architectures with no dedicated SIMD kernel ++/// above (e.g. riscv64). Correctness-first, no intrinsics. ++#[allow(non_camel_case_types)] ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++#[derive(Clone, Copy)] ++pub struct i32x8([i32; 8]); ++ + impl std::fmt::Debug for i32x8 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut arr = [0; 8]; +@@ -65,6 +76,11 @@ impl From<&[i32; 8]> for i32x8 { + } + } + ++#[cfg(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++))] + impl SIMD for i32x8 { + #[inline] + fn splat(val: i32) -> Self { +@@ -223,6 +239,11 @@ impl SIMD for i32x8 { + } + } + ++#[cfg(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++))] + impl Add for i32x8 { + type Output = Self; + +@@ -246,6 +267,11 @@ impl Add for i32x8 { + } + } + ++#[cfg(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++))] + impl AddAssign for i32x8 { + #[inline] + fn add_assign(&mut self, rhs: Self) { +@@ -265,6 +291,11 @@ impl AddAssign for i32x8 { + } + } + ++#[cfg(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++))] + impl Sub for i32x8 { + type Output = Self; + +@@ -288,6 +319,11 @@ impl Sub for i32x8 { + } + } + ++#[cfg(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++))] + impl SubAssign for i32x8 { + #[inline] + fn sub_assign(&mut self, rhs: Self) { +@@ -307,6 +343,11 @@ impl SubAssign for i32x8 { + } + } + ++#[cfg(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++))] + impl Mul for i32x8 { + type Output = Self; + +@@ -343,6 +384,161 @@ impl Mul for i32x8 { + } + } + ++// Portable scalar fallback for architectures with no dedicated SIMD kernel ++// above (e.g. riscv64). Correctness-first, no intrinsics. Add/Sub/Mul use ++// wrapping arithmetic to mirror the silent-wraparound semantics of the ++// `_mm256_{add,sub,mul}_epi32` / `vaddq_s32` / `lasx_xv{add,sub,mul}_w` ++// instructions above. ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SIMD for i32x8 { ++ #[inline] ++ fn splat(val: i32) -> Self { ++ Self([val; 8]) ++ } ++ ++ #[inline] ++ fn zeros() -> Self { ++ Self([0; 8]) ++ } ++ ++ #[inline] ++ unsafe fn load(ptr: *const i32) -> Self { ++ unsafe { Self::load_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn load_unaligned(ptr: *const i32) -> Self { ++ let mut arr = [0_i32; 8]; ++ unsafe { ++ std::ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 8); ++ } ++ Self(arr) ++ } ++ ++ #[inline] ++ unsafe fn store(&self, ptr: *mut i32) { ++ unsafe { self.store_unaligned(ptr) } ++ } ++ ++ #[inline] ++ unsafe fn store_unaligned(&self, ptr: *mut i32) { ++ unsafe { ++ std::ptr::copy_nonoverlapping(self.0.as_ptr(), ptr, 8); ++ } ++ } ++ ++ #[inline] ++ fn reduce_sum(&self) -> i32 { ++ self.0.iter().sum() ++ } ++ ++ #[inline] ++ fn reduce_min(&self) -> i32 { ++ todo!() ++ } ++ ++ #[inline] ++ fn min(&self, rhs: &Self) -> Self { ++ let mut result = [0_i32; 8]; ++ for i in 0..8 { ++ result[i] = std::cmp::min(self.0[i], rhs.0[i]); ++ } ++ Self(result) ++ } ++ ++ #[inline] ++ fn find(&self, val: i32) -> Option { ++ self.0.iter().position(|&x| x == val).map(|i| i as i32) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Add for i32x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn add(self, rhs: Self) -> Self::Output { ++ let mut result = [0_i32; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i].wrapping_add(rhs.0[i]); ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl AddAssign for i32x8 { ++ #[inline] ++ fn add_assign(&mut self, rhs: Self) { ++ for i in 0..8 { ++ self.0[i] = self.0[i].wrapping_add(rhs.0[i]); ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Sub for i32x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn sub(self, rhs: Self) -> Self::Output { ++ let mut result = [0_i32; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i].wrapping_sub(rhs.0[i]); ++ } ++ Self(result) ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl SubAssign for i32x8 { ++ #[inline] ++ fn sub_assign(&mut self, rhs: Self) { ++ for i in 0..8 { ++ self.0[i] = self.0[i].wrapping_sub(rhs.0[i]); ++ } ++ } ++} ++ ++#[cfg(not(any( ++ target_arch = "x86_64", ++ target_arch = "aarch64", ++ target_arch = "loongarch64" ++)))] ++impl Mul for i32x8 { ++ type Output = Self; ++ ++ #[inline] ++ fn mul(self, rhs: Self) -> Self::Output { ++ let mut result = [0_i32; 8]; ++ for i in 0..8 { ++ result[i] = self.0[i].wrapping_mul(rhs.0[i]); ++ } ++ Self(result) ++ } ++} ++ + #[cfg(test)] + mod tests { + use super::*; +-- +2.50.1 (Apple Git-155) +