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
223 lines
7.2 KiB
Python
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"])
|