From 5e6a93a746f5d4b0309937afba070c6c4e37faf9 Mon Sep 17 00:00:00 2001 From: Ali Sayed Salehi Date: Mon, 10 Aug 2026 15:31:36 -0400 Subject: [PATCH 1/2] add performance regresssion predictor inference feature --- .gitignore | 1 + bugbug/models/__init__.py | 1 + .../performance_regression_predictor.py | 320 ++++++++++++++++++ bugbug/tools/core/platforms/phabricator.py | 29 +- docs/README.md | 1 + .../performance-regression-predictor.md | 118 +++++++ http_service/README.md | 208 +++++++++++- http_service/bugbug_http/app.py | 86 +++++ http_service/bugbug_http/download_models.py | 4 +- http_service/bugbug_http/models.py | 102 +++++- http_service/docker-compose.yml | 4 +- http_service/pyproject.toml | 2 +- .../test_performance_regression_predictor.py | 266 +++++++++++++++ pyproject.toml | 13 + scripts/performance_regression_predictor.py | 108 ++++++ scripts/trainer.py | 3 + .../test_performance_regression_predictor.py | 202 +++++++++++ tests/test_phabricator.py | 37 ++ uv.lock | 246 +++++++++++--- 19 files changed, 1688 insertions(+), 63 deletions(-) create mode 100644 bugbug/models/performance_regression_predictor.py create mode 100644 docs/models/performance-regression-predictor.md create mode 100644 http_service/tests/test_performance_regression_predictor.py create mode 100644 scripts/performance_regression_predictor.py create mode 100644 tests/test_performance_regression_predictor.py diff --git a/.gitignore b/.gitignore index f3e58ba936..630e9d96c7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ node_modules/ # Local env files .env +/http_service/docker-compose.override.yml diff --git a/bugbug/models/__init__.py b/bugbug/models/__init__.py index 5e08694d08..239fb4b2b5 100644 --- a/bugbug/models/__init__.py +++ b/bugbug/models/__init__.py @@ -23,6 +23,7 @@ "invalidcompatibilityreport": "bugbug.models.invalid_compatibility_report.InvalidCompatibilityReportModel", "needsdiagnosis": "bugbug.models.needsdiagnosis.NeedsDiagnosisModel", "performancebug": "bugbug.models.performancebug.PerformanceBugModel", + "performanceregressionpredictor": "bugbug.models.performance_regression_predictor.PerformanceRegressionPredictorModel", "qaneeded": "bugbug.models.qaneeded.QANeededModel", "rcatype": "bugbug.models.rcatype.RCATypeModel", "regression": "bugbug.models.regression.RegressionModel", diff --git a/bugbug/models/performance_regression_predictor.py b/bugbug/models/performance_regression_predictor.py new file mode 100644 index 0000000000..9d3283a385 --- /dev/null +++ b/bugbug/models/performance_regression_predictor.py @@ -0,0 +1,320 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +"""Inference-only performance regression predictor.""" + +from __future__ import annotations + +import json +import re +from collections.abc import Sequence +from pathlib import Path +from typing import Any + +import numpy as np + +from bugbug.model import Model + +MODEL_NAME = "Performance Regression Predictor" +MODEL_IDENTIFIER = "performanceregressionpredictor" +DEFAULT_MODEL_DIRECTORY = f"{MODEL_IDENTIFIER}model" +POSITIVE_CLASS_ID = 1 + + +def clean_commit_message( + commit_message: str | None, *, clean_subject_only: bool = True +) -> str: + """Remove common noisy prefixes from a commit message. + + This intentionally mirrors the preprocessing used to prepare the model's + training data. + """ + if commit_message is None: + return "" + + message = str(commit_message) + lines = message.splitlines() + if not lines: + return "" + + def _clean_subject(subject: str) -> str: + prefix = r"(?:\[[^\]]+\]|\([^)]+\)|bug\s*#?\s*\d+\b)" + return re.sub( + rf"^\s*(?:{prefix}\s*(?:[-–—:.,]\s*)?)+", + "", + subject, + count=1, + flags=re.IGNORECASE, + ).strip() + + if clean_subject_only: + for index, line in enumerate(lines): + if line.strip(): + lines[index] = _clean_subject(line) + break + return "\n".join(lines).strip("\n") + + cleaned_lines = [ + _clean_subject(line) if index == 0 else line for index, line in enumerate(lines) + ] + return "\n".join(cleaned_lines).strip("\n") + + +def combine_commit_messages(commit_messages: Sequence[str]) -> str: + """Clean and combine commit messages uploaded for one Phabricator diff. + + Phabricator exposes local commit metadata as a list. Most Mozilla diffs have + one entry, but cleaning each message separately also gives deterministic + preprocessing for the uncommon multi-commit case. + """ + return "\n\n".join( + cleaned_message + for commit_message in commit_messages + if (cleaned_message := clean_commit_message(commit_message).strip()) + ) + + +def diff_to_structured_text(diff_string: str) -> str: + """Convert a Git or Mercurial diff to the model's structured format.""" + lines = diff_string.strip().splitlines() + output: list[str] = [] + + current_file: str | None = None + current_block_type: str | None = None + current_block_lines: list[str] = [] + + pending_binary_status: str | None = None + rename_from: str | None = None + rename_to: str | None = None + pending_rename = False + + def flush_block() -> None: + nonlocal current_block_type, current_block_lines + if current_block_type and current_block_lines: + output.append(f" <{current_block_type.upper()}>") + output.extend(f" {line}" for line in current_block_lines) + output.append(f" ") + current_block_type = None + current_block_lines = [] + + def flush_file() -> None: + nonlocal current_file, pending_binary_status + nonlocal rename_from, rename_to, pending_rename + + if current_file: + flush_block() + if pending_rename and rename_from and rename_to: + output.append(f" File renamed from {rename_from}.") + elif pending_binary_status: + output.append(f" Binary file {pending_binary_status}.") + output.append("") + + current_file = None + pending_binary_status = None + rename_from = None + rename_to = None + pending_rename = False + + for line in lines: + if line.startswith("diff -r"): + flush_file() + parts = line.split() + if len(parts) >= 4: + current_file = parts[-1] + output.extend(("", f" {current_file}")) + continue + + if line.startswith("diff --git"): + flush_file() + match = re.match(r"diff --git a/(.+?) b/(.+)", line) + if match: + current_file = match.group(2) + output.extend(("", f" {current_file}")) + elif line.startswith("rename from "): + rename_from = line[len("rename from ") :].strip() + pending_rename = True + elif line.startswith("rename to "): + rename_to = line[len("rename to ") :].strip() + if not current_file: + current_file = rename_to + output.extend(("", f" {current_file}")) + elif line.startswith("--- "): + pass + elif line.startswith("+++ "): + pass + elif line.startswith("Binary files "): + flush_block() + pending_binary_status = "changed" + flush_file() + elif line.startswith("@@"): + flush_block() + elif line.startswith("-"): + if current_block_type != "REMOVED": + flush_block() + current_block_type = "REMOVED" + current_block_lines.append(line[1:].rstrip()) + elif line.startswith("+"): + if current_block_type != "ADDED": + flush_block() + current_block_type = "ADDED" + current_block_lines.append(line[1:].rstrip()) + else: + flush_block() + + flush_file() + return "\n".join(output) + + +def build_model_input(commit_message: str | None, raw_diff: str) -> str: + """Build the exact text representation consumed during training.""" + cleaned_message = clean_commit_message(commit_message) + structured_diff = diff_to_structured_text(raw_diff) + return "\n".join( + ( + "", + cleaned_message, + "", + structured_diff, + ) + ) + + +class PerformanceRegressionPredictorModel(Model): + """Hugging Face sequence classifier used only for inference.""" + + training_supported = False + + def __init__(self, tokenizer: Any = None, transformer_model: Any = None) -> None: + super().__init__() + self.tokenizer = tokenizer + self.transformer_model = transformer_model + self.calculate_importance = False + self.model_directory: str | None = None + self.model_metadata: dict[str, Any] = {} + + @classmethod + def load(cls, model_directory: str) -> "PerformanceRegressionPredictorModel": + """Load a local Hugging Face checkpoint directory.""" + from transformers import AutoModelForSequenceClassification, AutoTokenizer + + tokenizer = AutoTokenizer.from_pretrained( + model_directory, + local_files_only=True, + ) + transformer_model = AutoModelForSequenceClassification.from_pretrained( + model_directory, + local_files_only=True, + ) + # The service runs inference on CPU. Converting here also makes + # checkpoints saved in bfloat16 usable on CPUs without bfloat16 + # acceleration. + transformer_model.float().to("cpu") + transformer_model.eval() + + model = cls(tokenizer=tokenizer, transformer_model=transformer_model) + model.model_directory = model_directory + + metadata_path = Path(model_directory) / "bugbug_model.json" + if metadata_path.exists(): + with metadata_path.open(encoding="utf-8") as metadata_file: + model.model_metadata = json.load(metadata_file) + + model._validate_checkpoint() + return model + + def _validate_checkpoint(self) -> None: + if self.tokenizer is None or self.transformer_model is None: + raise ValueError("The tokenizer and transformer model must both be loaded") + + config = self.transformer_model.config + if int(config.num_labels) != 2: + raise ValueError( + "Performance Regression Predictor requires exactly two labels" + ) + + id2label = { + int(label_id): label + for label_id, label in getattr(config, "id2label", {}).items() + } + if id2label and id2label.get(POSITIVE_CLASS_ID) not in ( + "POSITIVE", + "1", + 1, + ): + raise ValueError( + "Checkpoint label 1 must be the positive performance-regression class" + ) + + required_tokens = { + "", + "", + "", + "", + "", + "", + "", + "", + } + tokenizer_tokens = set(self.tokenizer.get_added_vocab()) + missing_tokens = required_tokens - tokenizer_tokens + if missing_tokens: + raise ValueError( + "Checkpoint tokenizer is missing structural tokens: " + f"{sorted(missing_tokens)}" + ) + + @property + def max_length(self) -> int: + tokenizer_limit = int(self.tokenizer.model_max_length) + model_limit = int(self.transformer_model.config.max_position_embeddings) + return min(tokenizer_limit, model_limit) + + def classify( + self, + items, + probabilities=False, + importances=False, + importance_cutoff=0.15, + background_dataset=None, + ): + """Classify commit-message/diff dictionaries.""" + del importance_cutoff, background_dataset + if importances: + raise ValueError("Transformer feature importances are not supported") + + if not isinstance(items, list): + items = [items] + if not items: + return np.empty((0, 2)) if probabilities else np.empty((0,), dtype=int) + + prompts = [ + build_model_input(item.get("commit_message"), item["diff"]) + for item in items + ] + encoded = self.tokenizer( + prompts, + truncation=True, + max_length=self.max_length, + padding=True, + return_tensors="pt", + ) + + import torch + + with torch.inference_mode(): + logits = self.transformer_model(**encoded).logits.float() + class_probabilities = torch.softmax(logits, dim=-1).cpu().numpy() + + if probabilities: + return class_probabilities + return class_probabilities.argmax(axis=-1) + + def get_extra_data(self) -> dict[str, Any]: + return { + "model_name": MODEL_NAME, + "model_version": self.model_metadata.get("model_version"), + "max_length": self.max_length, + "calibrated": False, + } diff --git a/bugbug/tools/core/platforms/phabricator.py b/bugbug/tools/core/platforms/phabricator.py index 39baabd645..a29fecabd8 100644 --- a/bugbug/tools/core/platforms/phabricator.py +++ b/bugbug/tools/core/platforms/phabricator.py @@ -429,11 +429,38 @@ async def _commit_available(commit_hash: str) -> bool: def _diff_metadata(self) -> dict: phabricator = get_phabricator_client() diffs = phabricator.search_diffs(diff_id=self.diff_id) - assert len(diffs) == 1 + if len(diffs) != 1: + raise PhabricatorRevisionNotFoundException(f"Diff {self.diff_id} not found") diff = diffs[0] return diff + @cached_property + def diff_commits(self) -> list[dict]: + """Return local commit metadata uploaded with this immutable diff.""" + phabricator = get_phabricator_client() + diffs = phabricator.search_diffs( + diff_id=self.diff_id, + attachments={"commits": True}, + ) + if len(diffs) != 1: + raise PhabricatorRevisionNotFoundException(f"Diff {self.diff_id} not found") + return diffs[0].get("attachments", {}).get("commits", {}).get("commits", []) + + @property + def diff_revision_phid(self) -> str: + """Return the revision PHID associated with this diff.""" + return self._diff_metadata["revisionPHID"] + + @property + def commit_messages(self) -> list[str]: + """Return non-empty commit messages uploaded with this diff.""" + return [ + message + for commit in self.diff_commits + if isinstance((message := commit.get("message")), str) and message.strip() + ] + async def get_base_revision(self) -> Optional[str]: try: return await self.get_base_commit_hash() diff --git a/docs/README.md b/docs/README.md index c28dab0c05..ca060882ab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,4 @@ Detailed documentation per model - [Regressor model for predicting risky commits](models/regressor.md) +- [Performance Regression Predictor](models/performance-regression-predictor.md) diff --git a/docs/models/performance-regression-predictor.md b/docs/models/performance-regression-predictor.md new file mode 100644 index 0000000000..15c518f99b --- /dev/null +++ b/docs/models/performance-regression-predictor.md @@ -0,0 +1,118 @@ +# Performance Regression Predictor + +The Performance Regression Predictor is an inference-only binary transformer +model. It predicts whether a public Phabricator diff is likely to +introduce a performance regression. + +The input is the commit message from the diff's `commits` attachment plus the +raw diff. If a diff has multiple uploaded local commits, each message is cleaned +independently and the messages are separated by blank lines. If Phabricator did +not retain commit metadata, the revision title and summary are used as a +fallback. Before inference, leading bracketed tags, parenthesized tags, and +prefixes such as `Bug 123456` or `Bug #123456` are removed from the first +non-empty line of each commit message. +The diff is converted to the structured representation used to train the +checkpoint. The combined text is truncated to the checkpoint's context window +(512 tokens for the current CodeBERT checkpoint). + +The `risk_score` is the uncalibrated softmax probability for positive class +`1`. It must not be interpreted as a calibrated probability for operational +decision-making. + +## Local inference with the CLI + +The CLI runs preprocessing and model inference directly. It does not start the +HTTP service, Redis, an RQ worker, or fetch data from Phabricator. + +From the Bugbug repository root, run the included sample patch against a local +Hugging Face checkpoint: + +```sh +cd /path/to/bugbug + +uv run --extra performance-regression-predictor \ + bugbug-predict-performance-regression \ + --model-dir /absolute/path/to/predictor_model \ + --patch-file examples/performance_regression_predictor.patch +``` + +The sample is a Git `format-patch`, so the command extracts its commit message +automatically. For a raw diff, provide the message directly: + +```sh +uv run --extra performance-regression-predictor \ + bugbug-predict-performance-regression \ + --model-dir /absolute/path/to/predictor_model \ + --patch-file /absolute/path/to/change.patch \ + --commit-message "Bug 123456 - Improve rendering performance" +``` + +Alternatively, use `--commit-message-file /path/to/commit-message.txt`. If no +message argument is provided, the CLI tries Git `format-patch` and Mercurial +export formats. A raw diff without a detectable message is still accepted, +with a warning. + +The command prints the predicted binary `class`, both class probabilities in +`prob`, and the uncalibrated positive-class `risk_score`. + +## HTTP service + +The endpoint uses the service's existing Redis/RQ worker and API-key presence +check: + +```text +GET /performanceregressionpredictor/predict/phabricator/{diff_id} +X-Api-Key: ... +``` + +The first request normally returns `202 {"ready": false}`. Poll the same URL +until it returns `200`. The worker requires `PHABRICATOR_API_KEY`; a custom +Phabricator host can be set with `PHABRICATOR_URL`. + +See [HTTP service local development](../../http_service/README.md) for the +complete Docker Compose setup, including the local model mount and secret +file. + +Example result: + +```json +{ + "revision_id": 123456, + "diff_id": 789012, + "prob": [0.25, 0.75], + "class": 1, + "risk_score": 0.75, + "extra_data": { + "model_name": "Performance Regression Predictor", + "model_version": null, + "max_length": 512, + "calibrated": false, + "commit_message_source": "diff_metadata", + "commit_message_count": 1 + } +} +``` + +Only public revisions are processed, and the worker verifies that the diff +belongs to a public revision. The `revision_id` in the response is derived from +the diff metadata. + +## Model artifact + +Production follows the existing Bugbug model-artifact convention. The +checkpoint directory must be named `performanceregressionpredictormodel` and +published as: + +```text +public/performanceregressionpredictormodel.tar.zst +``` + +under the indexed Taskcluster namespace +`project.bugbug.train_performanceregressionpredictor.`. For this first +iteration, the archive can be created and published by a one-off Taskcluster +task; no `bugbug-train` workflow is registered for this model. The standard +background-worker image then downloads it alongside the other model artifacts. + +For local Docker development before the artifact is published, mount the local +checkpoint at `/code/performanceregressionpredictormodel` in the background +worker. This is the same fixed-directory convention used by the other models. diff --git a/http_service/README.md b/http_service/README.md index dae03bff7b..91ad378205 100644 --- a/http_service/README.md +++ b/http_service/README.md @@ -1,23 +1,207 @@ -### Local development +# HTTP Service Local Development -**For starting the service locally run the following commands.** +Run Docker Compose commands from this directory, not the repository root. The +root may have a different Compose application with unrelated credentials. -Start Redis: +```sh +cd /path/to/bugbug/http_service +``` - docker-compose up redis +## Services -Build the http service image: +The local Compose file defines: - docker build -t mozilla/bugbug-http-service -f Dockerfile . +- `redis`: local Redis used by the HTTP service and RQ worker. +- `bugbug-http-service`: Flask HTTP API on `http://localhost:8000`. +- `bugbug-http-service-bg-worker`: background worker that downloads/loads models + and processes queued prediction jobs. +- `bugbug-http-service-rq-dasboard`: optional local RQ dashboard on + `http://localhost:9181`. -Start the http service: +## Environment - docker-compose up bugbug-http-service +Most environment variables are optional for local startup, but individual +endpoints may need service-specific credentials: -Build the background worker image: +- `BUGBUG_BUGZILLA_TOKEN`: needed by Bugzilla bug classification endpoints. +- `BUGBUG_GITHUB_TOKEN`: needed by GitHub issue classification endpoints. +- `PHABRICATOR_API_KEY`: needed by Phabricator-backed endpoints. +- `PHABRICATOR_URL`: optional; defaults to Mozilla production Phabricator. +- `BUGBUG_ALLOW_MISSING_MODELS=1`: useful for local development when you only + need one model and do not have every model artifact locally. - docker build -t mozilla/bugbug-http-service-bg-worker --build-arg TAG=latest -f Dockerfile.bg_worker . +The API checks for the presence of an `X-Api-Key` header. For local testing, the +value can be any non-empty string unless you are testing deployment-specific +authentication behavior. -Run the background worker: +If you need local secrets, create `.env` in this directory +(`http_service/.env` from the repository root): - docker-compose up bugbug-http-service-bg-worker +```dotenv +BUGBUG_BUGZILLA_TOKEN= +BUGBUG_GITHUB_TOKEN= +PHABRICATOR_API_KEY= +PHABRICATOR_URL=https://phabricator.services.mozilla.com +BUGBUG_ALLOW_MISSING_MODELS=1 +``` + +Protect the file: + +```sh +chmod 600 .env +``` + +`.env` is ignored by Git. Do not put real tokens in tracked Compose files. + +## Start The Service + +Confirm that you are using the HTTP service Compose application: + +```sh +docker compose config --services +``` + +Start the core services: + +```sh +docker compose up --build \ + redis \ + bugbug-http-service \ + bugbug-http-service-bg-worker +``` + +The background worker downloads and validates model artifacts during startup +unless the image was built with `CHECK_MODELS=0`. + +In another terminal, follow the worker logs: + +```sh +docker compose logs -f bugbug-http-service-bg-worker +``` + +## Test A Generic Model Endpoint + +Use an endpoint for a model that the HTTP service exposes through the generic +Bugzilla classifier route: + +```sh +curl --compressed -sS \ + -w '\nHTTP status: %{http_code}\n' \ + -H "X-Api-Key: local-test" \ + http://localhost:8000/component/predict/123456 +``` + +The first request normally queues the job and returns: + +```text +{"ready":false} +HTTP status: 202 +``` + +Repeat the same request after the worker finishes. A completed prediction +returns HTTP 200. + +## Optional RQ Dashboard + +Start the dashboard when you want to inspect queued, running, or failed jobs: + +```sh +docker compose up bugbug-http-service-rq-dasboard +``` + +Open: + +```text +http://localhost:9181 +``` + +This is a local debugging interface and should not be exposed publicly without +authentication. + +## Stop The Service + +```sh +docker compose down +``` + +## Performance Regression Predictor + +The Performance Regression Predictor uses the same HTTP service and background +worker, but it needs two extra local-development pieces while the model artifact +is unpublished: + +- a local Hugging Face checkpoint mounted at the standard model directory; +- a Phabricator Conduit token so the worker can fetch diff metadata and raw + diffs. + +### Create The Secret File + +Create `.env` in this directory (`http_service/.env` from the repository root) +with your Conduit token: + +```dotenv +CONDUIT_API_TOKEN=api-replace-with-your-token +PHABRICATOR_URL=https://phabricator.services.mozilla.com +``` + +### Create The Compose Override + +Create `http_service/docker-compose.override.yml` and replace the source side +of the volume with the absolute path to your local Hugging Face checkpoint: + +```yaml +services: + bugbug-http-service-bg-worker: + build: + args: + CHECK_MODELS: "0" + environment: + BUGBUG_ALLOW_MISSING_MODELS: "1" + PHABRICATOR_API_KEY: ${CONDUIT_API_TOKEN} + PHABRICATOR_URL: "${PHABRICATOR_URL:-https://phabricator.services.mozilla.com}" + volumes: + - /absolute/path/to/predictor_model:/code/performanceregressionpredictormodel:ro +``` + +The host directory can be anywhere. Inside the container it must be mounted at: + +```text +/code/performanceregressionpredictormodel +``` + +That is the same fixed model-directory convention used by the other HTTP worker +models. `CHECK_MODELS=0` skips startup artifact downloads while this model is +unpublished, and `BUGBUG_ALLOW_MISSING_MODELS=1` lets the worker start without +unrelated model artifacts. + +Start the core services as usual: + +```sh +docker compose up --build \ + redis \ + bugbug-http-service \ + bugbug-http-service-bg-worker +``` + +Verify that the checkpoint is mounted: + +```sh +docker compose exec bugbug-http-service-bg-worker \ + test -f /code/performanceregressionpredictormodel/config.json \ + && echo "Model is mounted" +``` + +Request a prediction with an immutable Phabricator diff ID: + +```sh +curl --compressed -sS \ + -w '\nHTTP status: %{http_code}\n' \ + -H "X-Api-Key: local-test" \ + http://localhost:8000/performanceregressionpredictor/predict/phabricator/DIFF_ID +``` + +The first request normally returns HTTP 202. Repeat the same request until it +returns HTTP 200. + +For direct inference without Docker, Redis, Phabricator, or the HTTP API, see +the [Performance Regression Predictor CLI documentation](../docs/models/performance-regression-predictor.md#local-inference-with-the-cli). diff --git a/http_service/bugbug_http/app.py b/http_service/bugbug_http/app.py index 60c4800bee..d7a640ecc9 100644 --- a/http_service/bugbug_http/app.py +++ b/http_service/bugbug_http/app.py @@ -32,9 +32,11 @@ from bugbug import bugzilla, get_bugbug_version, utils from bugbug_http.models import ( MODELS_NAMES, + PERFORMANCE_REGRESSION_LOG_PREFIX, classify_broken_site_report, classify_bug, classify_issue, + classify_performance_regression, get_config_specific_groups, schedule_tests, schedule_tests_from_patch, @@ -108,6 +110,15 @@ class BugPrediction(Schema): extra_data = fields.Dict() +class PerformanceRegressionPrediction(Schema): + revision_id = fields.Integer() + diff_id = fields.Integer() + prob = fields.List(fields.Float()) + predicted_class = fields.Integer(data_key="class") + risk_score = fields.Float() + extra_data = fields.Dict() + + class NotAvailableYet(Schema): ready = fields.Boolean(metadata={"enum": [False]}) @@ -130,6 +141,10 @@ class Schedules(Schema): spec.components.schema(BugPrediction.__name__, schema=BugPrediction) +spec.components.schema( + PerformanceRegressionPrediction.__name__, + schema=PerformanceRegressionPrediction, +) spec.components.schema(NotAvailableYet.__name__, schema=NotAvailableYet) spec.components.schema(ModelName.__name__, schema=ModelName) spec.components.schema(UnauthorizedError.__name__, schema=UnauthorizedError) @@ -522,6 +537,77 @@ def model_prediction(model_name, bug_id): return compress_response(data, status_code) +@application.route("/performanceregressionpredictor/predict/phabricator/") +@cross_origin() +def performance_regression_prediction(diff_id: int): + """ + --- + get: + description: Predict performance-regression risk for a public Phabricator diff + summary: Predict performance-regression risk + parameters: + - name: diff_id + in: path + required: true + schema: + type: integer + example: 789012 + responses: + 200: + description: A performance-regression risk prediction + content: + application/json: + schema: PerformanceRegressionPrediction + 202: + description: The prediction is being processed + content: + application/json: + schema: NotAvailableYet + 401: + description: API key is missing + content: + application/json: + schema: UnauthorizedError + """ + if not request.headers.get(API_TOKEN): + return jsonify(UnauthorizedError().dump({})), 401 + + LOGGER.info( + "%s Received prediction request for diff_id=%d", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + ) + + job = JobInfo(classify_performance_regression, diff_id) + data = get_result(job) + status_code = 200 + + if not data: + if not is_pending(job): + LOGGER.info( + "%s Queueing prediction job for diff_id=%d", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + ) + schedule_job(job) + else: + LOGGER.info( + "%s Prediction job is pending for diff_id=%d", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + ) + status_code = 202 + data = {"ready": False} + else: + LOGGER.info( + "%s Returning cached prediction for diff_id=%d", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + ) + + return compress_response(data, status_code) + + @application.route( "//predict/github///" ) diff --git a/http_service/bugbug_http/download_models.py b/http_service/bugbug_http/download_models.py index 5e2d421807..b3a61a19a7 100644 --- a/http_service/bugbug_http/download_models.py +++ b/http_service/bugbug_http/download_models.py @@ -7,13 +7,13 @@ from bugbug import utils from bugbug_http import ALLOW_MISSING_MODELS -from bugbug_http.models import MODEL_CACHE, MODELS_NAMES +from bugbug_http.models import MODEL_CACHE, MODELS_TO_DOWNLOAD LOGGER = logging.getLogger() def download_models(): - for model_name in MODELS_NAMES: + for model_name in MODELS_TO_DOWNLOAD: utils.download_model(model_name) # Try loading the model try: diff --git a/http_service/bugbug_http/models.py b/http_service/bugbug_http/models.py index 5fd17b9c4f..2e73a2c6d5 100644 --- a/http_service/bugbug_http/models.py +++ b/http_service/bugbug_http/models.py @@ -18,7 +18,12 @@ from bugbug import bugzilla, repository, test_scheduling, utils from bugbug.github import Github from bugbug.model import Model -from bugbug.models import testselect +from bugbug.models import get_model_class, testselect +from bugbug.models.performance_regression_predictor import ( + MODEL_IDENTIFIER as PERFORMANCE_REGRESSION_PREDICTOR, +) +from bugbug.models.performance_regression_predictor import combine_commit_messages +from bugbug.tools.core.platforms.phabricator import PhabricatorPatch from bugbug.utils import get_hgmo_stack from bugbug_http.readthrough_cache import ReadthroughTTLCache @@ -41,6 +46,8 @@ "worksforme", "fenixcomponent", ] +MODELS_TO_DOWNLOAD = [*MODELS_NAMES, PERFORMANCE_REGRESSION_PREDICTOR] +PERFORMANCE_REGRESSION_LOG_PREFIX = "[performance-regression-predictor]" DEFAULT_EXPIRATION_TTL = 7 * 24 * 3600 # A week url = urlparse(os.environ.get("REDIS_URL", "redis://localhost/0")) @@ -53,8 +60,14 @@ ssl_cert_reqs=None, ) + +def load_model(model_name: str) -> Model: + """Load a model using the implementation registered for its name.""" + return get_model_class(model_name).load(f"{model_name}model") + + MODEL_CACHE: ReadthroughTTLCache[str, Model] = ReadthroughTTLCache( - timedelta(hours=1), lambda m: Model.load(f"{m}model") + timedelta(hours=1), load_model ) MODEL_CACHE.start_ttl_thread() @@ -225,6 +238,91 @@ def classify_broken_site_report(model_name: str, reports_data: list[dict]) -> st return "OK" +def classify_performance_regression(diff_id: int) -> str: + """Predict performance-regression risk for one immutable Phabricator diff.""" + from bugbug_http.app import JobInfo + + job = JobInfo(classify_performance_regression, diff_id) + LOGGER.info( + "%s Processing prediction for diff_id=%d", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + ) + patch = PhabricatorPatch(diff_id=diff_id) + + if not patch.is_accessible() or not patch.is_public(): + LOGGER.warning( + "%s Prediction unavailable for diff_id=%d", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + ) + setkey(job.result_key, orjson.dumps({"available": False})) + return "OK" + + commit_messages = patch.commit_messages + if commit_messages: + if len(commit_messages) > 1: + LOGGER.warning( + "%s Diff %d has %d uploaded commit messages; combining them " + "for inference", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + len(commit_messages), + ) + commit_message = combine_commit_messages(commit_messages) + commit_message_source = "diff_metadata" + else: + LOGGER.warning( + "%s Diff %d has no uploaded commit message metadata; using revision " + "title and summary fallback", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + ) + commit_message = "\n\n".join( + part for part in (patch.patch_title, patch.patch_description) if part + ) + commit_message_source = "revision_title_and_summary_fallback" + + LOGGER.info( + "%s Using commit message source %s for diff_id=%d with commit_message_count=%d", + PERFORMANCE_REGRESSION_LOG_PREFIX, + commit_message_source, + diff_id, + len(commit_messages), + ) + + model = MODEL_CACHE.get(PERFORMANCE_REGRESSION_PREDICTOR) + + probabilities = model.classify( + [{"commit_message": commit_message, "diff": patch.raw_diff}], + probabilities=True, + )[0] + predicted_class = int(probabilities.argmax()) + data = { + "revision_id": patch.revision_id, + "diff_id": diff_id, + "prob": probabilities.tolist(), + "class": predicted_class, + "risk_score": float(probabilities[1]), + "extra_data": { + **model.get_extra_data(), + "commit_message_source": commit_message_source, + "commit_message_count": len(commit_messages), + }, + } + setkey(job.result_key, orjson.dumps(data), compress=True) + LOGGER.info( + "%s Finished prediction for diff_id=%d, " + "revision_id=%d, class=%d, risk_score=%f", + PERFORMANCE_REGRESSION_LOG_PREFIX, + diff_id, + patch.revision_id, + predicted_class, + float(probabilities[1]), + ) + return "OK" + + @lru_cache(maxsize=None) def get_known_tasks() -> tuple[str, ...]: with open("known_tasks", "r") as f: diff --git a/http_service/docker-compose.yml b/http_service/docker-compose.yml index 31e0e1d671..90944a07e7 100644 --- a/http_service/docker-compose.yml +++ b/http_service/docker-compose.yml @@ -39,6 +39,8 @@ services: - REDIS_URL=redis://redis:6379/0 - BUGBUG_ALLOW_MISSING_MODELS - BUGBUG_REPO_DIR + - PHABRICATOR_API_KEY + - PHABRICATOR_URL - SENTRY_DSN depends_on: - redis @@ -61,7 +63,7 @@ services: - redis redis: - image: redis:4 + image: redis:7 ports: - target: 6379 published: 6379 diff --git a/http_service/pyproject.toml b/http_service/pyproject.toml index 4175a8d462..c70ffa0a8c 100644 --- a/http_service/pyproject.toml +++ b/http_service/pyproject.toml @@ -17,7 +17,7 @@ classifiers = [ dependencies = [ "apispec-webframeworks~=1.2.0", "apispec[yaml]~=6.10.0", - "bugbug", + "bugbug[performance-regression-predictor]", "cerberus~=1.3.8", "Flask~=3.1.3", "flask-apispec~=0.11.4", diff --git a/http_service/tests/test_performance_regression_predictor.py b/http_service/tests/test_performance_regression_predictor.py new file mode 100644 index 0000000000..358446c5d9 --- /dev/null +++ b/http_service/tests/test_performance_regression_predictor.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import gzip + +import numpy as np +import orjson +import pytest +import zstandard + +from bugbug_http import models +from bugbug_http.app import API_TOKEN, JobInfo + + +def _response_json(response): + if response.headers.get("Content-Encoding") == "gzip": + return orjson.loads(gzip.decompress(response.data)) + return response.json + + +def test_endpoint_queues_and_returns_prediction(client, jobs, add_result) -> None: + endpoint = "/performanceregressionpredictor/predict/phabricator/789012" + + unauthorized = client.get(endpoint) + assert unauthorized.status_code == 401 + + wrong_input_kind = client.get( + "/performanceregressionpredictor/predict/123456", + headers={API_TOKEN: "test"}, + ) + assert wrong_input_kind.status_code == 404 + + response = client.get(endpoint, headers={API_TOKEN: "test"}) + assert response.status_code == 202 + assert _response_json(response) == {"ready": False} + + prediction = { + "revision_id": 123456, + "diff_id": 789012, + "prob": [0.25, 0.75], + "class": 1, + "risk_score": 0.75, + "extra_data": {"calibrated": False}, + } + keys = next(iter(jobs.values())) + add_result(keys[0], prediction) + + response = client.get(endpoint, headers={API_TOKEN: "test"}) + assert response.status_code == 200 + assert _response_json(response) == prediction + + +def test_worker_uses_diff_commit_metadata(monkeypatch) -> None: + class FakePatch: + def __init__(self, diff_id): + assert diff_id == 789012 + self.revision_id = 123456 + self.commit_messages = ["[PATCH] - Make rendering faster"] + self.patch_title = "Unused title" + self.patch_description = "Unused summary" + self.raw_diff = "diff --git a/a b/a\n" + + def is_accessible(self): + return True + + def is_public(self): + return True + + class FakeModel: + def __init__(self): + self.items = None + + def classify(self, items, probabilities=False): + assert probabilities + self.items = items + return np.array([[0.2, 0.8]]) + + def get_extra_data(self): + return {"calibrated": False} + + fake_model = FakeModel() + monkeypatch.setattr(models, "PhabricatorPatch", FakePatch) + monkeypatch.setattr( + models.MODEL_CACHE, + "get", + lambda model_name: fake_model, + ) + + assert models.classify_performance_regression(789012) == "OK" + assert fake_model.items == [ + { + "commit_message": "Make rendering faster", + "diff": "diff --git a/a b/a\n", + } + ] + + job = JobInfo(models.classify_performance_regression, 789012) + stored = models.redis.get(job.result_key) + assert stored is not None + result = orjson.loads(zstandard.ZstdDecompressor().decompress(stored)) + assert result["revision_id"] == 123456 + assert result["diff_id"] == 789012 + assert result["risk_score"] == 0.8 + assert result["class"] == 1 + assert result["extra_data"]["commit_message_source"] == "diff_metadata" + assert result["extra_data"]["commit_message_count"] == 1 + + +def test_worker_marks_inaccessible_diff_unavailable(monkeypatch) -> None: + class FakePatch: + def __init__(self, diff_id): + assert diff_id == 789012 + + def is_accessible(self): + return False + + def is_public(self): + raise AssertionError("is_public should not be called") + + monkeypatch.setattr(models, "PhabricatorPatch", FakePatch) + + assert models.classify_performance_regression(789012) == "OK" + job = JobInfo(models.classify_performance_regression, 789012) + stored = models.redis.get(job.result_key) + assert stored is not None + result = orjson.loads(stored) + assert result == {"available": False} + + +def test_worker_cleans_and_combines_multiple_commit_messages(monkeypatch) -> None: + class FakePatch: + revision_id = 123456 + commit_messages = [ + "Bug 123456 - Improve rendering\n\nFirst body.", + "[PATCH] Bug 789012 - Avoid repeated work\n\nSecond body.", + ] + patch_title = "Unused title" + patch_description = "Unused summary" + raw_diff = "diff --git a/a b/a\n" + + def __init__(self, diff_id): + assert diff_id == 789012 + + def is_accessible(self): + return True + + def is_public(self): + return True + + class FakeModel: + def classify(self, items, probabilities=False): + assert items[0]["commit_message"] == ( + "Improve rendering\n\nFirst body.\n\n" + "Avoid repeated work\n\nSecond body." + ) + return np.array([[0.3, 0.7]]) + + def get_extra_data(self): + return {} + + monkeypatch.setattr(models, "PhabricatorPatch", FakePatch) + monkeypatch.setattr(models.MODEL_CACHE, "get", lambda model_name: FakeModel()) + + assert models.classify_performance_regression(789012) == "OK" + job = JobInfo(models.classify_performance_regression, 789012) + stored = models.redis.get(job.result_key) + assert stored is not None + result = orjson.loads(zstandard.ZstdDecompressor().decompress(stored)) + assert result["extra_data"]["commit_message_source"] == "diff_metadata" + assert result["extra_data"]["commit_message_count"] == 2 + + +def test_worker_falls_back_to_revision_message(monkeypatch) -> None: + class FakePatch: + revision_id = 123456 + commit_messages: list[str] = [] + patch_title = "Improve rendering" + patch_description = "Avoid repeated work." + raw_diff = "diff --git a/a b/a\n" + + def __init__(self, diff_id): + assert diff_id == 789012 + + def is_accessible(self): + return True + + def is_public(self): + return True + + class FakeModel: + def classify(self, items, probabilities=False): + assert items[0]["commit_message"] == ( + "Improve rendering\n\nAvoid repeated work." + ) + return np.array([[0.6, 0.4]]) + + def get_extra_data(self): + return {} + + monkeypatch.setattr(models, "PhabricatorPatch", FakePatch) + monkeypatch.setattr(models.MODEL_CACHE, "get", lambda model_name: FakeModel()) + + assert models.classify_performance_regression(789012) == "OK" + job = JobInfo(models.classify_performance_regression, 789012) + stored = models.redis.get(job.result_key) + assert stored is not None + result = orjson.loads(zstandard.ZstdDecompressor().decompress(stored)) + assert result["extra_data"]["commit_message_source"] == ( + "revision_title_and_summary_fallback" + ) + assert result["extra_data"]["commit_message_count"] == 0 + + +def test_worker_propagates_model_loading_failure(monkeypatch) -> None: + class FakePatch: + revision_id = 123456 + commit_messages = ["Improve rendering"] + patch_title = "Unused title" + patch_description = "Unused summary" + + def __init__(self, diff_id): + assert diff_id == 789012 + + def is_accessible(self): + return True + + def is_public(self): + return True + + monkeypatch.setattr(models, "PhabricatorPatch", FakePatch) + + def raise_missing_model(model_name): + raise FileNotFoundError("missing checkpoint") + + monkeypatch.setattr(models.MODEL_CACHE, "get", raise_missing_model) + + with pytest.raises(FileNotFoundError, match="missing checkpoint"): + models.classify_performance_regression(789012) + + job = JobInfo(models.classify_performance_regression, 789012) + assert models.redis.get(job.result_key) is None + + +def test_load_model_uses_registered_model_class_and_standard_directory( + monkeypatch, +) -> None: + loaded_directories: list[str] = [] + sentinel = object() + + class FakeModel: + @staticmethod + def load(model_directory): + loaded_directories.append(model_directory) + return sentinel + + monkeypatch.setattr( + models, + "get_model_class", + lambda model_name: FakeModel, + ) + + loaded_model = models.load_model("somecustommodel") + assert loaded_model is sentinel + assert loaded_directories == ["somecustommodelmodel"] diff --git a/pyproject.toml b/pyproject.toml index f132683dcf..77e7f110f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,10 @@ nlp = [ "spacy==3.8.14", ] nn = [] +performance-regression-predictor = [ + "torch>=2.6,<3", + "transformers==4.56.2", +] [dependency-groups] test = [ @@ -116,6 +120,7 @@ bugbug-fixed-comments = "scripts.inline_comments_data_collection:main" bugbug-ci-failures-retriever = "scripts.retrieve_ci_failures:main" bugbug-try-pushes-retriever = "scripts.retrieve_try_pushes:main" bugbug-validate-review-context = "bugbug.tools.code_review.review_context_schema:main" +bugbug-predict-performance-regression = "scripts.performance_regression_predictor:main" [tool.hatch.version] path = "VERSION" @@ -139,6 +144,14 @@ exclude = [ [tool.uv.sources] hackbot-runtime = { workspace = true } agent-tools = { workspace = true } +torch = [ + { index = "pytorch-cpu", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, +] + +[[tool.uv.index]] +name = "pytorch-cpu" +url = "https://download.pytorch.org/whl/cpu" +explicit = true [tool.ruff] extend-exclude = ["data"] diff --git a/scripts/performance_regression_predictor.py b/scripts/performance_regression_predictor.py new file mode 100644 index 0000000000..7841465572 --- /dev/null +++ b/scripts/performance_regression_predictor.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +"""Run the Performance Regression Predictor against a local patch.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from email import policy +from email.parser import Parser +from pathlib import Path + +from bugbug.models.performance_regression_predictor import ( + PerformanceRegressionPredictorModel, +) + + +def extract_commit_message_from_patch(patch: str) -> str | None: + """Extract a message from Git format-patch or Mercurial export content.""" + if patch.startswith("# HG changeset patch"): + message_lines: list[str] = [] + metadata_finished = False + for line in patch.splitlines()[1:]: + if not metadata_finished and (line.startswith("#") or not line.strip()): + continue + metadata_finished = True + if line.startswith(("diff -r ", "diff --git ")): + break + message_lines.append(line) + message = "\n".join(message_lines).strip() + return message or None + + if re.search(r"^Subject:", patch, flags=re.MULTILINE): + email_message = Parser(policy=policy.default).parsestr(patch) + subject = str(email_message.get("Subject", "")).strip() + body = email_message.get_payload() + if not isinstance(body, str): + body = "" + body = re.split(r"^---\s*$|^diff --git ", body, maxsplit=1, flags=re.MULTILINE)[ + 0 + ].strip() + message = "\n\n".join(part for part in (subject, body) if part) + return message or None + + return None + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Predict performance-regression risk from a local patch", + ) + parser.add_argument( + "--model-dir", + required=True, + help="Hugging Face checkpoint directory", + ) + parser.add_argument("--patch-file", required=True, type=Path) + commit_message = parser.add_mutually_exclusive_group() + commit_message.add_argument("--commit-message") + commit_message.add_argument("--commit-message-file", type=Path) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(argv) + raw_diff = args.patch_file.read_text(encoding="utf-8") + + if args.commit_message is not None: + commit_message = args.commit_message + commit_message_source = "argument" + elif args.commit_message_file is not None: + commit_message = args.commit_message_file.read_text(encoding="utf-8") + commit_message_source = "file" + else: + commit_message = extract_commit_message_from_patch(raw_diff) or "" + commit_message_source = "patch" if commit_message else "none" + if not commit_message: + print( + "Warning: no commit message was found; predicting from the diff only", + file=sys.stderr, + ) + + model = PerformanceRegressionPredictorModel.load(args.model_dir) + probabilities = model.classify( + [{"commit_message": commit_message, "diff": raw_diff}], + probabilities=True, + )[0] + predicted_class = int(probabilities.argmax()) + result = { + "prob": probabilities.tolist(), + "class": predicted_class, + "risk_score": float(probabilities[1]), + "extra_data": { + **model.get_extra_data(), + "commit_message_source": commit_message_source, + }, + } + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/trainer.py b/scripts/trainer.py index 29df276581..da0828a5a5 100644 --- a/scripts/trainer.py +++ b/scripts/trainer.py @@ -91,6 +91,9 @@ def parse_args(args): subparsers = main_parser.add_subparsers(title="model", dest="model", required=True) for model_name in MODELS: + if not getattr(get_model_class(model_name), "training_supported", True): + continue + subparser = subparsers.add_parser( model_name, parents=[parser], help=f"Train {model_name} model" ) diff --git a/tests/test_performance_regression_predictor.py b/tests/test_performance_regression_predictor.py new file mode 100644 index 0000000000..26e783533e --- /dev/null +++ b/tests/test_performance_regression_predictor.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from bugbug.models.performance_regression_predictor import ( + build_model_input, + clean_commit_message, + combine_commit_messages, + diff_to_structured_text, +) +from scripts.performance_regression_predictor import ( + extract_commit_message_from_patch, +) + +RAW_DIFF = """\ +diff --git a/widget.py b/widget.py +index 1111111..2222222 100644 +--- a/widget.py ++++ b/widget.py +@@ -1 +1 @@ +-old_value = 1 ++new_value = 2 + context +""" + +HG_DIFF = """\ +diff -r abcdef123456 widget.py +--- a/widget.py ++++ b/widget.py +@@ -1 +1 @@ +-old_value = 1 ++new_value = 2 + context +""" + +BINARY_DIFF = """\ +diff --git a/image.png b/image.png +index 1111111..2222222 100644 +Binary files a/image.png and b/image.png differ +""" + + +def test_clean_commit_message_preserves_body() -> None: + message = "[wpt PR 55677] - Rename tests\n\nKeep this body [verbatim]." + assert clean_commit_message(message) == ( + "Rename tests\n\nKeep this body [verbatim]." + ) + + +def test_clean_commit_message_removes_bug_number_prefixes() -> None: + messages = { + "Bug 123456 - Improve rendering": "Improve rendering", + "bug #123456: Improve rendering": "Improve rendering", + "[PATCH] Bug 123456. Improve rendering": "Improve rendering", + "Bug 123456 - [performance] Improve rendering": "Improve rendering", + } + for message, expected in messages.items(): + assert clean_commit_message(message) == expected + + +def test_combine_commit_messages_cleans_each_subject() -> None: + assert combine_commit_messages( + [ + "Bug 123456 - Improve rendering\n\nFirst body.", + "[PATCH] Bug 789012 - Avoid repeated work\n\nSecond body.", + ] + ) == ("Improve rendering\n\nFirst body.\n\nAvoid repeated work\n\nSecond body.") + + +def test_combine_commit_messages_drops_empty_messages() -> None: + assert ( + combine_commit_messages( + [ + "", + " ", + "Bug 123456 - Improve rendering", + ] + ) + == "Improve rendering" + ) + + +def test_diff_to_structured_text() -> None: + assert ( + diff_to_structured_text(RAW_DIFF) + == """\ + + widget.py + + old_value = 1 + + + new_value = 2 + +""" + ) + + +def test_diff_to_structured_text_mercurial_diff() -> None: + assert ( + diff_to_structured_text(HG_DIFF) + == """\ + + widget.py + + old_value = 1 + + + new_value = 2 + +""" + ) + + +def test_diff_to_structured_text_renamed_file() -> None: + diff = """\ +diff --git a/old_widget.py b/new_widget.py +similarity index 91% +rename from old_widget.py +rename to new_widget.py +--- a/old_widget.py ++++ b/new_widget.py +@@ -1 +1 @@ +-old_value = 1 ++new_value = 2 +""" + assert ( + diff_to_structured_text(diff) + == """\ + + new_widget.py + + old_value = 1 + + + new_value = 2 + + File renamed from old_widget.py. +""" + ) + + +def test_diff_to_structured_text_binary_file() -> None: + assert ( + diff_to_structured_text(BINARY_DIFF) + == """\ + + image.png + Binary file changed. +""" + ) + + +def test_build_model_input_cleans_commit_message() -> None: + prompt = build_model_input("[PATCH] Bug 123456 - Make it faster", RAW_DIFF) + assert prompt.startswith( + "\nMake it faster\n\n" + ) + assert "[PATCH]" not in prompt + assert "Bug 123456" not in prompt + + +def test_build_model_input_allows_missing_commit_message() -> None: + prompt = build_model_input(None, RAW_DIFF) + assert prompt.startswith("\n\n\n") + assert "widget.py" in prompt + + +def test_extract_commit_message_from_git_format_patch() -> None: + patch = """\ +From abcdef Mon Sep 17 00:00:00 2001 +From: Developer +Subject: [PATCH] Speed up rendering + +Avoid unnecessary work in the hot path. + +--- + widget.py | 2 +- +diff --git a/widget.py b/widget.py +""" + assert extract_commit_message_from_patch(patch) == ( + "[PATCH] Speed up rendering\n\nAvoid unnecessary work in the hot path." + ) + + +def test_extract_commit_message_from_mercurial_export() -> None: + patch = """\ +# HG changeset patch +# User Developer +# Date 123456 0 +# Node ID abc +# Parent def +Speed up rendering + +Avoid unnecessary work in the hot path. + +diff -r def -r abc widget.py +""" + assert extract_commit_message_from_patch(patch) == ( + "Speed up rendering\n\nAvoid unnecessary work in the hot path." + ) diff --git a/tests/test_phabricator.py b/tests/test_phabricator.py index 5fe1e72a62..7bbf398e68 100644 --- a/tests/test_phabricator.py +++ b/tests/test_phabricator.py @@ -324,6 +324,43 @@ def test_get_project_members_empty(monkeypatch) -> None: phab_platform.get_project_members.cache_clear() +def test_diff_commit_messages(monkeypatch) -> None: + client = MagicMock() + client.search_diffs.return_value = [ + { + "attachments": { + "commits": { + "commits": [ + {"identifier": "abc", "message": "First message"}, + {"identifier": "def", "message": ""}, + {"identifier": "ghi", "message": "Second message\n\nBody"}, + ] + } + } + } + ] + monkeypatch.setattr(phab_platform, "get_phabricator_client", lambda: client) + + patch = phab_platform.PhabricatorPatch(diff_id=123) + + assert patch.commit_messages == ["First message", "Second message\n\nBody"] + client.search_diffs.assert_called_once_with( + diff_id=123, + attachments={"commits": True}, + ) + + +def test_missing_diff_is_not_accessible(monkeypatch) -> None: + client = MagicMock() + client.search_diffs.return_value = [] + monkeypatch.setattr(phab_platform, "get_phabricator_client", lambda: client) + + patch = phab_platform.PhabricatorPatch(diff_id=123) + + assert not patch.is_accessible() + client.search_diffs.assert_called_once_with(diff_id=123) + + # --------------------------------------------------------------------------- # Rotation recovery: historical_reviewer_project_phids # --------------------------------------------------------------------------- diff --git a/uv.lock b/uv.lock index 2024e35395..56bb79a261 100644 --- a/uv.lock +++ b/uv.lock @@ -5,15 +5,18 @@ resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version >= '3.14' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", - "(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version < '3.13' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] [manifest] @@ -659,6 +662,11 @@ dependencies = [ nlp = [ { name = "spacy" }, ] +performance-regression-predictor = [ + { name = "torch", version = "2.13.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, + { name = "torch", version = "2.13.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "transformers" }, +] [package.dev-dependencies] spawn-pipeline = [ @@ -727,13 +735,16 @@ requires-dist = [ { name = "tabulate", specifier = "~=0.10.0" }, { name = "taskcluster", specifier = ">=97.1,<100.6" }, { name = "tenacity", specifier = "~=9.1.4" }, + { name = "torch", marker = "(sys_platform == 'linux' and extra == 'performance-regression-predictor') or (sys_platform == 'win32' and extra == 'performance-regression-predictor')", specifier = ">=2.6,<3", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32' and extra == 'performance-regression-predictor'", specifier = ">=2.6,<3" }, { name = "tqdm", specifier = ">=4.67.3,<4.69.0" }, + { name = "transformers", marker = "extra == 'performance-regression-predictor'", specifier = "==4.56.2" }, { name = "unidiff", specifier = "~=0.7.5" }, { name = "weave", specifier = ">=0.50.0" }, { name = "xgboost", specifier = ">=3.2,<3.4" }, { name = "zstandard", specifier = "~=0.25.0" }, ] -provides-extras = ["nlp", "nn"] +provides-extras = ["nlp", "nn", "performance-regression-predictor"] [package.metadata.requires-dev] spawn-pipeline = [ @@ -761,7 +772,7 @@ source = { editable = "http_service" } dependencies = [ { name = "apispec", extra = ["yaml"] }, { name = "apispec-webframeworks" }, - { name = "bugbug" }, + { name = "bugbug", extra = ["performance-regression-predictor"] }, { name = "cerberus" }, { name = "flask" }, { name = "flask-apispec" }, @@ -778,7 +789,7 @@ dependencies = [ requires-dist = [ { name = "apispec", extras = ["yaml"], specifier = "~=6.10.0" }, { name = "apispec-webframeworks", specifier = "~=1.2.0" }, - { name = "bugbug", editable = "." }, + { name = "bugbug", extras = ["performance-regression-predictor"], editable = "." }, { name = "cerberus", specifier = "~=1.3.8" }, { name = "flask", specifier = "~=3.1.3" }, { name = "flask-apispec", specifier = "~=0.11.4" }, @@ -2850,22 +2861,21 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.16.1" +version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, { name = "packaging" }, { name = "pyyaml" }, + { name = "requests" }, { name = "tqdm" }, - { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/0f/ed994dbade67a54407c28cab96ef845e0e6d25500be56aca6394f8bfc9dd/huggingface_hub-1.16.1.tar.gz", hash = "sha256:7f1dc4c5ec21aed69be630ad0c3378616be16f3de1a47b141c0e812965d9c832", size = 792534, upload-time = "2026-05-21T18:40:00.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl", hash = "sha256:64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", size = 668176, upload-time = "2026-05-21T18:39:58.596Z" }, + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, ] [[package]] @@ -3708,13 +3718,16 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version >= '3.14' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version < '3.13' and sys_platform == 'emscripten'", - "(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version < '3.13' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } wheels = [ @@ -4315,6 +4328,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/32/512ae30c4ef7fa2c606a24fc1f9a331a215988f4819328233d427f3269de/mozphab-2.15.3-py3-none-any.whl", hash = "sha256:3265eb9ddd03e08c44c8ea784912790f60a8ec0a1f104fdc64eea410f4dff23e", size = 124401, upload-time = "2026-06-25T16:31:55.998Z" }, ] +[[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 = "multidict" version = "6.7.1" @@ -4490,9 +4512,9 @@ resolution-markers = [ "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", ] dependencies = [ - { name = "llvmlite", version = "0.45.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "numpy", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, - { name = "setuptools", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "llvmlite", version = "0.45.1", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, + { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b0/2a/975f49e156dae4edd3ab5afc60e2b3d65add014db2ddbbc23b9bb89882a4/numba-0.47.0.tar.gz", hash = "sha256:c0703df0a0ea2e29fbef7937d9849cc4734253066cb5820c5d6e0851876e3b0a", size = 1935290, upload-time = "2020-01-03T17:03:47.391Z" } @@ -4503,17 +4525,20 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version >= '3.14' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version < '3.13' and sys_platform == 'emscripten'", - "(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')", + "python_full_version < '3.13' and sys_platform == 'linux'", + "(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] dependencies = [ - { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, - { name = "numpy", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" } }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } wheels = [ @@ -4949,7 +4974,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -6421,6 +6446,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/5f/4c174edad94f82de888ac00a5ddd8d07b35609b6c94f0bdf4d74af57703e/s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262", size = 90101, upload-time = "2026-06-16T19:44:50.439Z" }, ] +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + [[package]] name = "scikit-learn" version = "1.7.2" @@ -6533,8 +6582,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, - { name = "jeepney", marker = "(platform_machine != 'x86_64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32')" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -6931,6 +6980,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[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 = "tabulate" version = "0.10.0" @@ -7089,29 +7150,28 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.23.1" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, - { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, - { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, - { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, - { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, - { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] [[package]] @@ -7123,6 +7183,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" }, ] +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version >= '3.14' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.13.*' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.13' and platform_machine != 'x86_64' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0+cpu" +source = { registry = "https://download.pytorch.org/whl/cpu" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'linux'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'linux'", +] +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:ffadde149901c8afa138daa38d898264003cfcf1a3336ca5cd964b5af227d867", upload-time = "2026-07-08T19:28:41Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f307c2c32d764ffc6ff6893b801fad6d4752f3e67966cb8abf1843427c02604", upload-time = "2026-07-08T19:28:51Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ca4a9394b0c771238a4f73590fdbbc4debad85ed0fa63d026ae1b085da7d6e2", upload-time = "2026-07-08T19:29:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:a8b450c1e58e5800e5b4691dac412f8d2d65a1dc3298166f91596603a3531e6f", upload-time = "2026-07-08T19:29:15Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:fa0762705b933624d59f6823db9ce7ec2e35b3e1e9c319c9db51fbeecfc3e319", upload-time = "2026-07-08T19:29:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:966d020354f465672dc7dd10d3a5c6cd17d7eb48620aa1d265b48a1f78f06898", upload-time = "2026-07-08T19:29:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0b8f7d0423027ae8b90c7977c627f3379f325363a08224dffad9b4b2d684a83d", upload-time = "2026-07-08T19:29:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3fbf9c9d1f3c10c2d59d04aca426dee9ccc6ceb32d255c61e93acc3b4f75fae6", upload-time = "2026-07-08T19:29:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:a17ff48608634db245e17e8bb00a9558554a49aeb1e4f5fe6cd039af2a10515b", upload-time = "2026-07-08T19:30:05Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:ac7aaf322be4777765a53bed7264a214dd81b3a1d276b93150515a3c5f75e4b0", upload-time = "2026-07-08T19:30:12Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:dec241fef3984c0d1edadd1f58708e218d4eae881ceef7bc10cf9964d41b68b9", upload-time = "2026-07-08T19:30:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ca021f9eb2f8345c83fa03e3a04587308afb8df71bd472670b3ece00df58621c", upload-time = "2026-07-08T19:30:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d20fa53ee744502fa4c69818a720b05ca0d37abd055d4f6e66cae155114bc691", upload-time = "2026-07-08T19:30:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:e2e5134decf00e218da62318f3dc5df156231d367871918e91eba95ab0ad43ab", upload-time = "2026-07-08T19:30:58Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:991cc14b39e751122c01f017be6448533989868731cb5eecd1006893d26787c2", upload-time = "2026-07-08T19:31:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7b8d26e29bceafbdaa8d63bfe7612f23875b5af2cc07e13f809c3ed890bbe1d8", upload-time = "2026-07-08T19:31:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b222c15a0fc2ce207d1c1a59700b46c8fa6748df1f447ad11e5c870dde0933d9", upload-time = "2026-07-08T19:31:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:a43376bd094124ef626bfdd3d4c2c62eacb0b5ddc99776f4a32d4fd16f1f3420", upload-time = "2026-07-08T19:31:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5002ca81af00ae69b57540f615b58b8ae922b6d4848176b366a52bd2196e6", upload-time = "2026-07-08T19:32:00Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:1a3a35229fdc13446b4eab50e7fcf9399ff941e89a3b761497786297a5d8dde5", upload-time = "2026-07-08T19:32:16Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:8e109528e6bab044815daebaf71770fbaace3a66ef1c816cb55c875350f78a60", upload-time = "2026-07-08T19:32:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.13.0%2Bcpu-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:222a6681467cc7f6f05cd3068dfbc603def3a1e46d1d4620c1c8cdf6178bd563", upload-time = "2026-07-08T19:32:44Z" }, +] + [[package]] name = "tqdm" version = "4.68.4" @@ -7144,6 +7281,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, ] +[[package]] +name = "transformers" +version = "4.56.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/82/0bcfddd134cdf53440becb5e738257cc3cf34cf229d63b57bfd288e6579f/transformers-4.56.2.tar.gz", hash = "sha256:5e7c623e2d7494105c726dd10f6f90c2c99a55ebe86eef7233765abd0cb1c529", size = 9844296, upload-time = "2025-09-19T15:16:26.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/26/2591b48412bde75e33bfd292034103ffe41743cacd03120e3242516cd143/transformers-4.56.2-py3-none-any.whl", hash = "sha256:79c03d0e85b26cb573c109ff9eafa96f3c8d4febfd8a0774e8bba32702dd6dde", size = 11608055, upload-time = "2025-09-19T15:16:23.736Z" }, +] + [[package]] name = "treeherder-client" version = "5.0.0" From 2038a539af8630aae1f05441876aa39e15e1ee7e Mon Sep 17 00:00:00 2001 From: Ali Sayed Salehi Date: Mon, 10 Aug 2026 16:07:01 -0400 Subject: [PATCH 2/2] upgrade transformers and torch dependencies to the latest stable version --- pyproject.toml | 4 ++-- uv.lock | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b027ed2dda..6279e756a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,8 +72,8 @@ nlp = [ ] nn = [] performance-regression-predictor = [ - "torch>=2.6,<3", - "transformers==4.56.2", + "torch==2.13.0", + "transformers==5.15.0", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 9dc274940b..cb02844cf1 100644 --- a/uv.lock +++ b/uv.lock @@ -738,10 +738,10 @@ requires-dist = [ { name = "tabulate", specifier = "~=0.10.0" }, { name = "taskcluster", specifier = ">=97.1,<102.1" }, { name = "tenacity", specifier = "~=9.1.4" }, - { name = "torch", marker = "(sys_platform == 'linux' and extra == 'performance-regression-predictor') or (sys_platform == 'win32' and extra == 'performance-regression-predictor')", specifier = ">=2.6,<3", index = "https://download.pytorch.org/whl/cpu" }, - { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32' and extra == 'performance-regression-predictor'", specifier = ">=2.6,<3" }, + { name = "torch", marker = "(sys_platform == 'linux' and extra == 'performance-regression-predictor') or (sys_platform == 'win32' and extra == 'performance-regression-predictor')", specifier = "==2.13.0", index = "https://download.pytorch.org/whl/cpu" }, + { name = "torch", marker = "sys_platform != 'linux' and sys_platform != 'win32' and extra == 'performance-regression-predictor'", specifier = "==2.13.0" }, { name = "tqdm", specifier = ">=4.67.3,<4.71.0" }, - { name = "transformers", marker = "extra == 'performance-regression-predictor'", specifier = "==4.56.2" }, + { name = "transformers", marker = "extra == 'performance-regression-predictor'", specifier = "==5.15.0" }, { name = "unidiff", specifier = "~=0.7.5" }, { name = "weave", specifier = ">=0.53.4" }, { name = "xgboost", specifier = ">=3.2,<3.4" }, @@ -2887,21 +2887,22 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.36.2" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click" }, { name = "filelock" }, { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "requests" }, { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" }, ] [[package]] @@ -7365,23 +7366,22 @@ wheels = [ [[package]] name = "transformers" -version = "4.56.2" +version = "5.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, { name = "huggingface-hub" }, { name = "numpy" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, - { name = "requests" }, { name = "safetensors" }, { name = "tokenizers" }, { name = "tqdm" }, + { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/82/0bcfddd134cdf53440becb5e738257cc3cf34cf229d63b57bfd288e6579f/transformers-4.56.2.tar.gz", hash = "sha256:5e7c623e2d7494105c726dd10f6f90c2c99a55ebe86eef7233765abd0cb1c529", size = 9844296, upload-time = "2025-09-19T15:16:26.778Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/3f/d89353267d511e18f137dfd7769d07837350c11b88408ce1dfe2e93e56c7/transformers-5.15.0.tar.gz", hash = "sha256:bbf98f57b2ddd7c4ecbccfa2c0069017aa6fd01cc204bd50cbc0eeadcf2a13b8", size = 9377983, upload-time = "2026-08-10T10:27:23.261Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/26/2591b48412bde75e33bfd292034103ffe41743cacd03120e3242516cd143/transformers-4.56.2-py3-none-any.whl", hash = "sha256:79c03d0e85b26cb573c109ff9eafa96f3c8d4febfd8a0774e8bba32702dd6dde", size = 11608055, upload-time = "2025-09-19T15:16:23.736Z" }, + { url = "https://files.pythonhosted.org/packages/d8/43/81355710a4c84e9420e11a86d41a5364deb561f2ef36dfdf254a07371bbb/transformers-5.15.0-py3-none-any.whl", hash = "sha256:d7f007736f67749ae9490c4f8cb5d30b452ae2d68c8675e50ba8d63ea7feb107", size = 11749280, upload-time = "2026-08-10T10:27:20.416Z" }, ] [[package]]