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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: python3 scripts/test-dependency-checks.py
- run: bash scripts/check-standalone-lockfiles.sh

fmt:
Expand Down Expand Up @@ -479,6 +480,10 @@ jobs:
- run: cargo test --no-default-features --features clusterer,vbx --lib --locked
- run: cargo test --no-default-features --features clusterer,vbx --test library_vbx_from_dir --locked
- run: cargo clippy --no-default-features --features clusterer,vbx --all-targets --locked -- -D warnings
# Local model loading must also compile its tests without download.
# Serial execution avoids races between process-environment tests.
- run: cargo test --no-default-features --features pipeline-local,vbx --lib --locked -- --test-threads=1
- run: cargo clippy --no-default-features --features pipeline-local,vbx --lib --tests --locked -- -D warnings
# Hand-written kernels + kernel-only CLI (no ort, no tract).
- run: cargo test -p polyvoice-kernels --locked
- run: cargo clippy -p polyvoice-kernels --all-targets --locked -- -D warnings
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist
args: --locked --release --out dist
sccache: ${{ matrix.os != 'macos-latest' }}
manylinux: ${{ matrix.manylinux || 'auto' }}
working-directory: python
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
with:
python-version: ${{ matrix.python }}
- run: pip install maturin pytest
- run: maturin build --release --out dist
- run: maturin build --locked --release --out dist
- name: Wheel has no ONNX Runtime dylib
shell: bash
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ jobs:
~/AppData/Local/ort.pyke.io
key: ort-dfbin-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
- run: pip install maturin
- run: maturin build --release --out dist
- run: maturin build --locked --release --out dist
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: wheels-${{ matrix.os }}
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Invalidate thread-local packed GEMM weights when a native model is dropped,
preventing stale weights when subsequent models reuse allocation addresses.

- Refresh the Python lockfile for mmap-backed kernels. Dependency checks and
wheel builds enforce locked resolution; Cargo failures no longer pass as
evidence that a forbidden dependency is absent.
- Restore FFI compilation after configuration validation was added, mapping
invalid settings to `InvalidArg`. Local-pipeline tests now compile without
the optional downloader and are covered by CI.

### Breaking

- Development version advances to 0.22.0 for the accumulated API changes.
Exhaustive configuration literals must account for `PipelineConfig.reconstruct`
and `VbxClustererConfig.{ahc_on_raw_l2,soft_reassign}`; exhaustive error matches
must handle `ConfigError::InvalidSetting` and the local registry verification
variants `RegistryError::{ChecksumMismatch,SignatureRejected}`.

- `PipelineConfig` gains `max_audio_samples`. Full struct literals must add
this field or use `..PipelineConfig::default()`. This API change is part
of 0.22.0. The default remains one hour
at 16 kHz; Rust callers can override it with `PipelineBuilder::max_audio_samples`.
C FFI and WAV-loading limits remain unchanged.

### Added

- `pipeline-local`: powerset + ResNet34 kernels + VBx from a local directory
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ resolver = "2"

[package]
name = "polyvoice"
version = "0.21.0"
version = "0.22.0"
edition = "2024"
# MSRV: rten-gemm / rten-simd (Linux INT8 GEMM) need 1.94.
rust-version = "1.94.0"
Expand Down
2 changes: 1 addition & 1 deletion docs/semver.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Semver and API freeze

Crate version is **0.21.x**. This is not `1.0.0`. The GO checklist in
Development version is **0.22.0** (unreleased). This is not `1.0.0`. The GO checklist in
[`PRODUCTION-READINESS.md`](../PRODUCTION-READINESS.md) stays open until a
freeze window has held and the other 1.0 boxes are true.

Expand Down
2 changes: 1 addition & 1 deletion fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion polyvoice-asr-sherpa/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions polyvoice-kernels/src/powerset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ pub struct Powerset {
onnx_map: Option<crate::onnx_init::MappedOnnx>,
}

impl Drop for Powerset {
fn drop(&mut self) {
crate::rten_matmul::invalidate_packed_weights();
}
}

impl Powerset {
pub fn from_onnx_path(path: &Path) -> Result<Self, KernelError> {
crate::rten_matmul::pin_parallelism();
Expand Down
6 changes: 6 additions & 0 deletions polyvoice-kernels/src/resnet34.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,12 @@ pub struct ResNet34 {
onnx_map: Option<crate::onnx_init::MappedOnnx>,
}

impl Drop for ResNet34 {
fn drop(&mut self) {
crate::rten_matmul::invalidate_packed_weights();
}
}

impl ResNet34 {
/// Load weights from shipping `resnet34_int8.onnx` or the FP32 file
/// (initializers only; QDQ weights are dequantized).
Expand Down
62 changes: 62 additions & 0 deletions polyvoice-kernels/src/rten_matmul.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@ use rten_tensor::NdTensorView;
#[cfg(not(target_vendor = "apple"))]
use std::cell::RefCell;

// Packed matrices are keyed by weight address. A model drop invalidates all
// thread-local packs before freed addresses can belong to another model.
#[cfg(not(target_vendor = "apple"))]
static WEIGHT_GENERATION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);

pub(crate) fn invalidate_packed_weights() {
#[cfg(not(target_vendor = "apple"))]
WEIGHT_GENERATION.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
}

#[cfg(not(target_vendor = "apple"))]
fn refresh_packed_weights() {
let generation = WEIGHT_GENERATION.load(std::sync::atomic::Ordering::Acquire);
PACK_GENERATION.with(|seen| {
if seen.get() != generation {
PACKED_F32_A.with(|cache| cache.borrow_mut().clear());
PACKED_F32_B.with(|cache| cache.borrow_mut().clear());
PACKED_I8_B.with(|cache| cache.borrow_mut().clear());
seen.set(generation);
}
});
}

pub fn pin_parallelism() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
Expand Down Expand Up @@ -52,6 +75,7 @@ struct I8Scratch {

#[cfg(not(target_vendor = "apple"))]
thread_local! {
static PACK_GENERATION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
static F32_EXEC: GemmExecutor<f32, f32, f32> = {
pin_parallelism();
GemmExecutor::new()
Expand Down Expand Up @@ -97,6 +121,7 @@ pub fn gemm_rowbias(
n: usize,
k: usize,
) -> bool {
refresh_packed_weights();
if m == 0 || n == 0 || k == 0 {
return false;
}
Expand Down Expand Up @@ -146,6 +171,7 @@ pub fn gemm_colbias(
n: usize,
k: usize,
) -> bool {
refresh_packed_weights();
if m == 0 || n == 0 || k == 0 {
return false;
}
Expand Down Expand Up @@ -188,6 +214,7 @@ pub fn gemm_colbias(
/// `C[m,n] += A[m,k] @ B[k,n]`.
#[cfg(not(target_vendor = "apple"))]
pub fn gemm_add(a: &[f32], b: &[f32], c: &mut [f32], m: usize, n: usize, k: usize) -> bool {
refresh_packed_weights();
if m == 0 || n == 0 || k == 0 {
return false;
}
Expand Down Expand Up @@ -247,6 +274,7 @@ pub fn gemm_i8_static(
k: usize,
accumulate: bool,
) -> bool {
refresh_packed_weights();
if m == 0 || n == 0 || k == 0 || a_scale.abs() < 1e-12 || !i8_exact_on_this_cpu() {
return false;
}
Expand Down Expand Up @@ -897,6 +925,40 @@ fn dequant_nchw(acc: &[i32], conv: &Conv2d, y: &mut Tensor, ni: usize, spatial:

#[cfg(all(test, not(target_vendor = "apple")))]
mod tests {
#[test]
fn packed_weights_are_invalidated_across_threads() {
let mut weights = vec![1.0f32; 4];
let input = vec![1.0f32; 4];
let mut out = vec![0.0f32; 4];
assert!(super::gemm_colbias(
&input, &weights, &[0.0; 2], &mut out, 2, 2, 2
));
assert_eq!(out, vec![2.0; 4]);
// Model destruction on another thread must invalidate this thread's
// packed weights before an allocator reuses the same address.
assert!(
std::thread::spawn(super::invalidate_packed_weights)
.join()
.is_ok()
);
weights.fill(3.0);
assert!(super::gemm_colbias(
&input, &weights, &[0.0; 2], &mut out, 2, 2, 2
));
assert_eq!(out, vec![6.0; 4]);

assert!(super::gemm_rowbias(
&weights, &input, &[0.0; 2], &mut out, 2, 2, 2
));
assert_eq!(out, vec![6.0; 4]);
super::invalidate_packed_weights();
weights.fill(5.0);
assert!(super::gemm_rowbias(
&weights, &input, &[0.0; 2], &mut out, 2, 2, 2
));
assert_eq!(out, vec![10.0; 4]);
}

use super::try_conv_i8;
use crate::conv::Conv2d;
use crate::tensor::Tensor;
Expand Down
14 changes: 12 additions & 2 deletions python/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "polyvoice-python"
version = "0.21.0"
version = "0.22.0"
edition = "2024"
# Tracks the core crate's MSRV (the path dependency on polyvoice requires 1.94);
# pyo3 itself is fine with older toolchains.
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "maturin"

[project]
name = "polyvoice"
version = "0.21.0"
version = "0.22.0"
description = "Speaker diarization for Python — who spoke when. Hand-written INT8 kernels (~8.4 MB), no ONNX Runtime. VBx clustering, overlap detection."
readme = "README.md"
license = {text = "MIT"}
Expand Down
20 changes: 4 additions & 16 deletions scripts/check-ort-free.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,14 @@ set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

# Returns 0 if `ort` appears in the normal dependency graph for the given
# cargo-tree args; 1 if cargo cannot resolve package ID `ort` (clean).
ort_in_graph() {
# Redirect stderr: when clean, cargo prints "package ID specification `ort`
# did not match any packages" and exits non-zero. We only care whether any
# package lines land on stdout.
# Portable grep (CI images may not have ripgrep). Any stdout line means
# cargo resolved an `ort` package into the normal graph.
if cargo tree -e normal -i ort "$@" 2>/dev/null | grep -q .; then
return 0
fi
return 1
}

fail_if_ort() {
local label="$1"
shift
if ort_in_graph "$@"; then
local graph
graph="$(cargo tree --locked -e normal --prefix none "$@")" || exit 1
if grep -q '^ort v' <<< "$graph"; then
echo "FAIL: ort leaked into ${label} dependency graph:"
cargo tree -e normal -i ort "$@" || true
printf '%s\n' "$graph"
exit 1
fi
echo "OK: no ort in ${label}"
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-ort-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ EXPECTED="2.0.0-rc.12"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"

versions="$(cargo metadata --format-version 1 2>/dev/null \
| python3 -c "import sys,json; print('\n'.join(sorted({p['version'] for p in json.load(sys.stdin)['packages'] if p['name']=='ort'})))")"
metadata="$(cargo metadata --locked --format-version 1)"
versions="$(python3 -c "import sys,json; print('\n'.join(sorted({p['version'] for p in json.load(sys.stdin)['packages'] if p['name']=='ort'})))" <<< "$metadata")"

count="$(printf '%s\n' "$versions" | grep -c . || true)"

Expand Down
Loading
Loading