From 4996d9a3927add4abe1b1b56355a990bcf3ee05a Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 6 Oct 2026 16:21:14 +0200 Subject: [PATCH 1/2] fix(hash): fall back to fxhash when AES is not enabled gxhash only compiles with hardware AES enabled at compile time, so any crate depending on ppvm failed to build on x86_64 and aarch64 Linux unless its users set RUSTFLAGS themselves (the repo's .cargo/config.toml only applies to builds inside this repo). Gate the gxhash dependency and every use of it on `all(target_feature = "aes", any(target_arch = "x86_64", target_arch = "aarch64"))` instead of `not(wasm32)`. Without AES the gxhash configs are dropped and ppvm-tableau-sum's fingerprint uses fxhash, as on wasm. This also fixes the dashmap gxhash configs failing to compile with the `gxhash` feature off. Update the build docs and the usage skill. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/ppvm-pauli-sum/Cargo.toml | 9 ++++--- .../examples/trotter_qubit_sweep.rs | 13 +++++++--- crates/ppvm-pauli-sum/src/config/dashmap.rs | 15 +++++++++++ crates/ppvm-pauli-sum/src/config/indexmap.rs | 22 ++++++++++++---- crates/ppvm-pauli-sum/src/config/mod.rs | 6 +++-- crates/ppvm-pauli-word/Cargo.toml | 6 ++--- crates/ppvm-tableau-sum/Cargo.toml | 10 ++++--- crates/ppvm-tableau-sum/src/storage/mod.rs | 26 ++++++++++++++----- crates/ppvm-traits/Cargo.toml | 16 +++++++----- crates/ppvm-traits/src/traits/hash.rs | 12 +++++++-- docs/src/pages/develop.astro | 21 ++++++++------- docs/src/pages/index.astro | 5 ++-- skills/ppvm-usage/SKILL.md | 2 +- 13 files changed, 116 insertions(+), 47 deletions(-) diff --git a/crates/ppvm-pauli-sum/Cargo.toml b/crates/ppvm-pauli-sum/Cargo.toml index c286a6973..9cd8e1a9f 100644 --- a/crates/ppvm-pauli-sum/Cargo.toml +++ b/crates/ppvm-pauli-sum/Cargo.toml @@ -13,11 +13,14 @@ bon = "3.7.2" approx = { version = "0.5.1", optional = true } indexmap = { version = "2.11.4", optional = true } -# Native-only optional deps: dashmap pulls rayon (OS threads) and gxhash needs -# AES intrinsics — neither builds on wasm32. Pruned there; the matching features -# go inert (see the `not(target_arch = "wasm32")` gates on the config modules). +# Native-only optional dep: dashmap pulls rayon (OS threads), which doesn't build +# on wasm32. Pruned there; the feature goes inert (see the config module gates). [target.'cfg(not(target_arch = "wasm32"))'.dependencies] dashmap = { version = "6.1.0", features = ["rayon"], optional = true } + +# gxhash only compiles with hardware AES enabled; pruned otherwise, which drops +# the gxhash configs (same condition on the config gates). +[target.'cfg(all(target_feature = "aes", any(target_arch = "x86_64", target_arch = "aarch64")))'.dependencies] gxhash = { version = "3.5.0", features = ["deterministic"], optional = true } [dev-dependencies] diff --git a/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs b/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs index 977699cf4..7228efb2d 100644 --- a/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs +++ b/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs @@ -225,8 +225,15 @@ fn main() { println!("{n},fxhash,{bytes},{fx_t:.6},{fx_len}"); eprintln!("n={n:3} bytes={bytes:2} fxhash {fx_t:8.4}s ({fx_len} terms)"); - let (gx_t, gx_len) = sweep!(ByteGxHashF64, n, it, p); - println!("{n},gxhash,{bytes},{gx_t:.6},{gx_len}"); - eprintln!("n={n:3} bytes={bytes:2} gxhash {gx_t:8.4}s ({gx_len} terms)"); + // The gxhash configs only exist when AES is enabled at compile time. + #[cfg(all( + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") + ))] + { + let (gx_t, gx_len) = sweep!(ByteGxHashF64, n, it, p); + println!("{n},gxhash,{bytes},{gx_t:.6},{gx_len}"); + eprintln!("n={n:3} bytes={bytes:2} gxhash {gx_t:8.4}s ({gx_len} terms)"); + } } } diff --git a/crates/ppvm-pauli-sum/src/config/dashmap.rs b/crates/ppvm-pauli-sum/src/config/dashmap.rs index 66274b38a..550de2a8d 100644 --- a/crates/ppvm-pauli-sum/src/config/dashmap.rs +++ b/crates/ppvm-pauli-sum/src/config/dashmap.rs @@ -28,6 +28,11 @@ impl, >(PhantomData<(C, St, W)>); +#[cfg(all( + feature = "gxhash", + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +))] impl Config for ByteGxHash { @@ -50,4 +60,9 @@ impl = ByteFxHash; /// [`ByteGxHash`] specialised to `f64` coefficients. +#[cfg(all( + feature = "gxhash", + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +))] pub type ByteGxHashF64 = ByteGxHash; diff --git a/crates/ppvm-pauli-sum/src/config/indexmap.rs b/crates/ppvm-pauli-sum/src/config/indexmap.rs index 0be201eed..35b545710 100644 --- a/crates/ppvm-pauli-sum/src/config/indexmap.rs +++ b/crates/ppvm-pauli-sum/src/config/indexmap.rs @@ -29,9 +29,13 @@ impl Config /// `IndexMap`-backed [`Config`] with `[u8; N]` storage and `gxhash`. /// -/// `gxhash` is AES-based and native-only, so this config is unavailable on -/// `wasm32`; use [`ByteFxHash`] there. -#[cfg(all(feature = "gxhash", not(target_arch = "wasm32")))] +/// `gxhash` needs hardware AES, so this config is only available when it is +/// enabled (e.g. `-C target-feature=+aes`); use [`ByteFxHash`] otherwise. +#[cfg(all( + feature = "gxhash", + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ByteGxHash< const N: usize, @@ -40,7 +44,11 @@ pub struct ByteGxHash< W: PauliWordTrait = PauliWord<[u8; N], gxhash::GxBuildHasher>, >(PhantomData<(C, St, W)>); -#[cfg(all(feature = "gxhash", not(target_arch = "wasm32")))] +#[cfg(all( + feature = "gxhash", + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +))] impl Config for ByteGxHash { @@ -59,7 +67,11 @@ pub type ByteFxHashF64< Wd = PauliWord<[u8; N], fxhash::FxBuildHasher>, > = ByteFxHash; /// [`ByteGxHash`] specialised to `f64` coefficients. -#[cfg(all(feature = "gxhash", not(target_arch = "wasm32")))] +#[cfg(all( + feature = "gxhash", + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +))] pub type ByteGxHashF64< const N: usize, St = NoStrategy, diff --git a/crates/ppvm-pauli-sum/src/config/mod.rs b/crates/ppvm-pauli-sum/src/config/mod.rs index 7672e32cd..39a08532f 100644 --- a/crates/ppvm-pauli-sum/src/config/mod.rs +++ b/crates/ppvm-pauli-sum/src/config/mod.rs @@ -21,9 +21,11 @@ pub mod dashmap; pub mod indexmap; /// Pre-built configs using `gxhash` — fast on platforms with AES -/// hardware acceleration. Requires the `gxhash` feature. +/// hardware acceleration. Requires the `gxhash` feature and AES enabled at +/// compile time (e.g. `-C target-feature=+aes`). #[cfg(all( feature = "gxhash", - any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64") + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") ))] pub mod gxhash; diff --git a/crates/ppvm-pauli-word/Cargo.toml b/crates/ppvm-pauli-word/Cargo.toml index 5a30cbeb6..c789944c5 100644 --- a/crates/ppvm-pauli-word/Cargo.toml +++ b/crates/ppvm-pauli-word/Cargo.toml @@ -14,9 +14,9 @@ itertools = "0.14.0" serde = { version = "1.0.228", features = ["derive"], optional = true } bincode = { version = "2.0.1", optional = true } -# gxhash needs AES intrinsics and does not build on wasm32; pruned there (the -# `gxhash` feature goes inert). No non-test code in this crate names the crate. -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +# gxhash only compiles with hardware AES enabled; pruned otherwise (the `gxhash` +# feature goes inert). No non-test code in this crate names the crate. +[target.'cfg(all(target_feature = "aes", any(target_arch = "x86_64", target_arch = "aarch64")))'.dependencies] gxhash = { version = "3.5.0", features = ["deterministic"], optional = true } [features] diff --git a/crates/ppvm-tableau-sum/Cargo.toml b/crates/ppvm-tableau-sum/Cargo.toml index d88298f30..618bd6db0 100644 --- a/crates/ppvm-tableau-sum/Cargo.toml +++ b/crates/ppvm-tableau-sum/Cargo.toml @@ -15,13 +15,15 @@ rand = "0.10.1" smallvec = "1.15" ppvm-pauli-word = { version = "0.1.0", path = "../ppvm-pauli-word" } -# Native-only deps: gxhash (AES, used for the word_fingerprint hasher — see the -# target-gated alias in src/storage/mod.rs) and rayon (OS threads). Pruned on -# wasm32; fxhash backs the fingerprint there. +# Native-only dep: rayon (OS threads). Pruned on wasm32. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -gxhash = "3.5.0" rayon = { version = "1", optional = true } +# gxhash backs the word_fingerprint hasher but only compiles with hardware AES +# enabled; fxhash is used otherwise (same condition in src/storage/mod.rs). +[target.'cfg(all(target_feature = "aes", any(target_arch = "x86_64", target_arch = "aarch64")))'.dependencies] +gxhash = "3.5.0" + [dev-dependencies] criterion = "0.8.2" rayon = "1" diff --git a/crates/ppvm-tableau-sum/src/storage/mod.rs b/crates/ppvm-tableau-sum/src/storage/mod.rs index c4320146c..518db8bf1 100644 --- a/crates/ppvm-tableau-sum/src/storage/mod.rs +++ b/crates/ppvm-tableau-sum/src/storage/mod.rs @@ -9,14 +9,17 @@ pub use entry_store::{Branch, EntryStore}; use fxhash::FxHashMap; use ppvm_traits::traits::Clifford; -// Hasher for the structural `word_fingerprint`. gxhash (AES-based) is fastest on -// native and exposes a `gxhash64` bulk free function, but it needs hardware AES -// and does not build on wasm32, so fall back to fxhash there. The fingerprint is +// Hasher for the structural `word_fingerprint`. gxhash (AES-based) is fastest and +// exposes a `gxhash64` bulk free function, but it only compiles with hardware AES +// enabled (never on wasm32), so fall back to fxhash otherwise. The fingerprint is // a transient in-memory dedup key — collisions are resolved by // `structurally_equal`, and it is never persisted or compared across builds — so // the hasher may differ per target without affecting results. use bitvec::view::{BitView, BitViewSized}; -#[cfg(target_arch = "wasm32")] +#[cfg(not(all( + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +)))] use fxhash::FxHasher as FingerprintHasher; use num::{ Complex, One, PrimInt, Zero, @@ -27,7 +30,10 @@ use ppvm_tableau::{ data::GeneralizedTableau, sparsevec::SparseVector, tableau_index::TableauIndex, }; use ppvm_traits::config::Config; -#[cfg(target_arch = "wasm32")] +#[cfg(not(all( + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +)))] use std::hash::Hasher; use std::ops::AddAssign; @@ -75,11 +81,17 @@ where buf.extend_from_slice(bytemuck::bytes_of(&row.word.zbits.data)); } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all( + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") + ))] { gxhash::gxhash64(&buf, 0) } - #[cfg(target_arch = "wasm32")] + #[cfg(not(all( + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") + )))] { let mut hasher = FingerprintHasher::default(); hasher.write(&buf); diff --git a/crates/ppvm-traits/Cargo.toml b/crates/ppvm-traits/Cargo.toml index 04697b147..10a1e54a5 100644 --- a/crates/ppvm-traits/Cargo.toml +++ b/crates/ppvm-traits/Cargo.toml @@ -10,17 +10,21 @@ fxhash = "0.2.1" num = "0.4.3" indexmap = { version = "2.11.4", optional = true } -# Native-only optional deps: gxhash needs AES intrinsics, dashmap/rayon need OS -# threads, and ahash pulls getrandom — none build on wasm32. They are pruned on -# wasm, where the matching features go inert (see the `not(target_arch = -# "wasm32")` code gates), so a wasm build silently drops these acceleration -# paths. On 64-bit native nothing changes. +# Native-only optional deps: dashmap/rayon need OS threads and ahash pulls +# getrandom — none build on wasm32. They are pruned on wasm, where the matching +# features go inert (see the `not(target_arch = "wasm32")` code gates), so a +# wasm build silently drops these acceleration paths. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ahash = { version = "0.8.12", optional = true } dashmap = { version = "6.1.0", features = ["rayon"], optional = true } -gxhash = { version = "3.5.0", features = ["deterministic"], optional = true } rayon = { version = "1.11.0", optional = true } +# gxhash only compiles with hardware AES enabled (e.g. `-C target-feature=+aes`). +# Without it the `gxhash` feature goes inert and fxhash is used instead; the +# same condition gates the code. Keep it in sync across the crates. +[target.'cfg(all(target_feature = "aes", any(target_arch = "x86_64", target_arch = "aarch64")))'.dependencies] +gxhash = { version = "3.5.0", features = ["deterministic"], optional = true } + [dev-dependencies] insta = { version = "1.43.2", features = ["yaml"] } diff --git a/crates/ppvm-traits/src/traits/hash.rs b/crates/ppvm-traits/src/traits/hash.rs index 14bc1a607..0552383fc 100644 --- a/crates/ppvm-traits/src/traits/hash.rs +++ b/crates/ppvm-traits/src/traits/hash.rs @@ -65,7 +65,11 @@ impl HashFinalize for fxhash::FxBuildHasher { // max bucket 6, essentially ideal, vs fxhash's 2257 at 64 qubits), so the // identity default is exactly right — folding would only pay the tag/bucket // coupling cost above with no distribution benefit. -#[cfg(all(feature = "gxhash", not(target_arch = "wasm32")))] +#[cfg(all( + feature = "gxhash", + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") +))] impl HashFinalize for gxhash::GxBuildHasher {} #[cfg(test)] @@ -99,7 +103,11 @@ mod tests { } } - #[cfg(all(feature = "gxhash", not(target_arch = "wasm32")))] + #[cfg(all( + feature = "gxhash", + target_feature = "aes", + any(target_arch = "x86_64", target_arch = "aarch64") + ))] #[test] fn gxhash_never_folds() { // gxhash already distributes its low bits, so it is the identity at diff --git a/docs/src/pages/develop.astro b/docs/src/pages/develop.astro index 6beaa457b..91dbbf5bf 100644 --- a/docs/src/pages/develop.astro +++ b/docs/src/pages/develop.astro @@ -159,11 +159,11 @@ cargo bench -p ppvm-tableau --bench micro cargo bench --bench micro -- "gates/single-qubit/h"

- Rust edition 2024. On x86 the default hasher (gxhash) needs - AES/SSE2 target features; this repo sets them for x86_64 in - .cargo/config.toml, and CI does the same. On non-x86 hosts, - build with - --no-default-features --features=indexmap,ahash or similar. + Rust edition 2024. The fast hasher (gxhash) only compiles with AES + enabled at compile time; this repo enables it for x86_64 in + .cargo/config.toml (Apple Silicon has it by default), and CI + does the same. Without AES, the gxhash dependency is pruned + and fxhash is used instead (see the WebAssembly notes below).

WebAssembly (wasm32)

@@ -180,17 +180,20 @@ cargo build --target wasm32-unknown-unknown --workspace --exclude ppvm-python-na

The build is wasm-clean automatically. Native-only acceleration - dependencies — gxhash (AES intrinsics), dashmap → - rayon (OS threads), and ahash — live in + dependencies — dashmap → rayon (OS threads) + and ahash — live in [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tables, so on wasm they are pruned and the matching features go inert (the code that names those crates is gated with the same - not(target_arch = "wasm32")). The fx64hash configs + not(target_arch = "wasm32")). gxhash works the + same way but is gated on AES being enabled + (all(target_feature = "aes", any(target_arch = "x86_64", + target_arch = "aarch64"))), which wasm never has. The fx64hash configs use native-word [usize; N] storage (u64 on 64-bit, u32 on wasm) since bitvec only implements BitStore for u64 on 64-bit pointer widths. ppvm-tableau-sum's structural fingerprint falls back from - gxhash to fxhash on wasm. + gxhash to fxhash whenever AES is unavailable.

diff --git a/docs/src/pages/index.astro b/docs/src/pages/index.astro index f5a8ea5db..8a4194ba6 100644 --- a/docs/src/pages/index.astro +++ b/docs/src/pages/index.astro @@ -180,8 +180,9 @@ ppvm-tableau = {`{ git = "https://github.com/QuEraComputing/ppvm" }`} ppvm-stim = {`{ git = "https://github.com/QuEraComputing/ppvm" }`} ppvm-sym = {`{ git = "https://github.com/QuEraComputing/ppvm" }`}

- Requires Rust edition 2024. On x86, set - RUSTFLAGS="-C target-feature=+aes,+sse2" — gxhash needs AES. + Requires Rust edition 2024. Set + RUSTFLAGS="-C target-feature=+aes" to use the faster + gxhash hasher; without AES, ppvm falls back to fxhash.

Continue to the Rust Quick Start → diff --git a/skills/ppvm-usage/SKILL.md b/skills/ppvm-usage/SKILL.md index 4c290a72f..495559fc6 100644 --- a/skills/ppvm-usage/SKILL.md +++ b/skills/ppvm-usage/SKILL.md @@ -171,7 +171,7 @@ ppvm-stim = { git = "https://github.com/QuEraComputing/ppvm" } # for Stim e ppvm-sym = { git = "https://github.com/QuEraComputing/ppvm" } # for symbolic propagation ``` -On x86, set `RUSTFLAGS="-C target-feature=+aes,+sse2"` (gxhash needs AES). On other targets, build with `--no-default-features --features=indexmap,ahash` to drop gxhash. +Set `RUSTFLAGS="-C target-feature=+aes"` (or `-C target-cpu=native`) to use the faster gxhash hasher. Without AES enabled at compile time, gxhash is dropped automatically: the `gxhash` configs disappear and everything else falls back to fxhash. ### Pauli propagation From 0463236b3bf8b3e8c5dd60d30dfcf317873022fe Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Tue, 6 Oct 2026 16:39:15 +0200 Subject: [PATCH 2/2] fix(pauli-sum): skip the example's gxhash run without the gxhash feature The gxhash configs need both the `gxhash` feature and AES, but the example only checked for AES, so `--no-default-features` builds on AES targets failed to compile it. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs b/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs index 7228efb2d..9ce9886c0 100644 --- a/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs +++ b/crates/ppvm-pauli-sum/examples/trotter_qubit_sweep.rs @@ -225,8 +225,9 @@ fn main() { println!("{n},fxhash,{bytes},{fx_t:.6},{fx_len}"); eprintln!("n={n:3} bytes={bytes:2} fxhash {fx_t:8.4}s ({fx_len} terms)"); - // The gxhash configs only exist when AES is enabled at compile time. + // The gxhash configs need the `gxhash` feature and AES at compile time. #[cfg(all( + feature = "gxhash", target_feature = "aes", any(target_arch = "x86_64", target_arch = "aarch64") ))]