From 3906f21955e9575a41dce2c02dfd12154a3d22d4 Mon Sep 17 00:00:00 2001 From: Sam Snelling Date: Wed, 16 Sep 2026 18:58:33 -0500 Subject: [PATCH] Add experimental NLI inference backend --- README.md | 30 ++ docs/api.md | 25 +- server/pyproject.toml | 4 + server/src/system_one_lite/api.py | 27 +- server/src/system_one_lite/engine.py | 9 +- server/src/system_one_lite/errors.py | 9 + server/src/system_one_lite/nli_engine.py | 342 +++++++++++++++++++++ server/tests/integration/test_api.py | 21 +- server/tests/integration/test_nli_model.py | 95 ++++++ server/tests/unit/test_nli_engine.py | 197 ++++++++++++ server/uv.lock | 328 ++++++++++++++++++++ 11 files changed, 1062 insertions(+), 25 deletions(-) create mode 100644 server/src/system_one_lite/errors.py create mode 100644 server/src/system_one_lite/nli_engine.py create mode 100644 server/tests/integration/test_nli_model.py create mode 100644 server/tests/unit/test_nli_engine.py diff --git a/README.md b/README.md index 38fb555..347e538 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,36 @@ uv run uvicorn system_one_lite.api:app --port 8010 The default profile uses `mlx-community/Qwen3-1.7B-4bit`. On first startup, the server downloads the pinned model revision and compiles the Metal kernels. +### Try the OpenJEV backend + +The experimental NLI backend keeps the same HTTP contract. It turns each +allowed answer into a premise-hypothesis pair, batches those pairs, and maps +their entailment probabilities back to Choice, Score, and Noul responses. + +```bash +cd server +uv sync --group nli +SYSTEM_ONE_BACKEND=nli uv run uvicorn system_one_lite.api:app --port 8010 +``` + +The first run downloads `AlexWortega/openjev` and loads its +`qwen3.5-4b-nli` checkpoint through Transformers. Override its settings with +`SYSTEM_ONE_NLI_MODEL`, `SYSTEM_ONE_NLI_SUBFOLDER`, +`SYSTEM_ONE_NLI_REVISION`, `SYSTEM_ONE_NLI_DEVICE`, `SYSTEM_ONE_NLI_BATCH_SIZE`, +`SYSTEM_ONE_NLI_MAX_LENGTH`, `SYSTEM_ONE_NLI_TEMPERATURE`, and +`SYSTEM_ONE_NLI_PREFIX_CACHE`. A custom model needs an explicit +`SYSTEM_ONE_NLI_REVISION`. + +This first experiment batches every candidate pair. It does not yet reuse the +shared premise cache by default, so batching lowers latency without removing +repeated premise computation. Set `SYSTEM_ONE_NLI_PREFIX_CACHE=1` to run each +candidate batch's shared causal prefix once. It then branches the option +suffixes from that cache. Float16 cached scores can differ slightly from +full-pair scores because the model runs in different chunk shapes. Cache mode +also scores one question at a time. This can reduce batching across separate +questions. The published checkpoint was trained on 256-token pairs, so longer +inputs need their own quality checks. The MLX backend remains the default. + Send a request to `POST /evaluate`: ```bash diff --git a/docs/api.md b/docs/api.md index 4ec1bbf..d1765f9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,8 +14,12 @@ a typed answer for every question, keyed by the IDs you chose. | `questions` | `map` | yes | At least one entry. Each key is an ID you pick; the answer comes back under it. The engine never sees your IDs. | There is no `model` field. The engine is fixed for the life of the server -process. Set `SYSTEM_ONE_MODEL=default` or `SYSTEM_ONE_MODEL=larger` before -startup. The response's `model` field reports the exact model that answered. +process. `SYSTEM_ONE_BACKEND=mlx` is the default. Set it to `nli` to use the +experimental OpenJEV backend. `SYSTEM_ONE_MODEL` selects an MLX profile or +model. `SYSTEM_ONE_NLI_MODEL`, `SYSTEM_ONE_NLI_SUBFOLDER`, and +`SYSTEM_ONE_NLI_REVISION` select an NLI checkpoint. The response's `model` +field reports the exact checkpoint that answered. A custom NLI model needs an +explicit revision. IDs only route answers back to your code. Two requests can use the same option names with different IDs, and renaming an ID never changes the probabilities. @@ -35,7 +39,7 @@ Chooses one option from your list. | --- | --- | --- | --- | | `type` | `"choice"` | yes | | | `instructions` | `string \| object \| array` | yes | What to decide. | -| `criteria` | `map` | yes | At least one option. Option name → description. `null` means the name says it all. The model sees both the name and description. Options are answer-coded `A`–`Z`, then `AA`, `AB`, …; at most 578 options. | +| `criteria` | `map` | yes | At least one option. Option name → description. `null` means the name says it all. The model sees both the name and description. The MLX backend supports at most 578 options in one Choice. The NLI backend allows 4,096 candidates across the whole request. | ```json "language": { @@ -99,7 +103,7 @@ How likely the statement is to be true. | --- | --- | --- | | `model` | `string` | The engine's own model ID. The default is `mlx-community/Qwen3-1.7B-4bit`. | | `answers` | `map` | One answer per question, keyed by your IDs, in request order. | -| `usage.input_tokens` | `integer` | Total tokens evaluated across the independent question prompts. | +| `usage.input_tokens` | `integer` | Tokens processed by the selected backend. MLX counts each independent question prompt. NLI counts each candidate pair, with reused prefix tokens counted once per candidate batch when prefix caching is on. | | `usage.output_tokens` | `integer` | Always 0. Nothing is generated. | ### Choice answer @@ -141,16 +145,17 @@ use an array of error objects. | Unknown `type` | Must be `choice`, `score`, or `noul`. | | Unknown request field | Misspelled and unsupported fields are rejected. | | Choice with no options | A Choice needs at least one option. | -| More options in a Choice than the engine has answer codes | Each option needs its own single-token answer code (`A`–`Z`, then two-letter codes). The model registry holds 578. | +| More options in a Choice than the MLX engine has answer codes | Each option needs its own single-token answer code (`A`–`Z`, then two-letter codes). The model registry holds 578. | +| More than 4,096 NLI candidates | The NLI limit counts every Choice option, Score level, and Noul side in the request. | | Fewer than 2 or more than 10 Score levels | The schema enforces both bounds. | | Content or ID over its character limit | State: 100,000; instructions and each criterion: 20,000; IDs and option names: 200. | | Request content over 1,000,000 bytes | The combined validated request must stay within the aggregate limit. | -| Prompt or request over its token limit | Each prompt may use at most 32,768 tokens; one request may use at most 131,072 input tokens. | +| MLX prompt or request over its token limit | Each prompt may use at most 32,768 tokens; one request may use at most 131,072 input tokens. | +| NLI pair over its token limit | Each formatted premise-hypothesis pair may use at most 4,096 tokens by default. Set `SYSTEM_ONE_NLI_MAX_LENGTH` to change this limit. | -If an answer code does not land as a single token, the server returns `422`. -You get an error, never a wrong answer. +With the MLX backend, an invalid single-token answer code returns `422`. Only one inference request runs at a time. If the engine is already in use, the server returns `503` with `detail: "the inference engine is busy"`. -Each question uses a full independent prompt. `usage.input_tokens` counts -the state again for each question. +MLX uses a full prompt for each question. NLI uses one pair for each candidate. +Without prefix caching, both backends process the state more than once. diff --git a/server/pyproject.toml b/server/pyproject.toml index 61aab65..a0040a8 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -24,6 +24,10 @@ test = [ labeling = [ "httpx>=0.28.1", ] +nli = [ + "torch>=2.9.0", + "transformers>=5.15.0", +] ocean = [ "numpy>=2.5.3", ] diff --git a/server/src/system_one_lite/api.py b/server/src/system_one_lite/api.py index 5144e80..19ea5ae 100644 --- a/server/src/system_one_lite/api.py +++ b/server/src/system_one_lite/api.py @@ -10,7 +10,7 @@ from fastapi import FastAPI, HTTPException -from .engine import Engine, RequestContractError +from .errors import RequestContractError from .prompts import as_text, confidence, label from .schemas import ( ChoiceAnswer, @@ -24,9 +24,26 @@ ) +def build_mlx_engine(model_id=None): + from .engine import Engine + + return Engine(model_id) + + +def build_nli_engine(model_id=None): + from .nli_engine import NLIEngine + + return NLIEngine(model_id) + + def configured_engine(): - """Build the process-wide engine from a named profile or exact model ID.""" - return Engine(os.environ.get("SYSTEM_ONE_MODEL")) + """Build the selected process-wide inference backend.""" + backend = os.environ.get("SYSTEM_ONE_BACKEND", "mlx") + if backend == "mlx": + return build_mlx_engine(os.environ.get("SYSTEM_ONE_MODEL")) + if backend == "nli": + return build_nli_engine(os.environ.get("SYSTEM_ONE_NLI_MODEL")) + raise ValueError(f"unknown SYSTEM_ONE_BACKEND: {backend!r}") def labels_for(question): @@ -63,8 +80,8 @@ def answer_for(question, probabilities): def create_app( - engine: Engine | None = None, - engine_factory: Callable[[], Engine] = Engine, + engine=None, + engine_factory: Callable[[], object] = build_mlx_engine, ) -> FastAPI: """Create an app whose model loads during startup, not module import.""" diff --git a/server/src/system_one_lite/engine.py b/server/src/system_one_lite/engine.py index 8646d93..68ba94f 100644 --- a/server/src/system_one_lite/engine.py +++ b/server/src/system_one_lite/engine.py @@ -19,6 +19,7 @@ from huggingface_hub import snapshot_download from mlx_lm import load +from .errors import RequestContractError, TooManyOptions from .prompts import chat_filled QWEN3_1_7B_MODEL = "mlx-community/Qwen3-1.7B-4bit" @@ -50,14 +51,6 @@ _REGISTERED_IDS = object() -class RequestContractError(ValueError): - pass - - -class TooManyOptions(RequestContractError): - pass - - def resolve_model_id(model_id): """Expand a built-in profile name, or keep an exact model ID or path.""" return MODEL_PROFILES.get(model_id, model_id) diff --git a/server/src/system_one_lite/errors.py b/server/src/system_one_lite/errors.py new file mode 100644 index 0000000..49239ff --- /dev/null +++ b/server/src/system_one_lite/errors.py @@ -0,0 +1,9 @@ +"""Errors that an inference backend may report to the HTTP layer.""" + + +class RequestContractError(ValueError): + pass + + +class TooManyOptions(RequestContractError): + pass diff --git a/server/src/system_one_lite/nli_engine.py b/server/src/system_one_lite/nli_engine.py new file mode 100644 index 0000000..5fbb660 --- /dev/null +++ b/server/src/system_one_lite/nli_engine.py @@ -0,0 +1,342 @@ +"""Batched natural-language-inference backend for System One requests.""" + +import math +import os +import time + +from .errors import RequestContractError, TooManyOptions + +DEFAULT_NLI_MODEL = "AlexWortega/openjev" +DEFAULT_NLI_SUBFOLDER = "qwen3.5-4b-nli" +DEFAULT_NLI_REVISION = "8c9db06441316f3fff7a68feb7da3fea79a5eff7" +DEFAULT_BATCH_SIZE = 8 +DEFAULT_MAX_LENGTH = 4096 +MAX_NLI_CANDIDATES = 4096 +PREMISE_TEMPLATE = "State:\n{state}\n\nQuestion:\n{instructions}" +HYPOTHESIS_TEMPLATE = "The correct answer is: {label}" + + +def nli_pairs(state, questions): + """Flatten questions into premise-hypothesis pairs and group sizes.""" + if not questions: + raise RequestContractError("at least one question is required") + + pairs = [] + group_sizes = [] + for instructions, labels in questions: + if not labels: + raise RequestContractError(f"{instructions!r} has no options") + premise = PREMISE_TEMPLATE.format(state=state.strip(), instructions=instructions.strip()) + group_sizes.append(len(labels)) + pairs.extend((premise, HYPOTHESIS_TEMPLATE.format(label=label.strip())) for label in labels) + + if len(pairs) > MAX_NLI_CANDIDATES: + raise TooManyOptions( + f"request has {len(pairs)} candidates; the NLI backend allows {MAX_NLI_CANDIDATES}" + ) + return pairs, group_sizes + + +def normalize_entailment(values, temperature=1.0): + """Turn independent entailment probabilities into one choice distribution.""" + if not values: + raise RequestContractError("an NLI question has no candidate scores") + if not math.isfinite(temperature) or temperature <= 0: + raise ValueError("NLI temperature must be finite and positive") + if any(not math.isfinite(value) or not 0 <= value <= 1 for value in values): + raise ValueError("NLI entailment probabilities must be finite and in [0, 1]") + + log_weights = [math.log(max(value, 1e-12)) / temperature for value in values] + peak = max(log_weights) + weights = [math.exp(value - peak) for value in log_weights] + total = sum(weights) + return [value / total for value in weights] + + +def select_device(torch, requested=None): + """Choose an available PyTorch device unless the caller chose one.""" + if requested and requested != "auto": + return requested + if torch.cuda.is_available(): + return "cuda" + if torch.backends.mps.is_available(): + return "mps" + return "cpu" + + +def select_dtype(torch, device): + device_type = torch.device(device).type + if device_type == "cuda": + return torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16 + if device_type == "mps": + return torch.float16 + return torch.bfloat16 + + +def common_token_prefix(rows): + """Return the number of leading token IDs shared by every row.""" + if not rows: + return 0 + limit = min(len(row) for row in rows) + index = 0 + while index < limit and all(row[index] == rows[0][index] for row in rows[1:]): + index += 1 + return index + + +def resolve_nli_revision(model_id, revision=None): + """Pin the built-in model and require a pin for custom repositories.""" + if revision: + return revision + if model_id == DEFAULT_NLI_MODEL: + return DEFAULT_NLI_REVISION + raise ValueError("SYSTEM_ONE_NLI_REVISION is required for a custom NLI model") + + +def resolve_entailment_index(config): + """Find the entailment class without assuming one label order.""" + for label, index in getattr(config, "label2id", {}).items(): + if str(label).strip().casefold() == "entailment": + return int(index) + raise ValueError("NLI model config must define an entailment label") + + +def load_nli_assets(auto_tokenizer, auto_model, model_id, subfolder, revision, dtype): + """Load the tokenizer and classifier from the same pinned Hub revision.""" + tokenizer = auto_tokenizer.from_pretrained( + model_id, + subfolder=subfolder, + revision=revision, + ) + model = auto_model.from_pretrained( + model_id, + subfolder=subfolder, + revision=revision, + dtype=dtype, + ) + return tokenizer, model + + +class HuggingFaceNLIScorer: + """Load OpenJEV and return one entailment probability per text pair.""" + + def __init__( + self, + model_id=DEFAULT_NLI_MODEL, + subfolder=DEFAULT_NLI_SUBFOLDER, + revision=None, + device=None, + batch_size=DEFAULT_BATCH_SIZE, + max_length=DEFAULT_MAX_LENGTH, + prefix_cache=False, + ): + revision = resolve_nli_revision(model_id, revision) + try: + import torch + from transformers import AutoModelForSequenceClassification, AutoTokenizer + except ImportError as error: + raise RuntimeError( + "the NLI backend needs the nli dependency group; run uv sync --group nli" + ) from error + + if batch_size < 1 or max_length < 1: + raise ValueError("NLI batch size and maximum length must be positive") + self.torch = torch + self.model_id = model_id + self.subfolder = subfolder + self.revision = revision + self.device = select_device(torch, device) + self.batch_size = batch_size + self.max_length = max_length + self.prefix_cache = prefix_cache + self.tokenizer, self.model = load_nli_assets( + AutoTokenizer, + AutoModelForSequenceClassification, + model_id, + subfolder, + self.revision, + select_dtype(torch, self.device), + ) + if self.tokenizer.pad_token is None: + self.tokenizer.pad_token = self.tokenizer.eos_token + self.tokenizer.padding_side = "right" + text_config = self.model.config.get_text_config() + if text_config.pad_token_id is None: + text_config.pad_token_id = self.tokenizer.pad_token_id + self.entailment_index = resolve_entailment_index(self.model.config) + self.model.to(self.device).eval() + self.template = self.model.config.nli_template + + def score_pairs(self, pairs): + """Return entailment probabilities and the repeated-pair token count.""" + probabilities = [] + input_tokens = 0 + torch = self.torch + with torch.inference_mode(): + for start in range(0, len(pairs), self.batch_size): + group = pairs[start : start + self.batch_size] + texts = [ + self.template.format(premise=premise, hypothesis=hypothesis) + for premise, hypothesis in group + ] + encoded = self.tokenizer( + texts, + truncation=False, + padding=True, + return_tensors="pt", + ) + lengths = encoded["attention_mask"].sum(dim=1) + longest = int(lengths.max().item()) + if longest > self.max_length: + raise RequestContractError( + f"NLI pair has {longest} tokens; limit is {self.max_length}" + ) + input_tokens += int(lengths.sum().item()) + encoded = {key: value.to(self.device) for key, value in encoded.items()} + logits = self.model(**encoded).logits.float() + values = torch.softmax(logits, dim=-1)[:, self.entailment_index] + probabilities.extend(values.cpu().tolist()) + return probabilities, input_tokens + + def score_grouped_pairs(self, pairs, group_sizes): + """Score each question with optional exact shared-prefix reuse.""" + if not self.prefix_cache: + return self.score_pairs(pairs) + + probabilities = [] + input_tokens = 0 + offset = 0 + for size in group_sizes: + group = pairs[offset : offset + size] + for start in range(0, len(group), self.batch_size): + values, tokens = self._score_cached_group(group[start : start + self.batch_size]) + probabilities.extend(values) + input_tokens += tokens + offset += size + return probabilities, input_tokens + + def _score_cached_group(self, pairs): + """Run a shared causal prefix once, then batch the option suffixes.""" + if len(pairs) < 2: + return self.score_pairs(pairs) + + texts = [ + self.template.format(premise=premise, hypothesis=hypothesis) + for premise, hypothesis in pairs + ] + rows = [self.tokenizer.encode(text, truncation=False) for text in texts] + longest = max(len(row) for row in rows) + if longest > self.max_length: + raise RequestContractError(f"NLI pair has {longest} tokens; limit is {self.max_length}") + shared = common_token_prefix(rows) + shared = min(shared, min(len(row) - 1 for row in rows)) + if shared <= 0: + return self.score_pairs(pairs) + + torch = self.torch + prefix_ids = torch.tensor([rows[0][:shared]], dtype=torch.long, device=self.device) + prefix_mask = torch.ones_like(prefix_ids) + suffixes = [row[shared:] for row in rows] + suffix_length = max(len(row) for row in suffixes) + suffix_ids = torch.full( + (len(rows), suffix_length), + self.tokenizer.pad_token_id, + dtype=torch.long, + device=self.device, + ) + suffix_mask = torch.zeros_like(suffix_ids) + for index, row in enumerate(suffixes): + suffix_ids[index, : len(row)] = torch.tensor(row, dtype=torch.long, device=self.device) + suffix_mask[index, : len(row)] = 1 + full_mask = torch.cat( + [ + torch.ones( + (len(rows), shared), + dtype=suffix_mask.dtype, + device=self.device, + ), + suffix_mask, + ], + dim=1, + ) + + with torch.inference_mode(): + prefix_output = self.model( + input_ids=prefix_ids, + attention_mask=prefix_mask, + use_cache=True, + ) + cache = prefix_output.past_key_values + if cache is None: + raise RuntimeError("OpenJEV did not return a prefix cache") + branch_indices = torch.zeros(len(rows), dtype=torch.long, device=self.device) + cache.reorder_cache(branch_indices) + logits = self.model( + input_ids=suffix_ids, + attention_mask=full_mask, + past_key_values=cache, + use_cache=False, + ).logits.float() + values = torch.softmax(logits, dim=-1)[:, self.entailment_index] + processed_tokens = shared + sum(len(row) for row in suffixes) + return values.cpu().tolist(), processed_tokens + + +class NLIEngine: + """Map batched OpenJEV scores onto the existing decision-engine contract.""" + + def __init__(self, model_id=None, scorer=None, temperature=None): + model_id = model_id or DEFAULT_NLI_MODEL + if scorer is None: + scorer = HuggingFaceNLIScorer( + model_id=model_id, + subfolder=os.environ.get("SYSTEM_ONE_NLI_SUBFOLDER", DEFAULT_NLI_SUBFOLDER), + revision=os.environ.get("SYSTEM_ONE_NLI_REVISION"), + device=os.environ.get("SYSTEM_ONE_NLI_DEVICE", "auto"), + batch_size=int(os.environ.get("SYSTEM_ONE_NLI_BATCH_SIZE", DEFAULT_BATCH_SIZE)), + max_length=int(os.environ.get("SYSTEM_ONE_NLI_MAX_LENGTH", DEFAULT_MAX_LENGTH)), + prefix_cache=os.environ.get("SYSTEM_ONE_NLI_PREFIX_CACHE", "0") == "1", + ) + self.scorer = scorer + scorer_model_id = getattr(scorer, "model_id", model_id) + scorer_subfolder = getattr(scorer, "subfolder", None) + scorer_revision = getattr(scorer, "revision", None) + if scorer_subfolder and scorer_revision: + self.model_id = f"{scorer_model_id}:{scorer_subfolder}@{scorer_revision}" + else: + self.model_id = scorer_model_id + self.temperature = float( + temperature + if temperature is not None + else os.environ.get("SYSTEM_ONE_NLI_TEMPERATURE", "1.0") + ) + if not math.isfinite(self.temperature) or self.temperature <= 0: + raise ValueError("NLI temperature must be finite and positive") + + def evaluate(self, state, questions, template=None): + if template is not None: + raise RequestContractError("the NLI backend does not accept answer-slot templates") + started = time.perf_counter() + pairs, group_sizes = nli_pairs(state, questions) + grouped_scorer = getattr(self.scorer, "score_grouped_pairs", None) + if grouped_scorer is None: + entailment, input_tokens = self.scorer.score_pairs(pairs) + else: + entailment, input_tokens = grouped_scorer(pairs, group_sizes) + if len(entailment) != len(pairs): + raise RuntimeError( + f"NLI scorer returned {len(entailment)} scores for {len(pairs)} candidates" + ) + + results = [] + offset = 0 + for size in group_sizes: + results.append( + normalize_entailment( + entailment[offset : offset + size], + temperature=self.temperature, + ) + ) + offset += size + elapsed_ms = (time.perf_counter() - started) * 1000 + return results, input_tokens, elapsed_ms diff --git a/server/tests/integration/test_api.py b/server/tests/integration/test_api.py index bfc7f5e..fca5eb0 100644 --- a/server/tests/integration/test_api.py +++ b/server/tests/integration/test_api.py @@ -6,7 +6,7 @@ from system_one_lite import api as api_module from system_one_lite.api import app, create_app -from system_one_lite.engine import RequestContractError +from system_one_lite.errors import RequestContractError from system_one_lite.schemas import ( MAX_QUESTIONS, MAX_REQUEST_BYTES, @@ -42,13 +42,30 @@ def test_module_import_does_not_load_engine(): def test_configured_engine_uses_selected_profile(monkeypatch): selected = [] + monkeypatch.setenv("SYSTEM_ONE_BACKEND", "mlx") monkeypatch.setenv("SYSTEM_ONE_MODEL", "larger") - monkeypatch.setattr(api_module, "Engine", selected.append) + monkeypatch.setattr(api_module, "build_mlx_engine", selected.append) assert api_module.configured_engine() is None assert selected == ["larger"] +def test_configured_engine_uses_nli_backend(monkeypatch): + selected = [] + monkeypatch.setenv("SYSTEM_ONE_BACKEND", "nli") + monkeypatch.setenv("SYSTEM_ONE_NLI_MODEL", "example/nli") + monkeypatch.setattr(api_module, "build_nli_engine", selected.append) + + assert api_module.configured_engine() is None + assert selected == ["example/nli"] + + +def test_configured_engine_rejects_unknown_backend(monkeypatch): + monkeypatch.setenv("SYSTEM_ONE_BACKEND", "mystery") + with pytest.raises(ValueError, match="unknown SYSTEM_ONE_BACKEND"): + api_module.configured_engine() + + def test_quickstart_mixed_three_questions(): """The quickstart.md request: Choice + Score + Noul, one pass, full shape.""" r = evaluate( diff --git a/server/tests/integration/test_nli_model.py b/server/tests/integration/test_nli_model.py new file mode 100644 index 0000000..92125e5 --- /dev/null +++ b/server/tests/integration/test_nli_model.py @@ -0,0 +1,95 @@ +"""Opt-in checks against the full OpenJEV checkpoint.""" + +import os + +import pytest +from fastapi.testclient import TestClient + +from system_one_lite.api import create_app +from system_one_lite.errors import RequestContractError +from system_one_lite.nli_engine import HuggingFaceNLIScorer, NLIEngine + +pytestmark = pytest.mark.skipif( + os.environ.get("SYSTEM_ONE_RUN_NLI_MODEL") != "1", + reason="set SYSTEM_ONE_RUN_NLI_MODEL=1 to load the OpenJEV checkpoint", +) + + +@pytest.fixture(scope="module") +def scorer(): + return HuggingFaceNLIScorer( + device=os.environ.get("SYSTEM_ONE_NLI_DEVICE", "auto"), + batch_size=8, + max_length=512, + ) + + +def sample_pairs(): + state = "The parcel was due Friday and remains in transit. " * 12 + premise = f"State:\n{state}\n\nQuestion:\nWhich team should handle this request?" + return [ + (premise, "The correct answer is: deliveries — Late or missing orders"), + (premise, "The correct answer is: billing — Charges or refunds"), + (premise, "The correct answer is: account — Login or profile problems"), + ] + + +def test_prefix_cache_matches_full_pairs_and_processes_fewer_tokens(scorer): + pairs = sample_pairs() + previous_batch_size = scorer.batch_size + scorer.batch_size = 2 + try: + scorer.prefix_cache = False + full, full_tokens = scorer.score_grouped_pairs(pairs, [len(pairs)]) + scorer.prefix_cache = True + cached, cached_tokens = scorer.score_grouped_pairs(pairs, [len(pairs)]) + finally: + scorer.batch_size = previous_batch_size + + assert cached == pytest.approx(full, abs=2e-3) + assert cached_tokens < full_tokens + + +def test_pairs_over_the_configured_token_limit_are_rejected(scorer): + previous_limit = scorer.max_length + scorer.max_length = 8 + try: + with pytest.raises(RequestContractError, match="limit is 8"): + scorer.score_pairs(sample_pairs()[:1]) + finally: + scorer.max_length = previous_limit + + +def test_openjev_backend_serves_existing_api_shape(scorer): + scorer.prefix_cache = True + engine = NLIEngine(scorer=scorer) + app = create_app(engine=engine) + with TestClient(app) as client: + response = client.post( + "/evaluate", + json={ + "state": "The parcel was due Friday and remains in transit.", + "questions": { + "team": { + "type": "choice", + "instructions": "Which team should handle this request?", + "criteria": { + "deliveries": "Late or missing orders", + "billing": "Charges or refunds", + }, + }, + "needs_reply": { + "type": "noul", + "instructions": "Does the customer need a reply?", + }, + }, + }, + ) + + assert response.status_code == 200 + body = response.json() + assert body["model"].startswith("AlexWortega/openjev:qwen3.5-4b-nli@") + assert set(body["answers"]) == {"team", "needs_reply"} + assert sum(body["answers"]["team"]["probabilities"].values()) == pytest.approx(1.0) + assert 0 <= body["answers"]["needs_reply"]["noul"] <= 1 + assert body["usage"]["input_tokens"] > 0 diff --git a/server/tests/unit/test_nli_engine.py b/server/tests/unit/test_nli_engine.py new file mode 100644 index 0000000..3addc2b --- /dev/null +++ b/server/tests/unit/test_nli_engine.py @@ -0,0 +1,197 @@ +"""NLI backend tests that do not load the real checkpoint.""" + +import math + +import pytest + +from system_one_lite.errors import RequestContractError, TooManyOptions +from system_one_lite.nli_engine import ( + MAX_NLI_CANDIDATES, + NLIEngine, + common_token_prefix, + load_nli_assets, + nli_pairs, + normalize_entailment, + resolve_entailment_index, + resolve_nli_revision, + select_dtype, +) + + +class FakeScorer: + model_id = "fake/openjev" + + def __init__(self, values, input_tokens=123): + self.values = values + self.input_tokens = input_tokens + self.pairs = None + + def score_pairs(self, pairs): + self.pairs = pairs + return self.values, self.input_tokens + + +class FakeTorch: + bfloat16 = "bfloat16" + float16 = "float16" + + class cuda: + @staticmethod + def is_bf16_supported(): + return True + + @staticmethod + def device(value): + return type("Device", (), {"type": value.split(":", 1)[0]})() + + +class FakeLoader: + calls = [] + + @classmethod + def from_pretrained(cls, *args, **kwargs): + cls.calls.append((args, kwargs)) + return cls + + +def test_common_token_prefix(): + assert common_token_prefix([[1, 2, 3], [1, 2, 4], [1, 2]]) == 2 + assert common_token_prefix([]) == 0 + assert common_token_prefix([[1], [2]]) == 0 + + +@pytest.mark.parametrize( + ("device", "expected"), + [("cuda:0", "bfloat16"), ("mps:0", "float16"), ("cpu", "bfloat16")], +) +def test_dtype_uses_qualified_device_type(device, expected): + assert select_dtype(FakeTorch, device) == expected + + +def test_huggingface_assets_use_the_same_pinned_revision(): + class FakeTokenizerLoader(FakeLoader): + calls = [] + + class FakeModelLoader(FakeLoader): + calls = [] + + tokenizer, model = load_nli_assets( + FakeTokenizerLoader, + FakeModelLoader, + "example/model", + "classifier", + "abc123", + "float16", + ) + + assert tokenizer is FakeTokenizerLoader + assert model is FakeModelLoader + assert FakeTokenizerLoader.calls == [ + (("example/model",), {"subfolder": "classifier", "revision": "abc123"}) + ] + assert FakeModelLoader.calls == [ + ( + ("example/model",), + {"subfolder": "classifier", "revision": "abc123", "dtype": "float16"}, + ) + ] + + +def test_builtin_revision_is_pinned_and_custom_model_requires_one(): + assert resolve_nli_revision("AlexWortega/openjev") == ( + "8c9db06441316f3fff7a68feb7da3fea79a5eff7" + ) + assert resolve_nli_revision("example/model", "abc123") == "abc123" + with pytest.raises(ValueError, match="required for a custom NLI model"): + resolve_nli_revision("example/model") + + +def test_entailment_index_comes_from_model_config(): + config = type("Config", (), {"label2id": {"neutral": 0, "ENTAILMENT": 2}})() + assert resolve_entailment_index(config) == 2 + + missing = type("Config", (), {"label2id": {"LABEL_0": 0}})() + with pytest.raises(ValueError, match="define an entailment label"): + resolve_entailment_index(missing) + + +def test_pairs_share_one_premise_per_question(): + pairs, sizes = nli_pairs( + "A parcel is late.", + [ + ("Who should handle it?", ["delivery", "billing"]), + ("Is a reply needed?", ["yes", "no"]), + ], + ) + + assert sizes == [2, 2] + assert len(pairs) == 4 + assert pairs[0][0] == pairs[1][0] + assert pairs[2][0] == pairs[3][0] + assert pairs[0][0] != pairs[2][0] + assert pairs[0][1] == "The correct answer is: delivery" + + +def test_entailment_normalization_preserves_ranking(): + probabilities = normalize_entailment([0.1, 0.6, 0.3]) + + assert probabilities == pytest.approx([0.1, 0.6, 0.3]) + assert sum(probabilities) == pytest.approx(1.0) + + +def test_temperature_changes_distribution_not_winner(): + cool = normalize_entailment([0.2, 0.8], temperature=0.5) + warm = normalize_entailment([0.2, 0.8], temperature=2.0) + + assert cool[1] > warm[1] > 0.5 + + +@pytest.mark.parametrize("values", [[math.nan], [-0.1], [1.1]]) +def test_invalid_entailment_scores_rejected(values): + with pytest.raises(ValueError, match="probabilities"): + normalize_entailment(values) + + +def test_engine_batches_every_candidate_in_one_scorer_call(): + scorer = FakeScorer([0.1, 0.7, 0.2, 0.8, 0.2]) + engine = NLIEngine(scorer=scorer) + + results, input_tokens, elapsed_ms = engine.evaluate( + "A parcel is late.", + [ + ("Who should handle it?", ["delivery", "billing", "account"]), + ("Is a reply needed?", ["yes", "no"]), + ], + ) + + assert engine.model_id == "fake/openjev" + assert len(scorer.pairs) == 5 + assert results[0] == pytest.approx([0.1, 0.7, 0.2]) + assert results[1] == pytest.approx([0.8, 0.2]) + assert input_tokens == 123 + assert elapsed_ms >= 0 + + +def test_engine_rejects_wrong_score_count(): + engine = NLIEngine(scorer=FakeScorer([0.5])) + with pytest.raises(RuntimeError, match="1 scores for 2 candidates"): + engine.evaluate("state", [("question", ["one", "two"])]) + + +def test_empty_questions_and_options_rejected(): + with pytest.raises(RequestContractError, match="at least one"): + nli_pairs("state", []) + with pytest.raises(RequestContractError, match="no options"): + nli_pairs("state", [("question", [])]) + + +def test_request_candidate_limit(): + labels = [f"option {index}" for index in range(MAX_NLI_CANDIDATES + 1)] + with pytest.raises(TooManyOptions, match=str(MAX_NLI_CANDIDATES)): + nli_pairs("state", [("question", labels)]) + + +def test_answer_slot_template_rejected(): + engine = NLIEngine(scorer=FakeScorer([1.0])) + with pytest.raises(RequestContractError, match="does not accept"): + engine.evaluate("state", [("question", ["yes"])], template=object()) diff --git a/server/uv.lock b/server/uv.lock index 57b9792..b3c1d60 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -60,6 +60,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cuda-bindings" +version = "13.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/d0/76d0e45d98bf4933bf48eac6bbeb17464684540f69edec41fc37c7a422b0/cuda_bindings-13.4.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84ee88862e2e6ac39a5434c061f7f4389fbefbc418487d0670c86601d517d038", size = 6479976, upload-time = "2026-09-10T01:16:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/43/56/d7b219516980f3333e232c13d727e8f4dc59afc5381cbbcfbc1215014c81/cuda_bindings-13.4.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f444d7e488cbc47e79b7be0d1cfe97201f3e1f186a18ee10f2e4da3265975b16", size = 7170956, upload-time = "2026-09-10T01:16:56.854Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/165b4d449f94956c2a60930cf5dfeb27132ead60a7e7f2c37819df1cba07/cuda_bindings-13.4.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b601c0cbf0dffb648f68e56a60b320738a20210293f33896a1964a6438cc65f1", size = 6313772, upload-time = "2026-09-10T01:17:03.969Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/cf021d1560541caa1f35f3e7e311d2678dbacb4fa6a4573b63470fe1ae00/cuda_bindings-13.4.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2c357698588b06ebd65811ee2013b6650dd0a10d16d899924aabad0d606d76", size = 6924300, upload-time = "2026-09-10T01:17:06.23Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/74346b49114779920929ec0ea1361f0a0357262d6020cfdd017f670da638/cuda_bindings-13.4.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7df2dddb81feb15787e8c4a13b7aa3f4c23eabeec7746037e94dbbed065fc6c", size = 6406821, upload-time = "2026-09-10T01:17:12.344Z" }, + { url = "https://files.pythonhosted.org/packages/f6/77/2f9a38be7399a34e3703b6ce1be60bb2c56611d324750f8d544ff90b0473/cuda_bindings-13.4.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62df23df11074e9833bf348bbcf0b8eec2fcbded4f305c6fbaa3ed067433e97d", size = 6972525, upload-time = "2026-09-10T01:17:15.098Z" }, + { url = "https://files.pythonhosted.org/packages/7d/de/41197aebf91f6c5f82b35e06e3b4bbe08edddb335c4d6e53c1f1fe542e0f/cuda_bindings-13.4.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0947a6c491a622b076e9afcbc0444eefd9cbe001630558bbcba8d0a90dab7fe7", size = 6243282, upload-time = "2026-09-10T01:17:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/be/d6/1b697092f53cfd4d721fbbe13e7d66dc72a9d508156af9be4c86607e4373/cuda_bindings-13.4.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e730e80d997b6037566033a79aae3995e1981654054536f512e5c091679c70", size = 6803245, upload-time = "2026-09-10T01:17:23.309Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/e6/22df83f82f9bc26cb1c42265cf14d34d4908dba2a0f261bd7b28244acb00/cuda_pathfinder-1.8.1-py3-none-any.whl", hash = "sha256:ae0137ff9e56ea97499bcbf54f5f2778ec25f3266715ac86da192a795af982a8", size = 62552, upload-time = "2026-09-02T16:55:28.64Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +curand = [ + { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, +] + [[package]] name = "fastapi" version = "0.141.1" @@ -362,6 +437,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/cd/4e50bf325100e7165e13d025f264362bf0009196269f9eaf87f2c6e738a2/mlx_metal-0.32.2-py3-none-macosx_26_0_arm64.whl", hash = "sha256:e6abeac9ac5265830c9c1541b6f96e9be37a85c2446763a46ad466c63a3837ab", size = 64367860, upload-time = "2026-08-25T10:31:28.056Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "numpy" version = "2.5.3" @@ -435,6 +528,158 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, ] +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.24.0.43" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ba/791cffd048fe5b044e620df55267e3e95c0e6e07d50b41e377c03dfc910f/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:71f181cd810e90f9b6023b01186fe82d13d65f0ec098581ee201d39fad769e4b", size = 553099438, upload-time = "2026-07-02T16:27:42.58Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.30.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/21/a73174c6157101bdf1ffc22b517f76ff0082613989dd9bc8f43e8034caac/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:ca786ffa5a647c75d4d1f5cc72a6c4f537947e2ba8823d7c8aaf768e7a7b9f77", size = 215983881, upload-time = "2026-06-09T03:23:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/c500f90c7ae641b8e0f98965b36b8a7ac79cc8b296e8d251fe3eb592ee54/nvidia_nccl_cu13-2.30.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:cefa7fdb9710efd0f39c5f1be1d61ff6fc9a996c451265bd7fbdcf9455ed4b50", size = 215965170, upload-time = "2026-06-09T03:23:39.73Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.4.92" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6b/eef7a9e32872b8f41e145bf10cddc9af26e153c338852811fe9a9baddf9e/nvidia_nvjitlink-13.4.92-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e0391f24ed94ec879b84e3da4d4ec320c879aff681f2c7a638462f7199284323", size = 42452378, upload-time = "2026-09-16T20:45:29.042Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a8/1cbd4014898af8b419e69b0d7dbc63da2121ee92d92b47d59f4fe9075349/nvidia_nvjitlink-13.4.92-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:25f74fad0d654271c921ac4dca614bd6258bc21791242fc7b2289dad7ae9c099", size = 40420120, upload-time = "2026-09-16T20:45:19.163Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -838,6 +1083,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/a1/70007fef3f818c688de4a730f98024a671599ab67f20270f8efb03d69dcc/sentencepiece-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fa9f5ef0e2a82233dd0b8b32ea3f5710e0c44afbc07ed3620219f32601e56090", size = 1302760, upload-time = "2026-07-12T08:39:32.457Z" }, ] +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -860,6 +1114,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "system-one-lite" version = "0.1.0" @@ -882,6 +1148,10 @@ dev = [ labeling = [ { name = "httpx" }, ] +nli = [ + { name = "torch" }, + { name = "transformers" }, +] ocean = [ { name = "numpy" }, ] @@ -907,6 +1177,10 @@ dev = [ { name = "ruff", specifier = ">=0.13.0" }, ] labeling = [{ name = "httpx", specifier = ">=0.28.1" }] +nli = [ + { name = "torch", specifier = ">=2.9.0" }, + { name = "transformers", specifier = ">=5.15.0" }, +] ocean = [{ name = "numpy", specifier = ">=2.5.3" }] test = [ { name = "httpx", specifier = ">=0.28.1" }, @@ -940,6 +1214,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/b0/dee84cb44175be1b4c35bd2f770727494e78f0bb38e571a623ade94dbebb/tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa", size = 2729352, upload-time = "2026-09-03T08:55:44.345Z" }, ] +[[package]] +name = "torch" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/76/bb4770f56cf6d8971671dbcbb7493e5a6a15ad2825f4e359b02c27c38297/torch-2.14.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c1f844f1c750e87df4b68bc3afbc0e2b0c7ef19d7b8f666e48bdcf6a0c4f0056", size = 127303200, upload-time = "2026-09-02T13:43:20.311Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7b/ec44bacf2c8886b85ba4ca2285e8b09f2dff5d9c99e6a031326954082cb5/torch-2.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ada340e62591d06a2bcc2d68170f20f45f0b0665d372dc510a8ed7eb3b1d609a", size = 454010251, upload-time = "2026-09-02T13:44:24.927Z" }, + { url = "https://files.pythonhosted.org/packages/15/71/49399acd41f750a906c686bd23c08a2001ccb8dd25f2971003c2ed89c1dd/torch-2.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fecffb58f51fd643d213acd68da21cc3fc19bea05a3bc64b4ee55128f47a4963", size = 554620488, upload-time = "2026-09-02T13:44:49.442Z" }, + { url = "https://files.pythonhosted.org/packages/be/16/9489b137112040f9911d7527e452854f21cc4e499ce0da79864e6a7451a7/torch-2.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:cad84f41bbdf3dcf333ce394aeeaf25237c4d94fd6b659ba5eb813c829978823", size = 124114011, upload-time = "2026-09-02T13:43:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/0db773452c2a62b37761d3f418acf933d381f9e87077036fb57c2a386c37/torch-2.14.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9d4b1022a5d9b71282ec67ad0d9e7235870096b8a246dc1c32d6ea1fc83dc998", size = 127311393, upload-time = "2026-09-02T13:43:59.607Z" }, + { url = "https://files.pythonhosted.org/packages/13/36/537fd9da2adad49e7b2bb20741398625bee548493158274e87369a8eed56/torch-2.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:731b9ebdea402b8b1996d4c2ae613b16660e559b19e47bdc45d970568bc91c53", size = 454010525, upload-time = "2026-09-02T13:45:03.719Z" }, + { url = "https://files.pythonhosted.org/packages/21/f1/39bd13b21f57d1982b7f3ddf663f01c7266e2957714880744eba9e8c8d11/torch-2.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:84bf384779a10c02fc3c6bdbab71a9cb66b0dd93c652d1ed5d6dfc0cb37e5962", size = 554619993, upload-time = "2026-09-02T13:45:28.209Z" }, + { url = "https://files.pythonhosted.org/packages/89/a8/683d9c44737554b67ca76dd2db4f42258a0f014246cb511293e51e0154bd/torch-2.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0e7cf18cb0d8bd666b6120932e29c7aef3502b61a08b44da4839580c539a7cdb", size = 124113865, upload-time = "2026-09-02T13:44:33.705Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/1241e7db5ccc2455f8735bd6b1becfad39916206ad18001c4c0014d139e2/torch-2.14.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:860423e970f2ce02c4476e8e2d1350131b1c5c5a5e4912180e78b50b53241efa", size = 127321431, upload-time = "2026-09-02T13:44:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/bbeba65e3fbb4b8f61ea19cf4ebea9f4268cc6fe64e6f7d38bfb6cc152eb/torch-2.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b985c7defeb8d28691b7513ebaecc64946c5f68ec770e9f4ddba39688864f46a", size = 454027631, upload-time = "2026-09-02T13:45:43.73Z" }, + { url = "https://files.pythonhosted.org/packages/50/75/8d2b9a7e724759470c209489b79260cac537f091cc9aac8001c8d2bc845c/torch-2.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b2cd92bce63d40bf6fc2e5d840fc2f0063bd180a242daa761cb6088cc2f46e27", size = 554623549, upload-time = "2026-09-02T13:46:04.252Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/a5475c00b0555e686333e6b4036f2213e7cbea021a772ce6f9ced4dcbd2f/torch-2.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:44b044b9f6f633d982839422a57433d6a1da520037fd88e0c8a47efde589b3b8", size = 124110863, upload-time = "2026-09-02T13:45:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/a5/49/bbaee76337742a42d2c5b0296ea62252567050e1d821bc2283b2e72ba6fb/torch-2.14.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:553aec938d37d77b783bcf801e638cad068870e7643c47eadac21eb180f551ed", size = 127653463, upload-time = "2026-09-02T13:45:17.1Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/6cc7a511bab384fbe8f4b8ddeecdd22e724b2162d6f15076f07a0a7ef15b/torch-2.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cd8cd8f714d511ccdca907282d1da3d9be8322d4e1520b9c3bce39a5c1318a4b", size = 454009927, upload-time = "2026-09-02T13:46:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/b6/da/0f04fe15fd05bf3f613a359b14acb44c35ae06fe69da0aefa8b5cdb4f9f9/torch-2.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d2526f71e6638133b97b3cd2881ece3df521460a4727030e8ae2a72a7d3ae31d", size = 554580530, upload-time = "2026-09-02T13:46:34.818Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c3/72ae1f02747b1f012e1975743e48cd608f83095d7f9ce58de78b79248b35/torch-2.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:731784e3914843c6bcc7aba3987ff7610ac57dbbc816a5d6b9b62e04c240a641", size = 124400194, upload-time = "2026-09-02T13:45:53.555Z" }, +] + [[package]] name = "tqdm" version = "4.70.1" @@ -972,6 +1285,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/d0/c502b60d684adbd98a8dc7d5bb866842772b816ac4354e4608be240041ae/transformers-5.17.0-py3-none-any.whl", hash = "sha256:78ec1ce21579b38dfb83950a0658cd119f87212a2fcfdff478096ce9d6c03801", size = 12295140, upload-time = "2026-09-09T15:39:53.746Z" }, ] +[[package]] +name = "triton" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/07/0f8cd8e8db0472334253efdaaab3d0819fea27aa99bf0e7f1aeea4ceb5ae/triton-3.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9c404c69ed4a39e8ec632eaf6b9fe058a060bf98979c177f6ef666f06bb8d50", size = 226474486, upload-time = "2026-08-28T16:08:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/c1/09/b7012e5bfae67640f268aa584caa80fe1674f6b0da949046b679972c33e3/triton-3.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91ffa46d095b252248297292dd22bcbacd53a125a0c2eefbbbf74925a320bc3", size = 247972921, upload-time = "2026-08-28T15:55:53.157Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/4c564374bcdadb166fccbf3e45aee0d4a473f88d341761bd2fefe3b8e8c1/triton-3.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7004666652f500ed854a86988e4b3d69d247188b5d2092b5df1e44f4a954099", size = 226476793, upload-time = "2026-08-28T16:08:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/3394d5548404c1cabd1dadadd28d0b3f9478db1dff8180da53bb3f0a1e19/triton-3.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f0497218e26b7d79773ad9c2a3fa3b539ee69f587a13fac2e552b1d322a8015", size = 247975122, upload-time = "2026-08-28T15:56:04.112Z" }, + { url = "https://files.pythonhosted.org/packages/b8/59/bf0e9493118bb353ab59a5d6a65db3618d9b314417cc1459f0121e0ec5c9/triton-3.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f6b48d0591929a3867973acac3dccd4e058585f91bfb41022de496c9ffab304", size = 226488654, upload-time = "2026-08-28T16:08:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/93/d9/08c75f3459f19ad00425b564058e40efa4bcd79b816064cf27499303ea42/triton-3.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:387dae4cb0089a7b6ba1a428ae0782b65c4c58f57d94617cb22ca8593d8ccbca", size = 247972313, upload-time = "2026-08-28T15:56:14.007Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/429c5592181cfb7361a0a8e0bff218e7224b726709d75da2472b3e819f70/triton-3.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b84e7d512490ba529111260fa6f7cad8b254a6bb5fbdf41d5ef9a5e57f52d0a", size = 226591133, upload-time = "2026-08-28T16:09:02.271Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/aa8a3e935c37efee7945984fdb64d7e0851bf6d920afd97b2d21f9d23360/triton-3.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74217bb56ed8692759227758e4c4b3bd2d608a209c1a7a081bf361fb4c2c1bf9", size = 248077577, upload-time = "2026-08-28T15:56:24.94Z" }, +] + [[package]] name = "typer" version = "0.27.2"