diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d546b6b..ba4f460 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,10 @@ env: jobs: release: runs-on: ubuntu-latest + permissions: + contents: write + issues: write + pull-requests: write steps: - name: "☁️ checkout repository" uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -24,12 +28,18 @@ jobs: run: bun install -D @semantic-release/git conventional-changelog-conventionalcommits@8 semantic-release-cargo - name: Get Author Name and Email + id: author_info run: | AUTHOR_NAME=$(git log -1 --pretty=format:%an ${{ github.sha }}) AUTHOR_EMAIL=$(git log -1 --pretty=format:%ae ${{ github.sha }}) - echo "AUTHOR_NAME=$AUTHOR_NAME" >> $GITHUB_OUTPUT - echo "AUTHOR_EMAIL=$AUTHOR_EMAIL" >> $GITHUB_OUTPUT - id: author_info + { + echo "AUTHOR_NAME<> "$GITHUB_OUTPUT" - name: "Semantic release🚀" id: release diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb9f9d2..6965e69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,8 @@ env: jobs: build-onnx: runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Restore ONNX Build @@ -40,6 +42,8 @@ jobs: test: needs: build-onnx runs-on: ubuntu-latest + permissions: + contents: read env: HF_TOKEN: ${{ secrets.HF_TOKEN }} strategy: @@ -74,6 +78,8 @@ jobs: lint: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -92,6 +98,8 @@ jobs: clippy-features: runs-on: ${{ matrix.os }} + permissions: + contents: read strategy: fail-fast: false matrix: diff --git a/src/common.rs b/src/common.rs index b8c1f3c..284ef6b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -231,7 +231,7 @@ pub fn normalize(v: &[f32]) -> Vec { v.iter().map(|&val| val / (norm + epsilon)).collect() } -/// Pulls a model repo from HuggingFace.. +/// Pulls a model repo from HuggingFace. /// HF_HOME decides the location of the cache folder /// HF_ENDPOINT modifies the URL for the HuggingFace location. #[cfg(feature = "hf-hub")] diff --git a/src/sparse_text_embedding/bgem3_weights.rs b/src/sparse_text_embedding/bgem3_weights.rs index cfddd94..f1a6b9a 100644 --- a/src/sparse_text_embedding/bgem3_weights.rs +++ b/src/sparse_text_embedding/bgem3_weights.rs @@ -20,10 +20,14 @@ impl Bgem3SparseWeights { .expect("Failed to deserialize sparse_linear.safetensors"); let weight_view = tensors.tensor("weight").expect("Missing 'weight' tensor"); - let weight: Vec = weight_view - .data() - .chunks_exact(4) - .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])) + let (weight_chunks, weight_remainder) = weight_view.data().as_chunks::<4>(); + assert!( + weight_remainder.is_empty(), + "'weight' tensor byte length is not divisible by 4" + ); + let weight: Vec = weight_chunks + .iter() + .map(|b| f32::from_le_bytes(*b)) .collect(); let bias_view = tensors.tensor("bias").expect("Missing 'bias' tensor"); diff --git a/src/sparse_text_embedding/impl.rs b/src/sparse_text_embedding/impl.rs index 194f84c..000c615 100644 --- a/src/sparse_text_embedding/impl.rs +++ b/src/sparse_text_embedding/impl.rs @@ -263,8 +263,8 @@ impl SparseTextEmbedding { .rows() .into_iter() .map(|row_scores| { - let mut values: Vec = Vec::with_capacity(scores.len()); - let mut indices: Vec = Vec::with_capacity(scores.len()); + let mut values: Vec = Vec::with_capacity(row_scores.len()); + let mut indices: Vec = Vec::with_capacity(row_scores.len()); row_scores.into_iter().enumerate().for_each(|(idx, f)| { if *f > 0.0 { diff --git a/tests/bgem3.rs b/tests/bgem3.rs index d4b234d..2c61f27 100644 --- a/tests/bgem3.rs +++ b/tests/bgem3.rs @@ -8,10 +8,11 @@ use std::collections::HashMap; use std::sync::Mutex; static MODEL_LOCK: Mutex<()> = Mutex::new(()); +const EPS: f32 = 2e-2; #[test] fn test_bgem3_joint_embeddings_match_python() { - let _guard = MODEL_LOCK.lock().unwrap(); + let _guard = MODEL_LOCK.lock().unwrap_or_else(|err| err.into_inner()); let mut model = Bgem3Embedding::try_new(Bgem3InitOptions::new(Bgem3Model::BGEM3Q)) .expect("Failed to initialize BGEM3Q model"); @@ -43,10 +44,10 @@ fn test_bgem3_joint_embeddings_match_python() { ]; for (i, val) in expected_dense_0.iter().enumerate() { - assert!((output.dense[0][i] - val).abs() < 1e-4); + assert!((output.dense[0][i] - val).abs() < EPS); } for (i, val) in expected_dense_1.iter().enumerate() { - assert!((output.dense[1][i] - val).abs() < 1e-4); + assert!((output.dense[1][i] - val).abs() < EPS); } // 2. Verify Sparse Embeddings @@ -93,7 +94,7 @@ fn test_bgem3_joint_embeddings_match_python() { .get(idx) .expect("Unexpected index in sparse 0"); assert!( - (val - expected_val).abs() < 1e-4, + (val - expected_val).abs() < EPS, "Sparse 0 index {}: expected {}, got {}", idx, expected_val, @@ -111,7 +112,7 @@ fn test_bgem3_joint_embeddings_match_python() { .get(idx) .expect("Unexpected index in sparse 1"); assert!( - (val - expected_val).abs() < 1e-4, + (val - expected_val).abs() < EPS, "Sparse 1 index {}: expected {}, got {}", idx, expected_val, @@ -146,22 +147,22 @@ fn test_bgem3_joint_embeddings_match_python() { ]; for (i, val) in expected_colbert_0_tok1.iter().enumerate() { - assert!((output.colbert[0][0][i] - val).abs() < 1e-4); + assert!((output.colbert[0][0][i] - val).abs() < EPS); } for (i, val) in expected_colbert_0_tok2.iter().enumerate() { - assert!((output.colbert[0][1][i] - val).abs() < 1e-4); + assert!((output.colbert[0][1][i] - val).abs() < EPS); } for (i, val) in expected_colbert_1_tok1.iter().enumerate() { - assert!((output.colbert[1][0][i] - val).abs() < 1e-4); + assert!((output.colbert[1][0][i] - val).abs() < EPS); } for (i, val) in expected_colbert_1_tok2.iter().enumerate() { - assert!((output.colbert[1][1][i] - val).abs() < 1e-4); + assert!((output.colbert[1][1][i] - val).abs() < EPS); } } #[test] fn test_bgem3_user_defined_model() { - let _guard = MODEL_LOCK.lock().unwrap(); + let _guard = MODEL_LOCK.lock().unwrap_or_else(|err| err.into_inner()); // We will verify the user-defined loader by pulling the files from HF and feeding them manually to simulate a local deployment // Reuse fastembed's cache — model already downloaded by test_bgem3_joint_embeddings_match_python @@ -213,13 +214,13 @@ fn test_bgem3_user_defined_model() { -0.01868816465139389, ]; for (i, val) in expected_dense_0.iter().enumerate() { - assert!((output.dense[0][i] - val).abs() < 1e-4); + assert!((output.dense[0][i] - val).abs() < EPS); } } #[test] fn test_bgem3_custom_max_length() { - let _guard = MODEL_LOCK.lock().unwrap(); + let _guard = MODEL_LOCK.lock().unwrap_or_else(|err| err.into_inner()); // Verify that the user can override the max length (e.g. to 5 tokens) and it successfully truncates let mut model = Bgem3Embedding::try_new(Bgem3InitOptions::new(Bgem3Model::BGEM3Q).with_max_length(5)) diff --git a/tests/text-embeddings.rs b/tests/text-embeddings.rs index dcf0220..b8c42c5 100644 --- a/tests/text-embeddings.rs +++ b/tests/text-embeddings.rs @@ -13,7 +13,7 @@ use fastembed::{ }; /// A small epsilon value for floating point comparisons. -const EPS: f32 = 1e-2; +const EPS: f32 = 2e-2; /// Precalculated embeddings for the supported models using #99 /// (4f09b6842ce1fcfaf6362678afcad9a176e05304).