Files
unslothai--unsloth/studio/backend/tests/test_rag_embeddings.py
T
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

223 lines
7.2 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Embedder concurrency tests: the fast tokenizer isn't thread-safe, so encode
and token counting must be serialized (else threads panic "Already borrowed")."""
import os
import threading
import time
import numpy as np
import pytest
from core.rag import config, embeddings
@pytest.fixture(autouse = True)
def _pin_st_backend(monkeypatch):
# Tests patch ST internals (_get), so force the ST backend.
monkeypatch.setattr(config, "EMBED_BACKEND", "sentence-transformers")
embeddings._reset_backend()
yield
embeddings._reset_backend()
class _ConcurrencyProbe:
"""Records whether two callers were in the guarded body at once."""
def __init__(self):
self.inside = 0
self.saw_overlap = False
self._g = threading.Lock()
def enter(self):
with self._g:
self.inside += 1
if self.inside > 1:
self.saw_overlap = True
time.sleep(0.005) # widen the race window
with self._g:
self.inside -= 1
class _FakeModel:
def __init__(self, probe):
self._probe = probe
self.tokenizer = _FakeTokenizer(probe)
def encode(self, texts, **_kw):
self._probe.enter()
return np.zeros((len(texts), 4), dtype = np.float32)
class _FakeTokenizer:
def __init__(self, probe):
self._probe = probe
def encode(self, text, **_kw):
self._probe.enter()
return list(range(len(text.split())))
def _hammer(fn, n = 8):
errors: list[Exception] = []
def worker():
try:
fn()
except Exception as exc: # noqa: BLE001
errors.append(exc)
threads = [threading.Thread(target = worker) for _ in range(n)]
for t in threads:
t.start()
for t in threads:
t.join()
return errors
def test_encode_is_serialized(monkeypatch):
probe = _ConcurrencyProbe()
monkeypatch.setattr(embeddings, "_get", lambda model_name = None: _FakeModel(probe))
errors = _hammer(lambda: embeddings.encode(["alpha beta", "gamma"]))
assert errors == []
assert probe.saw_overlap is False # compute lock serialized encode()
def test_token_counter_is_serialized(monkeypatch):
probe = _ConcurrencyProbe()
monkeypatch.setattr(embeddings, "_get", lambda model_name = None: _FakeModel(probe))
count = embeddings.token_counter()
errors = _hammer(lambda: count("one two three four"))
assert errors == []
assert probe.saw_overlap is False # counting shares the tokenizer lock
def test_encode_enables_parallelism_only_during_call(monkeypatch):
seen = {}
class _M:
tokenizer = None
def encode(self, texts, **_kw):
seen["during"] = os.environ.get("TOKENIZERS_PARALLELISM")
return np.zeros((len(texts), 4), dtype = np.float32)
monkeypatch.setattr(embeddings, "_get", lambda model_name = None: _M())
os.environ["TOKENIZERS_PARALLELISM"] = "false"
embeddings.encode(["alpha", "beta"])
assert seen["during"] == "true" # rayon batch tokenization enabled in-call
assert os.environ.get("TOKENIZERS_PARALLELISM") == "false" # restored after
def test_token_counter_enables_parallelism_only_during_call(monkeypatch):
seen = {}
class _Tok:
def encode(self, text, **_kw):
seen["during"] = os.environ.get("TOKENIZERS_PARALLELISM")
return list(range(len(text.split())))
class _M:
tokenizer = _Tok()
monkeypatch.setattr(embeddings, "_get", lambda model_name = None: _M())
os.environ["TOKENIZERS_PARALLELISM"] = "false"
count = embeddings.token_counter()
count("alpha beta gamma")
assert seen["during"] == "true" # rayon enabled in-call, like _st_encode
assert os.environ.get("TOKENIZERS_PARALLELISM") == "false" # restored after
class _SentinelLlamaBackend:
"""Stand-in for LlamaServerBackend; never spawns a real server."""
def _force_st_load_failure(monkeypatch):
"""Make the ST warm-probe raise."""
def _boom(model_name = None):
raise RuntimeError("torch is broken on this machine")
monkeypatch.setattr(embeddings, "_get", _boom)
def _patch_llama_backend(monkeypatch, *, binary):
from core.inference.llama_cpp import LlamaCppBackend
from core.rag import embed_llama_server
monkeypatch.setattr(LlamaCppBackend, "_find_llama_server_binary", staticmethod(lambda: binary))
monkeypatch.setattr(embed_llama_server, "LlamaServerBackend", _SentinelLlamaBackend)
def test_st_failure_falls_back_to_llama_server(monkeypatch):
# ST can't load but llama-server is available -> use it.
_force_st_load_failure(monkeypatch)
_patch_llama_backend(monkeypatch, binary = "/fake/llama-server")
embeddings._reset_backend()
backend = embeddings._get_backend()
assert isinstance(backend, _SentinelLlamaBackend)
def test_st_failure_without_llama_binary_reraises(monkeypatch):
# No llama-server binary -> surface the failure, don't degrade to nothing.
_force_st_load_failure(monkeypatch)
_patch_llama_backend(monkeypatch, binary = None)
embeddings._reset_backend()
with pytest.raises(RuntimeError, match = "torch is broken"):
embeddings._get_backend()
def test_st_success_keeps_sentence_transformers(monkeypatch):
# Clean ST probe -> ST backend stays selected, no fallback.
monkeypatch.setattr(embeddings, "_get", lambda model_name = None: object())
_patch_llama_backend(monkeypatch, binary = "/fake/llama-server")
embeddings._reset_backend()
backend = embeddings._get_backend()
assert isinstance(backend, embeddings._SentenceTransformersBackend)
class _BoomOnEncodeModel:
"""Loads fine (init probe passes) but raises when encoding."""
tokenizer = None
def encode(self, texts, **_kw):
raise RuntimeError("CUDA error during encode")
def test_st_encode_runtime_failure_switches_to_llama(monkeypatch):
# encode() blows up mid-run -> switch to llama-server and stay switched.
monkeypatch.setattr(embeddings, "_get", lambda model_name = None: _BoomOnEncodeModel())
_patch_llama_backend(monkeypatch, binary = "/fake/llama-server")
calls = {}
def _sentinel_encode(
self,
texts,
*,
model_name = None,
normalize = True,
):
calls["used"] = True
return np.zeros((len(texts), 4), dtype = np.float32)
monkeypatch.setattr(_SentinelLlamaBackend, "encode", _sentinel_encode, raising = False)
embeddings._reset_backend()
out = embeddings.encode(["alpha", "beta"])
assert calls.get("used") is True # retried on the llama fallback
assert out.shape == (2, 4)
# Switch is process-wide: later calls keep using llama, not ST.
assert isinstance(embeddings._get_backend(), _SentinelLlamaBackend)
def test_st_encode_failure_without_llama_binary_reraises(monkeypatch):
# No llama-server binary -> surface the encode error.
monkeypatch.setattr(embeddings, "_get", lambda model_name = None: _BoomOnEncodeModel())
_patch_llama_backend(monkeypatch, binary = None)
embeddings._reset_backend()
with pytest.raises(RuntimeError, match = "CUDA error during encode"):
embeddings.encode(["alpha", "beta"])