From 7b6ee0d6550c74d16984670f0ad6a65a5c7e305d Mon Sep 17 00:00:00 2001 From: Tobias <5562156+tobocop2@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:54:42 -0400 Subject: [PATCH] feat(wheels): publish lancedb-compat for pre-Haswell x86_64 hosts (#3327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracks #3324. On x86_64 CPUs without AVX2 (Sandy Bridge / Ivy Bridge / Westmere on Intel; Bulldozer / Piledriver / Steamroller on AMD), `import lancedb` SIGILLs because the wheel bakes AVX2 + FMA into every compiled function. Per [westonpace's review](https://github.com/lancedb/lancedb/issues/3324#issuecomment-4328944354), the default `lancedb` wheel stays fast; pre-Haswell users get a separately-published `lancedb-compat` wheel. ## Summary - Adds a `lancedb-compat` matrix entry to `pypi-publish.yml` that builds with `RUSTFLAGS="-C target-cpu=x86-64-v2"` (Nehalem-class baseline). Same Python API (`import lancedb` works) — files install to the same namespace, so the two wheels conflict at install time and users pick one. Same pattern as `psycopg2` / `psycopg2-binary` and `tensorflow` / `tensorflow-cpu`. - Generalizes `build_linux_wheel` and `upload_wheel` composites with optional `package-name` and `rustflags` inputs (defaults preserve the existing 4 `lancedb` matrix entries verbatim). - Documents the choice in `python/README.md`: `pip install lancedb-compat` for pre-Haswell hosts. The default `.cargo/config.toml` baseline is unchanged. ## Sequencing 1. ~~lance-format/lance#6630 merges → runtime SIMD dispatch lands in lance.~~ **Done — merged.** 2. lancedb's lance dep is bumped to a release that includes it (separate PR / normal cadence). 3. This PR's `lancedb-compat` wheel build path starts producing a wheel that runs on pre-Haswell hardware. **Maintainer setup**: register `lancedb-compat` on PyPI and configure trusted publishing. ## Verified end-to-end on Sandy Bridge Xeon E5-2609 Verification was done locally against a fork-pinned lance dep that includes the runtime dispatch implementation, using the same `RUSTFLAGS="-C target-cpu=x86-64-v2"` flags this PR uses in CI: ``` $ RUSTFLAGS="-C target-cpu=x86-64-v2" maturin build --release $ pip install ./target/wheels/lancedb-*.whl $ python verify.py PASS: import + simd dispatch + table create + vector search all work. ``` Pre-fix on the same CPU (default `pip install lancedb`): `Illegal instruction (core dumped)`. Full reproducer (deps + clone + build + verification): https://gist.github.com/tobocop2/2e341358b55c143527416edfdb1e37df. Fork-internal verification PR with the dep bump and full logs: [`tobocop2/lancedb#2`](https://github.com/tobocop2/lancedb/pull/2). ## Benchmarks — no regressions on modern CPUs from the lance-side change These are the numbers I ran for the lance PR, confirming the runtime dispatch doesn't slow down the default (`target-cpu=haswell`) wheel that existing users install. Criterion, one machine, one session, base → PR, no `RUSTFLAGS` override. Full methodology, null experiments, and logs: [lance-format/lance#6630 benchmark comment](https://github.com/lance-format/lance/pull/6630#issuecomment-4933063394) and the [logs gist](https://gist.github.com/tobocop2/3c6d0f449cbd736aa2501f89a7fe56a2). | benchmark | EPYC 7B13 (`avx2`, `fma`, no `avx512f`) | Xeon Cascade Lake (`avx512f`) | |---|---|---| | `Cosine(f32, scalar)` *(control)* | +0.04% | +0.09% | | `Cosine(f64, scalar)` | −0.34% | −1.94% | | `Cosine(u8, SIMD)` | +2.30% | +3.63% | | `Dot(f16, SIMD)` | −0.58% | +0.61% | | `Dot(f32, SIMD)` | +0.34% | **−6.08%** | | `Dot(f32, arrow_arity)` | +0.02% | −0.00% | | `L2(f32, scalar)` | −0.10% | −0.02% | | `L2(f32, simd)` (dim 1024) | +2.63% | −0.53% | | **`L2(simd,f32x8)` (dim 8)** | **−45.9%** | **−25.1%** | | `L2(u8, SIMD)` | +0.42% | −3.11% | | `NormL2(f32, SIMD)` | −1.02% | −4.17% | | `NormL2(f64, SIMD)` | +3.51% | −0.58% | Nothing regresses beyond the noise floor. Dim 8 — the PQ sub-vector width — improves 25–46%. --- To be transparent: this isn't my domain of expertise and the lance-side implementation is AI-generated. I verified it works end-to-end on the failing hardware. Happy to roll in feedback. --- .../workflows/build_linux_wheel/action.yml | 24 +++++++++++++++-- .github/workflows/pypi-publish.yml | 27 ++++++++++++++++--- python/README.md | 21 +++++++++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_linux_wheel/action.yml b/.github/workflows/build_linux_wheel/action.yml index b43f27354..8fa5b1f79 100644 --- a/.github/workflows/build_linux_wheel/action.yml +++ b/.github/workflows/build_linux_wheel/action.yml @@ -18,6 +18,14 @@ inputs: description: "The manylinux version to build for" required: false default: "2_17" + package-name: + description: "Override [project] name in python/pyproject.toml (e.g. 'lancedb-compat'). Default keeps 'lancedb'." + required: false + default: "lancedb" + rustflags: + description: "RUSTFLAGS for the build container, as a single whitespace-free token (e.g. '-Ctarget-cpu=x86-64-v2'). Empty leaves RUSTFLAGS unset, keeping the defaults from .cargo/config.toml." + required: false + default: "" runs: using: "composite" steps: @@ -27,6 +35,18 @@ runs: ARM_BUILD: ${{ inputs.arm-build }} run: | echo "ARM BUILD: $ARM_BUILD" + - name: Patch package name for variant build + if: ${{ inputs.package-name != 'lancedb' }} + shell: bash + env: + PACKAGE_NAME: ${{ inputs.package-name }} + run: | + # Swap the [project] name so this build produces e.g. lancedb-compat + # wheels. The package still installs files under the lancedb/ + # namespace -- import lancedb still works after pip install. + sed -i.bak 's/^name = "lancedb"$/name = "'"$PACKAGE_NAME"'"/' python/pyproject.toml + rm -f python/pyproject.toml.bak + grep '^name = ' python/pyproject.toml - name: Build x86_64 Manylinux wheel if: ${{ inputs.arm-build == 'false' }} uses: PyO3/maturin-action@v1 @@ -34,7 +54,7 @@ runs: maturin-version: "1.12.4" command: build working-directory: python - docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc" + docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc ${{ inputs.rustflags != '' && format('-e RUSTFLAGS={0}', inputs.rustflags) || '' }}" target: x86_64-unknown-linux-gnu manylinux: ${{ inputs.manylinux }} args: ${{ inputs.args }} @@ -51,7 +71,7 @@ runs: maturin-version: "1.12.4" command: build working-directory: python - docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc" + docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc ${{ inputs.rustflags != '' && format('-e RUSTFLAGS={0}', inputs.rustflags) || '' }}" target: aarch64-unknown-linux-gnu manylinux: ${{ inputs.manylinux }} args: ${{ inputs.args }} diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index b15f2e6b2..720079292 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -22,7 +22,7 @@ permissions: jobs: linux: - name: Python ${{ matrix.config.platform }} manylinux${{ matrix.config.manylinux }} + name: Python ${{ matrix.config.package_name }} ${{ matrix.config.platform }} manylinux${{ matrix.config.manylinux }} timeout-minutes: 60 strategy: matrix: @@ -31,11 +31,28 @@ jobs: manylinux: "2_28" extra_args: "--features fp16kernels" runner: ubuntu-22.04 + package_name: "lancedb" + rustflags: "" # For successful fat LTO builds, we need a large runner to avoid OOM errors. - platform: aarch64 manylinux: "2_28" extra_args: "--features fp16kernels" runner: ubuntu-2404-8x-arm64 + package_name: "lancedb" + rustflags: "" + # `lancedb-compat`: pre-Haswell-friendly variant for x86_64 hosts + # without AVX2 (Sandy Bridge / Ivy Bridge / Westmere on Intel, + # Bulldozer / Piledriver / Steamroller on AMD). Compiled at the + # `x86-64-v2` baseline; runtime SIMD dispatch in lance-linalg + # picks the appropriate tier (scalar / AVX / AVX+FMA / AVX2+FMA + # / AVX-512) at load time. Same import as `lancedb` -- conflicts + # at install time, so users pick one. + - platform: x86_64 + manylinux: "2_28" + extra_args: "" + runner: ubuntu-22.04 + package_name: "lancedb-compat" + rustflags: "-Ctarget-cpu=x86-64-v2" runs-on: ${{ matrix.config.runner }} steps: - uses: actions/checkout@v6 @@ -52,11 +69,13 @@ jobs: args: "--release --strip ${{ matrix.config.extra_args }}" arm-build: ${{ matrix.config.platform == 'aarch64' }} manylinux: ${{ matrix.config.manylinux }} + package-name: ${{ matrix.config.package_name }} + rustflags: ${{ matrix.config.rustflags }} - uses: actions/upload-artifact@v7 if: startsWith(github.ref, 'refs/tags/python-v') with: - name: wheels-linux-${{ matrix.config.platform }}-${{ matrix.config.manylinux }} - path: target/wheels/lancedb-*.whl + name: wheels-linux-${{ matrix.config.package_name }}-${{ matrix.config.platform }}-${{ matrix.config.manylinux }} + path: target/wheels/*.whl if-no-files-found: error mac: timeout-minutes: 90 @@ -145,7 +164,7 @@ jobs: FURY_TOKEN: ${{ secrets.FURY_TOKEN }} run: | shopt -s nullglob - WHEELS=(target/wheels/lancedb-*.whl) + WHEELS=(target/wheels/*.whl) if [[ ${#WHEELS[@]} -eq 0 ]]; then echo "No wheels found in target/wheels/" >&2 exit 1 diff --git a/python/README.md b/python/README.md index 81d567f04..550698500 100644 --- a/python/README.md +++ b/python/README.md @@ -8,6 +8,27 @@ A Python library for [LanceDB](https://github.com/lancedb/lancedb). pip install lancedb ``` +### Pre-Haswell x86_64 hosts: `lancedb-compat` + +The default `lancedb` wheel targets `x86-64-haswell` (AVX2 + FMA + F16C) for full performance on modern hardware. Pre-Haswell hosts — Intel Sandy Bridge / Ivy Bridge / Westmere; AMD Bulldozer / Piledriver / Steamroller — don't have AVX2 and crash with `Illegal instruction` at `import lancedb`. + +For those hosts, install the `lancedb-compat` package instead: + +```bash +pip install lancedb-compat +``` + +Same Python API (`import lancedb` works as usual). The compat wheel is compiled at the `x86-64-v2` baseline (Nehalem-class) and uses runtime SIMD dispatch in the embedded lance crate to pick the right kernel tier (scalar / AVX / AVX+FMA / AVX2+FMA / AVX-512) at load time, so it still goes fast on modern hardware while running cleanly on the pre-Haswell silicon. Use `lance.simd_info()` from Python to verify which tier was selected. + +`lancedb` and `lancedb-compat` install to the same `lancedb/` namespace and conflict at install time. Pick one. To switch, `pip uninstall lancedb` first, then `pip install lancedb-compat` (or vice-versa). + +If you need a custom baseline (or `lancedb-compat` isn't yet published for your platform), build from source with the override: + +```bash +RUSTFLAGS="-C target-cpu=x86-64-v2" maturin build --release +pip install ./target/wheels/lancedb-*.whl +``` + ### Preview Releases Stable releases are created about every 2 weeks. For the latest features and bug fixes, you can install the preview release. These releases receive the same level of testing as stable releases, but are not guaranteed to be available for more than 6 months after they are released. Once your application is stable, we recommend switching to stable releases.