diff --git a/README.md b/README.md index 4052c76c..ba4b157a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The reference documentation is available at ```python import torch from krum.primitives.aggregators import Krum, Average -from krum.primitives.attacks import Gaussian +from krum.primitives.attacks.gaussian import Gaussian # Simulate gradients from 10 workers (8 honest, 2 Byzantine) honest = torch.randn(8, 100) @@ -44,12 +44,21 @@ This project supports Python **3.10 through 3.14**. pip install krum ``` +This installs **PyTorch**, **torchvision**, and **pandas**. Additional +dependencies (``matplotlib``, ``numpy``, ``seaborn``) are required for running experiments +and visualisations: + +```bash +pip install "krum[experiments]" +``` + With `uv` (Recommended): ```bash uv pip install krum # or directly in a uv project uv add krum +uv add "krum[experiments]" # with optional experiment deps ``` ### From source @@ -60,7 +69,7 @@ install in editable mode with the development dependencies: ```bash git clone https://github.com/calicarpa/krum.git cd krum -pip install -e ".[dev]" +pip install -e ".[dev,experiments]" ``` With `uv` (Recommended): @@ -68,7 +77,7 @@ With `uv` (Recommended): ```bash git clone https://github.com/calicarpa/krum.git cd krum -uv sync --extra dev +uv sync --all-extras --all-groups ``` This installs all linting, type-checking, and documentation tools. diff --git a/docs/conf.py b/docs/conf.py index 81470ea1..85a60374 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -158,6 +158,10 @@ def linkcode_resolve(domain, info): "title": "Quickstart", "url": "quickstart", }, + { + "title": "Tutorials", + "url": "tutorials/index", + }, { "title": "Reference", "children": [ diff --git a/docs/index.rst b/docs/index.rst index e9b5c99f..5769372f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,6 +26,15 @@ Quickstart quickstart +Tutorials +--------- + +.. toctree:: + :maxdepth: 1 + :caption: Tutorials + + tutorials/index + Reference --------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e8d771f5..74b1e18a 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,8 +1,7 @@ Quickstart ========== -This guide walks you through installation, basic usage, and key concepts -of Krum. +This guide walks you through installing Krum and running your first command. Installation ------------ @@ -37,7 +36,7 @@ install in editable mode with the development dependencies: git clone https://github.com/calicarpa/krum.git cd krum - pip install -e ".[dev]" + pip install -e ".[dev,experiments]" With ``uv`` (recommended): @@ -45,182 +44,37 @@ With ``uv`` (recommended): git clone https://github.com/calicarpa/krum.git cd krum - uv sync --extra dev + uv sync --all-extras --all-groups Dependencies ~~~~~~~~~~~~ -Krum's only runtime dependencies are **PyTorch** and **torchvision**. If you plan -to use CUDA, ensure your PyTorch build matches your CUDA version. All other -requirements are pulled in automatically when you install Krum. +Krum's runtime dependencies are **PyTorch**, **torchvision**, and **pandas**. +If you plan to use CUDA, ensure your PyTorch build matches your CUDA version. +For experiments and visualisations, install the optional extras: -Basic Usage ------------ - -Here's a minimal example showing how to use Krum's aggregators and attacks: - -.. code-block:: python - - import torch - from krum.primitives.aggregators import Krum, Average - from krum.primitives.attacks import Gaussian, SignFlip - - # Simulate gradients from 10 workers (each gradient has 100 parameters) - n_workers = 10 - grad_dim = 100 - n_byzantine = 2 - - # Honest worker gradients (normally distributed) - honest_gradients = torch.randn(n_workers - n_byzantine, grad_dim) - - # Byzantine attack: generate malicious gradients - attack = Gaussian(std=10.0) - byzantine_gradients = attack.generate(honest_gradients, f=n_byzantine) - - # Combine all gradients - all_gradients = torch.cat([honest_gradients, byzantine_gradients], dim=0) - - # Aggregate using Krum (Byzantine-resilient) - robust_result = Krum.aggregate(all_gradients, n=n_workers, f=n_byzantine) +.. code-block:: bash - # Compare with simple average (not resilient) - naive_result = Average.aggregate(all_gradients) + pip install "krum[experiments]" - print(f"Krum result norm: {robust_result.norm().item():.4f}") - print(f"Average result norm: {naive_result.norm().item():.4f}") +This adds ``matplotlib``, ``numpy``, and ``seaborn``. -Key Concepts +Sanity check ------------ -Aggregators -~~~~~~~~~~~ - -Aggregators are **stateless** gradient aggregation rules. Call them as classmethods: - -.. code-block:: python - - from krum.primitives.aggregators import Average, Median, TrimmedMean, Krum, MultiKrum, Bulyan, Brute, GeoMed - - # Simple average (baseline, no resilience) - result = Average.aggregate(gradients) - - # Coordinate-wise median (basic resilience) - result = Median.aggregate(gradients) - - # Trimmed mean (basic resilience, requires 2f+1 workers) - result = TrimmedMean.aggregate(gradients, f=2) - - # Krum (moderate resilience, requires 2f+3 workers) - result = Krum.aggregate(gradients, n=10, f=2) - - # Multi-Krum (moderate resilience, averages m= n-f-2 gradients) - result = MultiKrum.aggregate(gradients, n=10, f=2) - - # Bulyan (strong resilience, two-stage, requires 4f+3 workers) - result = Bulyan.aggregate(gradients, n=15, f=2) - -Attacks -~~~~~~~ - -Attacks generate Byzantine gradients from honest worker gradients: - -.. code-block:: python - - from krum.primitives.attacks import SignFlip, ALIE, Gaussian, Omniscient, SmallPerturbation - - # Sign flip attack - byzantine = SignFlip.generate(honest_gradients, f=2, scale=1.5) - - # ALIE (A Little Is Enough) attack - byzantine = ALIE.generate(honest_gradients, f=2, z=2.0) - - # Gaussian attack - byzantine = Gaussian.generate(honest_gradients, f=2, std=10.0) - - # Omniscient attack (requires full dataset gradient) - byzantine = Omniscient.generate(honest_gradients, f=2, kappa=100.0, full_gradient=full_grad) - - # Small perturbation attack (exploits curse of dimensionality) - byzantine = SmallPerturbation.generate(honest_gradients, f=2, aggregator=Krum, n=10, p=2) - -Model Wrapper -~~~~~~~~~~~~~ - -Krum provides a ``Model`` wrapper for zero-copy flat views of PyTorch parameters and gradients: - -.. code-block:: python - - from krum.primitives.models import Model - import torch.nn as nn - - module = nn.Linear(10, 5) - model = Model(module) - - # Flat parameter view (zero-copy, lazy-initialized) - flat_params = model.parameters # shape: (55,) - - # Flat gradients after backward() - loss = module(torch.randn(3, 10)).sum() - loss.backward() - flat_grads = model.gradients # shape: (55,) - - # Write aggregated gradients back (zero-copy relink) - model.gradients = aggregated_flat - -.. note:: - - ``zero_grad(set_to_none=True)`` (the default since PyTorch 2.11) replaces - each ``.grad`` with ``None``, breaking the cached flat gradient view. - After calling ``zero_grad()``, access ``.gradients`` via - ``relink_gradients()`` to restore the link in a single call: - - .. code-block:: python - - optimizer.zero_grad() # drops .grad tensors - grads = model.relink_gradients() # re-link + get flat view - grads[:] = 0 # equivalent to zero_grad - - The same pattern applies to :meth:`~krum.primitives.models.Model.relink_parameters` - when a parameter's ``.data`` has been replaced externally. - - Both methods return the flat tensor directly, so no further property access is - needed. - -Standard Models -~~~~~~~~~~~~~~~ - -Krum provides standard models used in the literature for Byzantine-resilient -distributed learning simulations: - .. code-block:: python - from krum.primitives.models import Krum2017MLPMnist, Krum2017MLPSpambase, Krum2017CNN, Monna2023SmallMnist - - # MLP for MNIST (784 → 100 → 10) - mlp = Krum2017MLPMnist() - - # MLP for Spambase (57 → 20 → 20 → 2) - spambase = Krum2017MLPSpambase() - - # CNN for CIFAR-10 (3×32×32 → 10) - cnn = Krum2017CNN() - - # Small MLP for MNIST (784 → 128 → 10) - small_mnist = Monna2023SmallMnist() - -These models can be wrapped with the ``Model`` class for zero-copy flat views: - -.. code-block:: python - - from krum.primitives.models import Model, Krum2017MLPMnist + import torch + from krum.primitives.aggregators.krum import Krum - model = Model(Krum2017MLPMnist()) - flat_params = model.parameters # shape: (d,) where d ≈ 80,000 + result = Krum.aggregate(torch.randn(10, 100), n=10, f=2) + print(result.shape) # (100,) -Next Steps +Next steps ---------- -- Browse the :doc:`reference/primitives/models/index` for standard models -- Browse the :doc:`reference/primitives/aggregators/index` for all available aggregation rules -- Browse the :doc:`reference/primitives/attacks/index` for all available attack strategies -- See :doc:`reference/simulations/index` for reproducing published experiments in distributed settings +Dive into the :doc:`tutorials/index` for step-by-step guides: + +* :doc:`tutorials/centralised_simulation_walkthrough` — using the built-in simulations +* :doc:`tutorials/using_aggregators_attacks` — how to use all built-in aggregators and attacks +* :doc:`tutorials/working_with_models` — zero-copy flat tensor views and standard models diff --git a/docs/reference/primitives/aggregators/classes/aksel.rst b/docs/reference/primitives/aggregators/classes/aksel.rst index 640710b2..03aac439 100644 --- a/docs/reference/primitives/aggregators/classes/aksel.rst +++ b/docs/reference/primitives/aggregators/classes/aksel.rst @@ -1,7 +1,7 @@ AKSEL ===== -.. automodule:: aggregators.aksel +.. automodule:: krum.primitives.aggregators.aksel :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/average.rst b/docs/reference/primitives/aggregators/classes/average.rst index 9ad08a5b..ed53ebbd 100644 --- a/docs/reference/primitives/aggregators/classes/average.rst +++ b/docs/reference/primitives/aggregators/classes/average.rst @@ -1,7 +1,7 @@ Average ======= -.. automodule:: aggregators.average +.. automodule:: krum.primitives.aggregators.average :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/brute.rst b/docs/reference/primitives/aggregators/classes/brute.rst index 5bfcfc10..688c969a 100644 --- a/docs/reference/primitives/aggregators/classes/brute.rst +++ b/docs/reference/primitives/aggregators/classes/brute.rst @@ -1,7 +1,7 @@ Brute ===== -.. automodule:: aggregators.brute +.. automodule:: krum.primitives.aggregators.brute :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/bulyan.rst b/docs/reference/primitives/aggregators/classes/bulyan.rst index 62f103b6..c809788c 100644 --- a/docs/reference/primitives/aggregators/classes/bulyan.rst +++ b/docs/reference/primitives/aggregators/classes/bulyan.rst @@ -1,7 +1,7 @@ Bulyan ====== -.. automodule:: aggregators.bulyan +.. automodule:: krum.primitives.aggregators.bulyan :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/geomed.rst b/docs/reference/primitives/aggregators/classes/geomed.rst index 5f0d7c63..0ef8a962 100644 --- a/docs/reference/primitives/aggregators/classes/geomed.rst +++ b/docs/reference/primitives/aggregators/classes/geomed.rst @@ -1,7 +1,7 @@ GeoMed ====== -.. automodule:: aggregators.geomed +.. automodule:: krum.primitives.aggregators.geomed :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/krum.rst b/docs/reference/primitives/aggregators/classes/krum.rst index 80576c2c..f84a9f33 100644 --- a/docs/reference/primitives/aggregators/classes/krum.rst +++ b/docs/reference/primitives/aggregators/classes/krum.rst @@ -1,7 +1,7 @@ Krum ==== -.. automodule:: aggregators.krum +.. automodule:: krum.primitives.aggregators.krum :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/median.rst b/docs/reference/primitives/aggregators/classes/median.rst index cfd1e790..8f677fa4 100644 --- a/docs/reference/primitives/aggregators/classes/median.rst +++ b/docs/reference/primitives/aggregators/classes/median.rst @@ -1,7 +1,7 @@ Median ====== -.. automodule:: aggregators.median +.. automodule:: krum.primitives.aggregators.median :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/multikrum.rst b/docs/reference/primitives/aggregators/classes/multikrum.rst index e3803ebd..e3d72327 100644 --- a/docs/reference/primitives/aggregators/classes/multikrum.rst +++ b/docs/reference/primitives/aggregators/classes/multikrum.rst @@ -1,7 +1,7 @@ MultiKrum ========= -.. automodule:: aggregators.multikrum +.. automodule:: krum.primitives.aggregators.multikrum :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/nearest_neighbor_average.rst b/docs/reference/primitives/aggregators/classes/nearest_neighbor_average.rst index f3424619..47b2b98c 100644 --- a/docs/reference/primitives/aggregators/classes/nearest_neighbor_average.rst +++ b/docs/reference/primitives/aggregators/classes/nearest_neighbor_average.rst @@ -1,7 +1,7 @@ Nearest Neighbor Average ======================== -.. automodule:: aggregators.nearest_neighbor_average +.. automodule:: krum.primitives.aggregators.nearest_neighbor_average :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/aggregators/classes/trimmed_mean.rst b/docs/reference/primitives/aggregators/classes/trimmed_mean.rst index 31ff4751..91d43dc0 100644 --- a/docs/reference/primitives/aggregators/classes/trimmed_mean.rst +++ b/docs/reference/primitives/aggregators/classes/trimmed_mean.rst @@ -1,7 +1,7 @@ Trimmed Mean ============ -.. automodule:: aggregators.trimmed_mean +.. automodule:: krum.primitives.aggregators.trimmed_mean :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/attacks/classes/alie.rst b/docs/reference/primitives/attacks/classes/alie.rst index a75c86e5..535971ee 100644 --- a/docs/reference/primitives/attacks/classes/alie.rst +++ b/docs/reference/primitives/attacks/classes/alie.rst @@ -1,7 +1,7 @@ A Little Is Enough ================== -.. automodule:: attacks.alie +.. automodule:: krum.primitives.attacks.alie :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/attacks/classes/full_gradient_negation.rst b/docs/reference/primitives/attacks/classes/full_gradient_negation.rst index 143e1864..d7bbb924 100644 --- a/docs/reference/primitives/attacks/classes/full_gradient_negation.rst +++ b/docs/reference/primitives/attacks/classes/full_gradient_negation.rst @@ -1,7 +1,7 @@ Full Gradient Negation ====================== -.. automodule:: attacks.full_gradient_negation +.. automodule:: krum.primitives.attacks.full_gradient_negation :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/attacks/classes/gaussian.rst b/docs/reference/primitives/attacks/classes/gaussian.rst index dc5519d3..402e0fa2 100644 --- a/docs/reference/primitives/attacks/classes/gaussian.rst +++ b/docs/reference/primitives/attacks/classes/gaussian.rst @@ -1,7 +1,7 @@ Gaussian ======== -.. automodule:: attacks.gaussian +.. automodule:: krum.primitives.attacks.gaussian :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/attacks/classes/sign_flip.rst b/docs/reference/primitives/attacks/classes/sign_flip.rst index d0f0a522..a01254ec 100644 --- a/docs/reference/primitives/attacks/classes/sign_flip.rst +++ b/docs/reference/primitives/attacks/classes/sign_flip.rst @@ -1,7 +1,7 @@ SignFlip ======== -.. automodule:: attacks.sign_flip +.. automodule:: krum.primitives.attacks.sign_flip :members: :undoc-members: :show-inheritance: diff --git a/docs/reference/primitives/attacks/classes/small_perturbation.rst b/docs/reference/primitives/attacks/classes/small_perturbation.rst index c0f8c53f..a0426660 100644 --- a/docs/reference/primitives/attacks/classes/small_perturbation.rst +++ b/docs/reference/primitives/attacks/classes/small_perturbation.rst @@ -1,7 +1,7 @@ Small Perturbation ================== -.. automodule:: attacks.small_perturbation +.. automodule:: krum.primitives.attacks.small_perturbation :members: :undoc-members: :show-inheritance: diff --git a/docs/tutorials/centralised_simulation_walkthrough.rst b/docs/tutorials/centralised_simulation_walkthrough.rst new file mode 100644 index 00000000..b23e468e --- /dev/null +++ b/docs/tutorials/centralised_simulation_walkthrough.rst @@ -0,0 +1,154 @@ +Centralised simulation walkthrough +================================== + +**Problem:** You need to run a parameter-server simulation with +multiple workers, a gradient aggregator, and Byzantine attacks, but +you are not sure how to configure the training loop. + +Krum ships with ready-to-use centralised simulations that handle +the worker loop, gradient computation, and evaluation for you. + +.. seealso:: + + :doc:`/reference/simulations/centralised/index` + Reference for :class:`~krum.simulations.centralised.KrumSimulation` + and :class:`~krum.simulations.centralised.HiddenVulnerabilitySimulation`. + +Minimal example +--------------- + +Instantiation +^^^^^^^^^^^^^ + +Aggregator and attack are passed as **classes**, not instances. +The simulation calls ``aggregator.aggregate()`` and ``attack.generate()`` +each round. Extra parameters go through ``aggregator_kwargs`` and +``attack_kwargs``: + +.. code-block:: python + + from torchvision import datasets, transforms + + from krum.primitives.aggregators.multikrum import MultiKrum + from krum.primitives.attacks.sign_flip import SignFlipAttack + from krum.primitives.models.mlp import Krum2017MLPMnist + from krum.simulations.centralised.krum_nips_2017 import KrumSimulation + + transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)), + ]) + train_set = datasets.MNIST(root="./data", train=True, download=True, transform=transform) + test_set = datasets.MNIST(root="./data", train=False, download=True, transform=transform) + + sim = KrumSimulation( + model_cls=Krum2017MLPMnist, + train_set=train_set, + test_set=test_set, + aggregator=MultiKrum, + attack=SignFlipAttack, + attack_kwargs={"scale": 1.5}, + n=10, + f=2, + rounds=50, + batch_size=64, + lr=0.01, + seed=42, + ) + +Note that :class:`~krum.simulations.centralised.CentralisedSimulation` does +**not** provide a ``run(rounds)`` method. The training loop is always +manual so you can evaluate when you want (compare with the +:doc:`decentralised_simulation_walkthrough`, which does have ``run``). + +Setup +^^^^^ + +``setup()`` initialises the model parameters, splits the training set +into IID shards (one per worker), and seeds all RNG. The result is +deterministic for a given ``seed``: + +.. code-block:: python + + sim.setup() + +Step +^^^^ + +Each call to ``step()`` runs one synchronous round: + +* **Broadcast** the model to all :math:`n` workers. +* **Honest workers** compute gradients on their data shard. +* **Byzantine workers** generate attack gradients. +* **Aggregator** combines all :math:`n` gradients into one. +* **SGD update** is applied. + +.. code-block:: python + + for round_idx in range(50): + sim.step() + if round_idx % 10 == 0: + loss, accuracy = sim.evaluate() + print(f"round {round_idx}: loss={loss:.4f} accuracy={accuracy:.4f}") + +Evaluate +^^^^^^^^ + +``evaluate()`` returns the metrics specific to the protocol +(``(test_loss, test_accuracy)`` for :class:`~krum.simulations.centralised.KrumSimulation`, +``(test_loss, test_error, test_accuracy)`` for +:class:`~krum.simulations.centralised.HiddenVulnerabilitySimulation`). +You can also read the training loss with ``evaluate_train()``. + +Using the ICML 2018 simulation +------------------------------ + +Switching to the other built-in simulation changes only the import +and one extra parameter. The ICML 2018 variant adds Xavier weight +initialisation, L2 regularisation, and the Robbins-Monro learning-rate +schedule: + +.. code-block:: python + + from krum.simulations.centralised.hidden_vulnerability_icml_2018 import ( + HiddenVulnerabilitySimulation, + ) + + sim = HiddenVulnerabilitySimulation( + # same arguments as KrumSimulation ... + r_eta=10.0, # required by Robbins-Monro schedule + rounds=50, + ) + sim.setup() + for round_idx in range(50): + sim.step() + if round_idx % 10 == 0: + loss, error, accuracy = sim.evaluate() + print(f"round {round_idx}: loss={loss:.4f} error={error:.4f} accuracy={accuracy:.4f}") + + train_loss = sim.evaluate_train() + print(f"final training loss: {train_loss:.4f}") + +``HiddenVulnerabilitySimulation`` also accepts ``stop_attack_at``. This is an +optional round index after which the Byzantine attack is disabled (used +by the ICML 2018 paper, Experiment 1). Pass ``stop_attack_at=50`` to +stop the attack after round 50 while continuing training. + +Note that unlike the decentralised simulation, the centralised one +stores the round count at construction time (``rounds=50`` above) but +you drive the loop yourself. This lets you evaluate, log, or even change +hyperparameters between rounds. + +Next steps +---------- + +* :doc:`implement_aggregator`: write your own aggregation rule and test it + in this simulation. +* :doc:`implement_attack`: write your own Byzantine attack and test it + in this simulation. +* :doc:`decentralised_simulation_walkthrough`: peer-to-peer simulations + with per-worker models and model mixing. +* :doc:`structured_experiments`: collect structured results with + ``Metric`` and ``Orchestrator``, from single runs to systematic benchmarks. +* :doc:`/reference/simulations/index`: all bundled experiment scripts + reproducing published papers. diff --git a/docs/tutorials/decentralised_simulation_walkthrough.rst b/docs/tutorials/decentralised_simulation_walkthrough.rst new file mode 100644 index 00000000..5a3f9a78 --- /dev/null +++ b/docs/tutorials/decentralised_simulation_walkthrough.rst @@ -0,0 +1,296 @@ +Decentralised simulation walkthrough +==================================== + +**Problem:** Your scenario needs peer-to-peer communication with +per-worker models instead of a central parameter server. Each worker +trains its own model and the simulation handles model mixing between +neighbours each round. + +Krum ships with one built-in decentralised (peer-to-peer) simulation. +This tutorial covers the peer-to-peer framework where each worker holds +its **own** model and workers exchange models through a communication +topology. + +All decentralised simulations share the lifecycle: +**instantiate → step** (or ``run(rounds)``), with a per-round snapshot. + +.. seealso:: + + :doc:`/reference/simulations/decentralised/index` + Full reference for + :class:`~krum.simulations.decentralised.monna_icml_2023.MonnaSimulation`. + +Each round runs two phases: + +1. **Local optimisation**: each honest worker computes a gradient on its own + batch and updates its own model. +2. **Model mixing**: each worker gathers ``n - f`` models from other nodes + (*received set*) and replaces its model with an aggregate of that set. + +Minimal example +--------------- + +Data preparation +^^^^^^^^^^^^^^^^ + +The ``data`` argument of a decentralised simulation is a **sequence of +iterables**, one per honest worker. The standard pattern wraps a +:class:`~torch.utils.data.DataLoader` in an infinite cycle. Without the +cycle a stream that runs out raises ``StopIteration``: + +.. code-block:: python + + import random + import torch + import torch.nn as nn + from itertools import cycle + from torch.utils.data import DataLoader, Subset + from torchvision import datasets, transforms + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + seed = 42 + torch.manual_seed(seed) + random.seed(seed) + n, f = 6, 0 + + transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)), + ]) + train_set = datasets.MNIST(root="./data", train=True, download=True, transform=transform) + test_set = datasets.MNIST(root="./data", train=False, download=True, transform=transform) + + # One infinite data stream per honest worker + def cycle_loader(loader): + while True: + yield from loader + + workers_data = [ + cycle_loader(DataLoader( + Subset(train_set, range(i * 5000, (i + 1) * 5000)), + batch_size=64, shuffle=True, + )) + for i in range(n - f) + ] + +Any iterable of ``(inputs, targets)`` tuples also works: + +.. code-block:: python + + workers_data = [ + [(torch.randn(4, 784), torch.randint(0, 10, (4,))) for _ in range(100)] + for _ in range(4) + ] + +The IID ``Subset`` splitting above gives each worker an equal, shuffled +portion of the dataset. The experiment scripts in +``experiments/decentralised/`` add a ``split_dirichlet`` variant for +non-IID data (class-skew controlled by an ``alpha`` parameter). + +Instantiation +^^^^^^^^^^^^^ + +The model is wrapped in a :class:`~krum.primitives.models.Model` container +that exposes a ``.parameters`` tensor and a ``.module`` (the underlying +``nn.Module``). Pass the model, data streams, and hyperparameters to +the simulation constructor: + +.. code-block:: python + + from krum.primitives.models.mlp import Monna2023SmallMnist + from krum.primitives.models import Model + from krum.simulations.decentralised.monna_icml_2023 import MonnaSimulation + + model = Model(Monna2023SmallMnist().to(device)) + + sim = MonnaSimulation( + model=model, + data=workers_data, + loss_fn=nn.CrossEntropyLoss(), + n=n, + f=f, + learning_rate=0.1, + beta=0.99, + seed=seed, + ) + +``MonnaSimulation`` also accepts ``weight_decay`` (L2 regularization on +the honest gradients, ``0.0`` by default). Pass ``weight_decay=1e-4`` +to match the standard SGD-with-momentum recipe. + +Training +^^^^^^^^ + +Call ``run(rounds)`` to train. The result is a list of result dicts, one +per round: + +.. code-block:: python + + results = sim.run(50) + + # results is a list of MonnaStepResult dicts, one per round + print(f"Ran {len(results)} rounds") + print(f"Final per-worker losses: {results[-1]['losses']}") + +Inspecting the round snapshot +----------------------------- + +:meth:`~krum.simulations.decentralised.DecentralisedSimulation.step` returns a +:class:`~krum.simulations.decentralised.StepResult` dict (or a subclass like +:class:`~krum.simulations.decentralised.monna_icml_2023.MonnaStepResult`): + +.. code-block:: python + + result = sim.step() # a single round + +The returned dict contains: + +.. list-table:: + :header-rows: 1 + :widths: 20 40 15 + + * - Key + - Description + - Shape + * - ``step`` + - Round counter (1-indexed) + - scalar + * - ``parameters`` + - Committed parameters after mixing + - ``(n - f, d)`` + * - ``momentum`` + - Momentum buffer (``MonnaStepResult`` only) + - ``(n - f, d)`` + * - ``honest_gradients`` + - Computed gradients before local update + - ``(n - f, d)`` + * - ``local_parameters`` + - Parameters after local update, before mixing + - ``(n - f, d)`` + * - ``byzantine_parameters`` + - Byzantine models injected this round + - ``(f, d)`` + * - ``mixed_parameters`` + - Same as ``parameters`` (already committed) + - ``(n - f, d)`` + * - ``losses`` + - Per-worker scalar losses + - ``(n - f,)`` + +Byzantine workers +----------------- + +Add Byzantine workers by setting ``f > 0`` and providing an attack. Each +round the attack generates ``f`` Byzantine parameter vectors from the +honest ones. The +:attr:`~krum.simulations.decentralised.monna_icml_2023.MonnaSimulation.byzantine_reach` +mode controls which workers receive them: + +* ``"all"``: every Byzantine model reaches every worker (worst-case + adversary). +* ``"sampled"``: responders are drawn uniformly from all other nodes, + so a worker receives ``0`` to ``f`` Byzantine models. + +.. code-block:: python + + from krum.primitives.attacks.sign_flip import SignFlipAttack + + sim_all = MonnaSimulation( + model=model, data=workers_data, loss_fn=nn.CrossEntropyLoss(), + n=8, f=2, learning_rate=0.1, + attack=SignFlipAttack, attack_kwargs={"scale": 1.5}, + byzantine_reach="all", seed=42, + ) + + sim_sampled = MonnaSimulation( + model=model, data=workers_data, loss_fn=nn.CrossEntropyLoss(), + n=8, f=2, learning_rate=0.1, + attack=SignFlipAttack, attack_kwargs={"scale": 1.5}, + byzantine_reach="sampled", seed=42, + ) + + result_all = sim_all.run(50) + result_sampled = sim_sampled.run(50) + print(f"'all' mean final loss: {result_all[-1]['losses'].mean():.4f}") + print(f"'sampled' mean final loss: {result_sampled[-1]['losses'].mean():.4f}") + +Switching the mixing aggregator +------------------------------- + +By default ``MonnaSimulation`` uses +:class:`~krum.primitives.aggregators.nearest_neighbor_average.NearestNeighborAverage` +with ``num_closest = n - 2f``. Override it with any +:class:`~krum.primitives.aggregators.Aggregator` subclass. Pass extra +aggregator parameters through ``aggregator_kwargs``: + +.. code-block:: python + + from krum.primitives.aggregators.median import Median + + sim = MonnaSimulation( + model=model, + data=workers_data, + loss_fn=nn.CrossEntropyLoss(), + n=8, + f=2, + learning_rate=0.1, + attack=SignFlipAttack, + aggregator=Median, + seed=42, + ) + + results = sim.run(50) + +Evaluating worker models +------------------------ + +In the decentralised setting each worker has its own parameters. +Evaluate every honest worker on the same test set and average the +results. The function below loads each worker's parameter vector into +the shared model, runs the full test set, and averages across workers: + +.. code-block:: python + + @torch.no_grad() + def evaluate_workers(model, parameters, test_loader, loss_fn): + losses, accuracies = [], [] + for worker_params in parameters: + # copy worker params into the model + model.parameters.copy_(worker_params) + model.module.eval() + + # run the full test set for this worker + total_loss = total_correct = total = 0 + for inputs, targets in test_loader: + inputs, targets = inputs.to(device), targets.to(device) + logits = model.module(inputs) + loss = loss_fn(logits, targets) + total_loss += loss.item() * targets.numel() + total_correct += (logits.argmax(1) == targets).sum().item() + total += targets.numel() + + losses.append(total_loss / total) + accuracies.append(total_correct / total) + + # average across all honest workers + return sum(losses) / len(losses), sum(accuracies) / len(accuracies) + + test_loader = DataLoader(test_set, batch_size=256, shuffle=False) + avg_loss, avg_acc = evaluate_workers( + model, sim.parameters, test_loader, nn.CrossEntropyLoss() + ) + print(f"Average test loss: {avg_loss:.4f}, accuracy: {avg_acc:.2%}") + +Next steps +---------- + +* :doc:`centralised_simulation_walkthrough`: try the simpler + parameter-server simulation first if you haven't. +* :doc:`implement_aggregator`: write your own aggregation rule and test it + in this simulation. +* :doc:`implement_attack`: write your own Byzantine attack and test it + in this simulation. +* :doc:`structured_experiments`: collect structured results across + multiple configurations with ``Metric`` and ``Orchestrator``. +* :doc:`/reference/simulations/decentralised/index`: the full + decentralised simulation reference. diff --git a/docs/tutorials/implement_aggregator.rst b/docs/tutorials/implement_aggregator.rst new file mode 100644 index 00000000..8a2e066a --- /dev/null +++ b/docs/tutorials/implement_aggregator.rst @@ -0,0 +1,288 @@ +Implement a custom aggregator +============================= + +**Problem:** The built-in aggregation rules don't cover your use case. +How do you write your own aggregation rule? + +All aggregators in Krum follow the same protocol: + +* Subclass :class:`~krum.primitives.aggregators.Aggregator` +* Implement :meth:`~krum.primitives.aggregators.Aggregator.aggregate` as a ``@classmethod`` +* Accept ``gradients`` (first positional arg), an optional ``out`` tensor, and + rule-specific keyword arguments like ``f``, ``n``, ``m`` +* Return a single tensor of shape ``(d,)`` + +What we'll build +---------------- + +We'll implement **FirstGrad**, the simplest possible aggregator. It +discards all gradients except the first one. This is not a Byzantine-resilient +rule, but it illustrates the protocol in its purest form. + +We'll start with just ``gradients``, then add ``out`` for in-place output, +then ``**specialized`` for extra parameters. + +Step 1: subclass Aggregator +---------------------------- + +Create a file ``first_grad.py``: + +.. code-block:: python + + from collections.abc import Sequence + from typing import Any + + from torch import Tensor + + from krum.primitives.aggregators import Aggregator + + class FirstGrad(Aggregator): + ... + +Step 2: implement aggregate (gradients only) +---------------------------------------------- + +Start with only the ``gradients`` argument, the minimal contract: + +.. code-block:: python + + @classmethod + def aggregate( + cls, + gradients: Sequence[Tensor] | Tensor, + ) -> Tensor: + ... + +Normalise the input, then return the first gradient: + +.. code-block:: python + + @classmethod + def aggregate( + cls, + gradients: Sequence[Tensor] | Tensor, + ) -> Tensor: + if not isinstance(gradients, Tensor): + gradients = stack(list(gradients)) + + return gradients[0] + +That is the full algorithm. Feed it ``n`` worker gradients and get back +the gradient of worker 0. + +At this point the aggregator works for direct calls and the simulation +will accept it, but it ignores the ``out`` and ``**specialized`` +parameters that more advanced callers may pass. + +Step 3: add the out parameter +------------------------------- + +The optional ``out`` argument lets the caller pass a pre-allocated tensor. +When provided, write into it instead of returning a new tensor: + +.. code-block:: python + + @classmethod + def aggregate( + cls, + gradients: Sequence[Tensor] | Tensor, + out: Tensor | None = None, + ) -> Tensor: + if not isinstance(gradients, Tensor): + gradients = stack(list(gradients)) + + if out is not None: + out.copy_(gradients[0]) + return out + return gradients[0] + +With ``out``, the simulation can reuse the same output buffer every round +and avoid an allocation: + +.. code-block:: python + + buffer = torch.empty(100) + result = FirstGrad.aggregate(grads, out=buffer) + assert result is buffer # same tensor, no allocation + +.. note:: + + The ``out`` parameter is part of every aggregator's signature. It + enables the caller to control memory allocation. See the + :doc:`/reference/primitives/aggregators/index` for the full API + reference. + +Step 4: add specialized keyword arguments +------------------------------------------ + +Aggregators receive extra parameters like ``n`` and ``f`` from the +simulation. Absorb them with ``**specialized`` so the simulation +interface stays uniform: + +.. code-block:: python + + @classmethod + def aggregate( + cls, + gradients: Sequence[Tensor] | Tensor, + /, + out: Tensor | None = None, + **specialized: Any, + ) -> Tensor: + ... + +The ``/`` marks ``gradients`` as positional-only (prevents accidental +keyword usage). ``**specialized`` collects everything else (``n``, +``f``, ``m``, etc.) that the simulation passes automatically. You can +inspect them inside your algorithm: + +.. code-block:: python + + class FirstGrad(Aggregator): + @classmethod + def aggregate(cls, gradients, /, out=None, **specialized): + f = specialized.get("f", 0) + print(f"Running with f={f} Byzantine workers") + ... + +The simulation passes ``aggregator_kwargs`` directly into +``**specialized``, so your custom kwargs are accessible via +``specialized.get("my_param")``: + +.. code-block:: python + + class FirstGrad(Aggregator): + @classmethod + def aggregate(cls, gradients, /, out=None, **specialized): + my_param = specialized.get("my_param", "default") + ... + +To pass kwargs to a simulation: + +.. code-block:: python + + sim = KrumSimulation( + ..., + aggregator=FirstGrad, + aggregator_kwargs={"my_param": 42}, + ) + +Full code +--------- + +.. code-block:: python + + from collections.abc import Sequence + from typing import Any + + import torch + from torch import Tensor, stack + + from krum.primitives.aggregators import Aggregator + + + class FirstGrad(Aggregator): + """Aggregation rule that keeps only the first gradient. + + This is a pedagogical rule, not a robust one. Every worker + after the first is ignored entirely. + """ + + @classmethod + def aggregate( + cls, + gradients: Sequence[Tensor] | Tensor, + /, + out: Tensor | None = None, + **specialized: Any, + ) -> Tensor: + if not isinstance(gradients, Tensor): + gradients = stack(list(gradients)) + + if out is not None: + return out.copy_(gradients[0]) + return gradients[0] + +Using your aggregator +--------------------- + +Import it and call it like any built-in aggregator. Aggregators are +**stateless**, so you pass the class itself, never an instance: + +.. code-block:: python + + import torch + from first_grad import FirstGrad + + grads = torch.randn(10, 100) + result = FirstGrad.aggregate(grads) + print(result.shape) # (100,) + print(result is grads[0]) # True, same tensor + +In a simulation +--------------- + +Pass the **class** (not an instance) to a simulation: + +.. code-block:: python + + from krum.primitives.attacks.sign_flip import SignFlipAttack + from krum.primitives.models.mlp import Krum2017MLPMnist + from krum.simulations.centralised.krum_nips_2017 import KrumSimulation + + sim = KrumSimulation( + model_cls=Krum2017MLPMnist, + train_set=train_set, + test_set=test_set, + aggregator=FirstGrad, + attack=SignFlipAttack, + attack_kwargs={"scale": 1.5}, + n=10, f=2, rounds=50, batch_size=64, lr=0.01, seed=42, + ) + sim.setup() + for _ in range(50): + sim.step() + loss, accuracy = sim.evaluate() + +Testing +------- + +A minimal test suite. Run with ``pytest first_grad.py``: + +.. code-block:: python + + import pytest + import torch + + from first_grad import FirstGrad + + def test_first_grad_shape(): + grads = torch.randn(10, 100) + result = FirstGrad.aggregate(grads) + assert result.shape == (100,) + + def test_first_grad_returns_first(): + grads = torch.randn(10, 100) + result = FirstGrad.aggregate(grads) + assert torch.allclose(result, grads[0]) + + def test_first_grad_uses_out(): + grads = torch.randn(10, 100) + buffer = torch.empty(100) + result = FirstGrad.aggregate(grads, out=buffer) + assert result is buffer + assert torch.allclose(result, grads[0]) + +Next steps +---------- + +* :doc:`implement_attack`: write a Byzantine attack to test your + aggregator against. +* :doc:`centralised_simulation_walkthrough`: test your aggregator in a + full training loop. +* :doc:`decentralised_simulation_walkthrough`: use the same aggregator in a + peer-to-peer setting. +* :doc:`structured_experiments`: benchmark your aggregator across seeds + and attacks with ``Orchestrator``. +* Browse the :doc:`/reference/primitives/aggregators/index` for all built-in + aggregation rules. diff --git a/docs/tutorials/implement_attack.rst b/docs/tutorials/implement_attack.rst new file mode 100644 index 00000000..062e15f0 --- /dev/null +++ b/docs/tutorials/implement_attack.rst @@ -0,0 +1,296 @@ +Implement a custom attack +========================= + +**Problem:** You want to stress-test an aggregator against a novel attack +not yet in the library. How do you implement a custom Byzantine attack +strategy from scratch? + +All attacks in Krum follow the same protocol: + +* Subclass :class:`~krum.primitives.attacks.Attack` +* Implement :meth:`~krum.primitives.attacks.Attack.generate` as a ``@classmethod`` +* Accept ``honest_gradients`` (first positional arg), an optional ``out`` tensor, + a keyword-only ``f`` (number of Byzantine gradients to produce), and any + attack-specific keyword arguments +* Return a tensor of shape ``(f, d)`` + +What we'll build +---------------- + +We'll implement **RepeatAttack**, the simplest possible Byzantine attack. +It takes the first honest gradient and repeats it ``f`` times. Every +Byzantine worker sends the exact same gradient. The intuition: if honest +workers are converging, sending a stale or misleading gradient repeated +many times can shift the aggregate away from the true direction. + +Step 1: subclass Attack +------------------------- + +Create a file ``repeat_attack.py``: + +.. code-block:: python + + from collections.abc import Sequence + from typing import Any + + from torch import Tensor + + from krum.primitives.attacks import Attack + + class RepeatAttack(Attack): + ... + +Step 2: implement generate (gradients only) +---------------------------------------------- + +Start without ``out`` or ``**specialized`` to see the core logic: + +.. code-block:: python + + @classmethod + def generate( + cls, + honest_gradients: Sequence[Tensor] | Tensor, + *, + f: int, + ) -> Tensor: + ... + +Stack the honest gradients, take the first one, and repeat it: + +.. code-block:: python + + @classmethod + def generate( + cls, + honest_gradients: Sequence[Tensor] | Tensor, + *, + f: int, + ) -> Tensor: + if not isinstance(honest_gradients, Tensor): + honest_gradients = stack(list(honest_gradients)) + + _, d = honest_gradients.shape + first = honest_gradients[0:1] # (1, d) + + if f == 0: + return honest_gradients.new_empty((0, d)) + + return first.expand(f, d) # broadcast to (f, d) + +If ``f`` is zero the attack returns an empty tensor. The simulation handles +this correctly. + +Step 3: add the out parameter +------------------------------- + +Add support for the optional output buffer: + +.. code-block:: python + + @classmethod + def generate( + cls, + honest_gradients: Sequence[Tensor] | Tensor, + /, + out: Tensor | None = None, + *, + f: int, + ) -> Tensor: + if not isinstance(honest_gradients, Tensor): + honest_gradients = stack(list(honest_gradients)) + + _, d = honest_gradients.shape + first = honest_gradients[0:1] + + if f == 0: + empty = honest_gradients.new_empty((0, d)) + if out is not None: + return out.copy_(empty) + return empty + + result = first.expand(f, d) + if out is not None: + return out.copy_(result) + return result + +.. note:: + + The ``out`` parameter is part of every attack's signature. It + enables the caller to control memory allocation. See the + :doc:`/reference/primitives/attacks/index` for the full API + reference. + +Step 4: add specialized keyword arguments +------------------------------------------ + +Add ``**specialized`` to absorb whatever the simulation passes: + +.. code-block:: python + + @classmethod + def generate( + cls, + honest_gradients: Sequence[Tensor] | Tensor, + /, + out: Tensor | None = None, + *, + f: int, + **specialized: Any, + ) -> Tensor: + ... + +To pass your own custom keyword arguments, use ``attack_kwargs`` on the +simulation: + +.. code-block:: python + + sim = KrumSimulation( + ..., + attack=RepeatAttack, + attack_kwargs={"my_param": 42}, + ) + +Inside the attack, read them from ``specialized``: + +.. code-block:: python + + class RepeatAttack(Attack): + @classmethod + def generate(cls, honest_gradients, /, out=None, *, f, **specialized): + threshold = specialized.get("my_param", 0) + ... + +Full code +--------- + +.. code-block:: python + + from collections.abc import Sequence + from typing import Any + + from torch import Tensor, stack + + from krum.primitives.attacks import Attack + + + class RepeatAttack(Attack): + """Byzantine attack that repeats the first honest gradient f times. + + Every Byzantine worker sends the same gradient. Simple to + implement, useful as a baseline for testing aggregators. + """ + + @classmethod + def generate( + cls, + honest_gradients: Sequence[Tensor] | Tensor, + /, + out: Tensor | None = None, + *, + f: int, + **specialized: Any, + ) -> Tensor: + if not isinstance(honest_gradients, Tensor): + honest_gradients = stack(list(honest_gradients)) + + _, d = honest_gradients.shape + first = honest_gradients[0:1] + + if f == 0: + empty = honest_gradients.new_empty((0, d)) + if out is not None: + return out.copy_(empty) + return empty + + result = first.expand(f, d) + if out is not None: + return out.copy_(result) + return result + +Using your attack +----------------- + +Import it and call it like any built-in attack. Attacks are **stateless**, +so you pass the class itself, never an instance: + +.. code-block:: python + + import torch + from repeat_attack import RepeatAttack + + honest = torch.randn(8, 100) + byzantine = RepeatAttack.generate(honest, f=2) + print(byzantine.shape) # (2, 100) + print(torch.allclose(byzantine[0], byzantine[1])) # True, all identical + +In a simulation +--------------- + +Pass the **class** (not an instance) to a simulation: + +.. code-block:: python + + from krum.primitives.aggregators.multikrum import MultiKrum + from krum.primitives.models.mlp import Krum2017MLPMnist + from krum.simulations.centralised.krum_nips_2017 import KrumSimulation + + sim = KrumSimulation( + model_cls=Krum2017MLPMnist, + train_set=train_set, + test_set=test_set, + aggregator=MultiKrum, + attack=RepeatAttack, + n=10, f=2, rounds=50, batch_size=64, lr=0.01, seed=42, + ) + sim.setup() + for _ in range(50): + sim.step() + loss, accuracy = sim.evaluate() + +Testing +------- + +A minimal test suite. Run with ``pytest repeat_attack.py``: + +.. code-block:: python + + import pytest + import torch + + from repeat_attack import RepeatAttack + + def test_generates_correct_shape(): + honest = torch.randn(8, 100) + byzantine = RepeatAttack.generate(honest, f=2) + assert byzantine.shape == (2, 100) + + def test_generates_no_byzantine_when_f_is_zero(): + honest = torch.randn(8, 100) + byzantine = RepeatAttack.generate(honest, f=0) + assert byzantine.shape == (0, 100) + + def test_all_byzantine_are_identical(): + honest = torch.randn(8, 100) + byzantine = RepeatAttack.generate(honest, f=3) + assert torch.allclose(byzantine[0], byzantine[1]) + assert torch.allclose(byzantine[0], byzantine[2]) + + def test_byzantine_matches_first_honest(): + honest = torch.ones(4, 10) + byzantine = RepeatAttack.generate(honest, f=1) + assert torch.allclose(byzantine[0], honest[0]) + +Next steps +---------- + +* :doc:`implement_aggregator`: write an aggregation rule that defends + against your attack. +* :doc:`centralised_simulation_walkthrough`: test your attack in a + full training loop. +* :doc:`decentralised_simulation_walkthrough`: use the same attack in a + peer-to-peer setting. +* :doc:`structured_experiments`: benchmark your attack across seeds and + aggregators with ``Orchestrator``. +* Browse the :doc:`/reference/primitives/attacks/index` for all built-in + attacks. diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst new file mode 100644 index 00000000..aa507047 --- /dev/null +++ b/docs/tutorials/index.rst @@ -0,0 +1,73 @@ +Tutorials +========= + +Step-by-step guides to help you get the most out of Krum. + +Start with :doc:`using_aggregators_attacks` and +:doc:`working_with_models` to learn the basics. Next, +:doc:`implement_aggregator` and :doc:`implement_attack` show you how to +extend Krum. Then run full simulations with +:doc:`centralised_simulation_walkthrough` or +:doc:`decentralised_simulation_walkthrough`. +Finally, :doc:`structured_experiments` covers data collection and +analysis at scale. + +Available Tutorials +------------------- + +.. toctree:: + :maxdepth: 1 + + using_aggregators_attacks + working_with_models + implement_aggregator + implement_attack + centralised_simulation_walkthrough + decentralised_simulation_walkthrough + structured_experiments + +Detailed Tutorials +----------------- + +.. list-table:: + :widths: 25 75 + + * - :doc:`using_aggregators_attacks` + - All built-in aggregation rules and attack strategies with a + resilience table and a combined example. + + See :doc:`/reference/primitives/aggregators/index` and + :doc:`/reference/primitives/attacks/index`. + * - :doc:`working_with_models` + - The ``Model`` wrapper for zero-copy flat tensor views plus the + standard models from the literature (Krum NIPS 2017, MONNA ICML 2023). + + See :doc:`/reference/primitives/models/index`. + * - :doc:`implement_aggregator` + - Subclass ``Aggregator`` and implement ``aggregate``, with tests. + Build from gradients-only to fully parameterised. + + See :doc:`/reference/primitives/aggregators/index`. + * - :doc:`implement_attack` + - Subclass ``Attack`` and implement ``generate``, with tests. + + See :doc:`/reference/primitives/attacks/index`. + * - :doc:`centralised_simulation_walkthrough` + - Parameter-server simulation with ``KrumSimulation``: MultiKrum + + SignFlip on MNIST, dataset setup, training loop, baseline comparison. + + See :doc:`/reference/simulations/centralised/index` and + :doc:`/reference/simulations/decentralised/index`. + * - :doc:`decentralised_simulation_walkthrough` + - Peer-to-peer simulations with ``DecentralisedSimulation`` and + ``MonnaSimulation``: per-worker models, model mixing, Byzantine + reach modes, custom data streams. + + See :doc:`/reference/simulations/decentralised/index`. + * - :doc:`structured_experiments` + - Collect metrics with ``Metric`` and ``Orchestrator``, analyse + with filtering and plotting, run N×M benchmarks. + + See :doc:`/reference/orchestration/index` and + :doc:`/reference/orchestration/metricdataframe`. + diff --git a/docs/tutorials/structured_experiments.rst b/docs/tutorials/structured_experiments.rst new file mode 100644 index 00000000..8f2e6b9e --- /dev/null +++ b/docs/tutorials/structured_experiments.rst @@ -0,0 +1,370 @@ +Structured experiments +====================== + +**Problem:** Running a single simulation is fine for quick tests, but +research requires comparing configurations, collecting metrics at every +step, analysing results across seeds, and producing tables for papers. +How do you go from a one-off run to a reproducible, structured +experiment? + +Krum provides three tools for this: + +* :class:`~krum.orchestration.metric.Metric`: a named channel you push + ``(step, value)`` samples into during a run. +* :class:`~krum.orchestration.orchestrator.Orchestrator`: drives multiple + runs, owns all collected metrics, and returns them as a + :class:`~krum.orchestration.dataframe.MetricDataFrame`. +* :class:`~krum.orchestration.dataframe.MetricDataFrame`: a filtered, + queryable view of one metric channel, convertible to ``pandas``. + +The Metric object +----------------- + +A :class:`~krum.orchestration.metric.Metric` is created inside an experiment +function with a name and a value type: + +.. code-block:: python + + from krum.orchestration import Metric + + loss = Metric("test_loss", dtype=float) + accuracy = Metric("test_accuracy", dtype=float) + +.. warning:: + + :class:`~krum.orchestration.metric.Metric` can only be created **inside** an + :meth:`~krum.orchestration.orchestrator.Orchestrator.run` call. Creating one outside an + active run raises ``RuntimeError``. The metric is a write handle that + routes every push to the orchestrator driving the current experiment. + +Each call to :meth:`~krum.orchestration.metric.Metric.push` records one sample, +tagged with the current run's parameters: + +.. code-block:: python + + loss.push(step=10, value=0.1523) + accuracy.push(step=10, value=0.9531) + +The metric is just a **write handle**; it does not store the data itself. +Every push is routed to the orchestrator that is running the current +experiment. + +The Orchestrator +----------------- + +An :class:`~krum.orchestration.orchestrator.Orchestrator` runs a function multiple times +with different parameters and collects all the metrics pushed during each run: + +.. code-block:: python + + from krum.orchestration import Orchestrator + + orchestrator = Orchestrator("my_campaign") + + for lr in [0.01, 0.001]: + orchestrator.run(my_experiment, lr=lr, label=f"lr_{lr}") + +Once all runs are finished, retrieve every sample of a metric: + +.. code-block:: python + + frame = orchestrator.get("test_loss").to_pandas() + print(frame) + +The resulting ``pandas.DataFrame`` has one row per recorded step, with +columns for the run parameters (``label``, ``lr``, etc.), ``step``, and +``value``. + +How Metric, Orchestrator, and MetricDataFrame work together +------------------------------------------------------------ + +These three objects form a pipeline: you push data through a ``Metric``, +it lands in the ``Orchestrator``'s internal store tagged with run +parameters, and you retrieve a filtered view via ``Orchestrator.get()`` +which returns a ``MetricDataFrame``. + +The flow:: + + Orchestrator.run(fn, label="A", seed=42, …) + │ + ▼ + ┌────────────────────────────────────────┐ + │ fn(**params) │ + │ │ + │ Metric("acc").push(step, 0.95) │ + │ │ │ + │ │ thread-local context │ + │ ▼ │ + │ Orchestrator._record() │ + │ │ │ + │ ▼ │ + │ Internal store │ + │ ┌─────┬──────┬──────┬───────┬──────┐ │ + │ │name │ step │ val │ label │ seed │ │ + │ ├─────┼──────┼──────┼───────┼──────┤ │ + │ │ acc │ 0 │ 0.92 │ A │ 42 │ │ + │ │ acc │ 10 │ 0.95 │ A │ 42 │ │ + │ │ acc │ 20 │ 0.96 │ A │ 42 │ │ + │ │ acc │ 10 │ 0.88 │ B │ 43 │ │ + │ └─────┴──────┴──────┴───────┴──────┘ │ + └────────────────────────────────────────┘ + │ + ▼ + Orchestrator.get("acc") + │ + ▼ + MetricDataFrame ─── .filter(label="A") + .to_pandas() ───► pandas.DataFrame + +Key design decisions: + +- **Orchestrator owns the data.** Metric is a light proxy that discovers + the active orchestrator through thread-local state; you never pass the + orchestrator to the metric explicitly. +- **MetricDataFrame is a lazy view.** ``filter()`` chains without copying + data; ``to_pandas()`` materialises only at the end. + +A complete example +------------------ + +The following experiment runs a Krum simulation twice: once with a robust +aggregator and once with the Average baseline, collecting the results as +structured metrics. + +Setup +^^^^^ + +Imports, MNIST, and an MLP: + +.. code-block:: python + + from krum.orchestration import Metric, Orchestrator + from krum.primitives.aggregators.average import Average + from krum.primitives.aggregators.multikrum import MultiKrum + from krum.primitives.attacks.sign_flip import SignFlipAttack + from krum.primitives.models.mlp import Krum2017MLPMnist + from krum.simulations.centralised.krum_nips_2017 import KrumSimulation + + from torchvision import datasets, transforms + + transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize((0.1307,), (0.3081,)), + ]) + train_set = datasets.MNIST( + root="./data", train=True, download=True, transform=transform + ) + test_set = datasets.MNIST( + root="./data", train=False, download=True, transform=transform + ) + +Experiment function +^^^^^^^^^^^^^^^^^^^ + +Creates the simulation, loops over rounds, and +pushes metrics. The function accepts every configurable parameter so it +can be driven by the ``Orchestrator``: + +.. code-block:: python + + def run_experiment( + *, + label: str, + aggregator, + attack, + f: int, + n: int = 10, + lr: float = 0.01, + seed: int = 42, + attack_kwargs: dict | None = None, + rounds: int = 50, + batch_size: int = 64, + eval_every: int = 10, + ) -> None: + sim = KrumSimulation( + model_cls=Krum2017MLPMnist, + train_set=train_set, test_set=test_set, + aggregator=aggregator, attack=attack, + attack_kwargs=attack_kwargs, + n=n, f=f, rounds=rounds, + batch_size=batch_size, lr=lr, seed=seed, + ) + sim.setup() + + test_loss = Metric("test_loss", float) + test_accuracy = Metric("test_accuracy", float) + train_loss = Metric("train_loss", float) + + for step in range(rounds): + sim.step() + if step % eval_every == 0 or step == rounds - 1: + loss_val, acc_val = sim.evaluate() + test_loss.push(step, loss_val) + test_accuracy.push(step, acc_val) + train_loss.push(step, sim.evaluate_train()) + + print(f" {label}: final accuracy {acc_val:.2%}") + +Run the two configurations +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Each ``orchestrator.run()`` call records +every parameter so the data is self-describing: + +.. code-block:: python + + orchestrator = Orchestrator("mnist_comparison") + + orchestrator.run( + run_experiment, + label="MultiKrum (robust)", + aggregator=MultiKrum, + attack=SignFlipAttack, + attack_kwargs={"scale": 1.5}, + f=2, + ) + orchestrator.run( + run_experiment, + label="Average (non-robust)", + aggregator=Average, + attack=SignFlipAttack, + attack_kwargs={"scale": 1.5}, + f=2, + ) + +Inspect the results +^^^^^^^^^^^^^^^^^^^ + +``Orchestrator.get()`` returns a +``MetricDataFrame`` that supports filtering: + +.. code-block:: python + + print("\nAll results (last 5 rows):") + print(orchestrator.get("test_accuracy").to_pandas().tail(5)) + + print("\nMultiKrum only:") + print(orchestrator.get("test_accuracy").filter(label="MultiKrum (robust)").to_pandas()) + +Analysing results +----------------- + +Once you have a :class:`~krum.orchestration.dataframe.MetricDataFrame`, +convert it to ``pandas`` and use your usual toolkit: + +.. code-block:: python + + import matplotlib.pyplot as plt + import seaborn as sns + + df = orchestrator.get("test_accuracy").to_pandas() + + # Filter to one configuration, get the final value + final = df[df["step"] == 49] + best = final.loc[final["value"].idxmax()] + print(f"{best['label']}: {best['value']:.2%}") + + # Compare curves across labels + sns.lineplot(data=df, x="step", y="value", hue="label") + plt.title("Test accuracy per configuration") + plt.show() + + # Pivot so each run is a column + pivoted = df.pivot_table(index="step", columns="label", values="value") + pivoted.to_csv("accuracy.csv") + +See :doc:`/reference/orchestration/metricdataframe` for filtering options +and the :doc:`/reference/orchestration/index` for the full API. + +Systematic benchmark +-------------------- + +Byzantine-robust research typically compares multiple aggregation rules +against multiple attacks on a shared dataset. This section shows how to +run such a benchmark with ``Orchestrator`` and produce a comparison table. + +We build on the same MNIST + MLP setup from the previous example, but run +every combination of aggregators and attacks across multiple seeds. + +Setup +^^^^^ + +Import the aggregators and attacks we want to compare, and define the +grid constants: + +.. code-block:: python + + from krum.primitives.aggregators.average import Average + from krum.primitives.aggregators.median import Median + from krum.primitives.aggregators.trimmed_mean import TrimmedMean + from krum.primitives.aggregators.multikrum import MultiKrum + from krum.primitives.attacks.sign_flip import SignFlipAttack + from krum.primitives.attacks.alie import ALIEAttack + from krum.primitives.attacks.gaussian import GaussianAttack + + orch = Orchestrator("mnist_benchmark") + N, F, ROUNDS = 15, 3, 50 + SEEDS = [42, 43, 44] + +Running the grid +^^^^^^^^^^^^^^^^ + +Loop over every aggregator-attack-seed combination. Each call to +``orchestrator.run()`` records the experiment and tags it with its +parameters: + +.. code-block:: python + + for agg in [Average, Median, TrimmedMean, MultiKrum]: + for atk in [None, SignFlipAttack, ALIEAttack, GaussianAttack]: + atk_label = atk.__name__ if atk else "NoAttack" + label = f"{agg.__name__} + {atk_label}" + for seed in SEEDS: + orch.run( + run_experiment, + label=label, aggregator=agg, attack=atk, + f=F, n=N, lr=0.1, seed=seed, + ) + +Building the comparison table +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Collect the final accuracy, average across seeds, and pivot into a +matrix: + +.. code-block:: python + + df = orch.get("test_accuracy").to_pandas() + final = df[df["step"] == ROUNDS - 1] + + stats = final.groupby(["aggregator", "attack"])["value"].agg(["mean", "std"]).reset_index() + + table = stats.pivot_table( + index="attack", columns="aggregator", values="mean", + ) + table.index = [a.__name__ if a else "None" for a in table.index] + + print(table.round(2)) + +The output is a matrix where each cell is the mean accuracy for one +aggregator-attack pair, averaged across seeds. Use ``.std`` for error +bars in follow-up plots. + +As a rule of thumb, robust aggregators (MultiKrum, TrimmedMean) maintain +high accuracy across attack types, while non-robust baselines (Average) +collapse. Results vary with ``n``, ``f``, model size, and dataset; real +papers report ``mean ± std`` over 5–10 seeds. See the aggregator and +attack docstrings for configuration-specific constraints +(e.g., minimum ``n`` for Bulyan, extra kwargs for attacks like +SmallPerturbation). + +Next steps +---------- + +* :doc:`implement_aggregator`: write your own aggregation rule and + benchmark it with the patterns from this tutorial. +* :doc:`implement_attack`: write your own Byzantine attack and + benchmark it. +* :doc:`/reference/orchestration/index`: full Orchestrator and Metric API. +* :doc:`/reference/orchestration/metricdataframe`: available filtering + options. diff --git a/docs/tutorials/using_aggregators_attacks.rst b/docs/tutorials/using_aggregators_attacks.rst new file mode 100644 index 00000000..f0d5af7f --- /dev/null +++ b/docs/tutorials/using_aggregators_attacks.rst @@ -0,0 +1,203 @@ +Using Aggregators and Attacks +============================= + +**Problem:** You need to pick an aggregation rule and an attack strategy +for your experiment, but the library offers many options. How do you +choose the right ones and understand their resilience guarantees? + +This tutorial covers all built-in aggregation rules and attack strategies +available in Krum. + +Aggregators +----------- + +Aggregators are **stateless** gradient aggregation rules. Call them as classmethods: + +.. code-block:: python + + from krum.primitives.aggregators.average import Average + from krum.primitives.aggregators.median import Median + from krum.primitives.aggregators.trimmed_mean import TrimmedMean + from krum.primitives.aggregators.krum import Krum + from krum.primitives.aggregators.multikrum import MultiKrum + from krum.primitives.aggregators.bulyan import Bulyan + from krum.primitives.aggregators.aksel import Aksel + from krum.primitives.aggregators.geomed import GeoMed + from krum.primitives.aggregators.nearest_neighbor_average import NearestNeighborAverage + + # Simple average (baseline, no resilience) + result = Average.aggregate(gradients) + + # Coordinate-wise median (basic resilience) + result = Median.aggregate(gradients) + + # Geometric median (basic resilience; n and f accepted for API uniformity) + result = GeoMed.aggregate(gradients, n=10, f=2) + + # Trimmed mean (basic resilience, requires 2f+1 workers) + result = TrimmedMean.aggregate(gradients, f=2) + + # Krum (moderate resilience, requires 2f+3 workers) + result = Krum.aggregate(gradients, n=10, f=2) + + # Multi-Krum (moderate resilience, averages m = n - 2f - 3 gradients by default) + result = MultiKrum.aggregate(gradients, n=10, f=2) + + # Bulyan (strong resilience, two-stage, requires 4f+3 workers) + result = Bulyan.aggregate(gradients, n=15, f=2) + + # Aksel (optimal breakdown point, requires n > 2f) + result = Aksel.aggregate(gradients, f=2) + + # Nearest-neighbor average (model-mixing rule, requires a pivot) + result = NearestNeighborAverage.aggregate(gradients, pivot=gradients[0], num_closest=3) + +.. seealso:: + + :doc:`/reference/primitives/aggregators/index` for the full list with + resilience guarantees, algorithmic details, and literature references. + +Input shape +~~~~~~~~~~~ + +All aggregators expect a 2D tensor of shape ``(n, d)`` where: + +* ``n``: number of workers +* ``d``: gradient dimension (total number of parameters) + +The output is a 1D tensor of shape ``(d,)``. + +Resilience guarantees +~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - Aggregator + - Resilience + - Requirement + * - :class:`~krum.primitives.aggregators.average.Average` + - None (baseline) + - ``n >= 1`` + * - :class:`~krum.primitives.aggregators.median.Median` + - ``f < n / 2`` + - ``n >= 1`` + * - :class:`~krum.primitives.aggregators.geomed.GeoMed` + - ``f < n / 2`` + - ``n >= 1`` + * - :class:`~krum.primitives.aggregators.trimmed_mean.TrimmedMean` + - ``f < n / 2`` + - ``n >= 2f + 1`` + * - :class:`~krum.primitives.aggregators.aksel.Aksel` + - ``f < n / 2`` (optimal breakdown point) + - ``n > 2f`` + * - :class:`~krum.primitives.aggregators.krum.Krum` + - ``2f + 2 < n`` + - ``n >= 2f + 3`` + * - :class:`~krum.primitives.aggregators.multikrum.MultiKrum` + - ``2f + 2 < n`` + - ``n >= 2f + 3`` + * - :class:`~krum.primitives.aggregators.brute.Brute` + - ``f < n / 2`` (exact, exponential cost) + - ``n >= 2f + 1`` and ``f >= 1`` + * - :class:`~krum.primitives.aggregators.bulyan.Bulyan` + - ``4f + 2 < n`` + - ``n >= 4f + 3`` + * - :class:`~krum.primitives.aggregators.nearest_neighbor_average.NearestNeighborAverage` + - ``f < (n - num_closest) / 2`` + - ``n > num_closest`` + +Two rules deserve a caveat: + +* :class:`~krum.primitives.aggregators.brute.Brute` enumerates all + :math:`\binom{n}{n-f}` subsets to find the most clumped one. It is exact but + only feasible for small worker counts. +* :class:`~krum.primitives.aggregators.nearest_neighbor_average.NearestNeighborAverage` + is a **model-mixing** rule, not a gradient aggregator. It averages the + ``num_closest`` vectors nearest to a per-worker ``pivot``. It is the default + mixing rule of the decentralised MoNNA simulation and also works + in the parameter-server setting when passed as ``aggregator``. + +Attacks +------- + +Attacks generate Byzantine gradients from honest worker gradients: + +.. code-block:: python + + from krum.primitives.aggregators.multikrum import MultiKrum + from krum.primitives.attacks.sign_flip import SignFlipAttack + from krum.primitives.attacks.alie import ALIEAttack + from krum.primitives.attacks.gaussian import GaussianAttack + from krum.primitives.attacks.small_perturbation import SmallPerturbationAttack + from krum.primitives.attacks.full_gradient_negation import FullGradientNegationAttack + + # Sign flip attack + byzantine = SignFlipAttack.generate(honest_gradients, f=2, scale=1.5) + + # ALIE (A Little Is Enough) attack + byzantine = ALIEAttack.generate(honest_gradients, f=2, z=2.0) + + # Gaussian attack + byzantine = GaussianAttack.generate(honest_gradients, f=2, std=10.0) + + # Full gradient negation attack (requires the full-dataset gradient) + byzantine = FullGradientNegationAttack.generate( + honest_gradients, f=2, full_gradient=full_grad + ) + + # Small perturbation attack (targets a specific aggregator) + byzantine = SmallPerturbationAttack.generate( + honest_gradients, f=2, aggregator=MultiKrum, n=10 + ) + +All attacks follow the same pattern: pass the honest gradients and the number +of Byzantine workers ``f``, and they return a tensor of shape ``(f, d)``. + +.. seealso:: + + :doc:`/reference/primitives/attacks/index` for the full list with + attack mechanics, parameters, and literature references. + +Combining everything +-------------------- + +The example below generates a SignFlip attack and compares a robust +aggregator (MultiKrum) against a non-robust baseline (Average): + +.. code-block:: python + + import torch + from krum.primitives.aggregators.average import Average + from krum.primitives.aggregators.multikrum import MultiKrum + from krum.primitives.attacks.sign_flip import SignFlipAttack + + n_workers, n_byzantine, dim = 10, 2, 100 + + honest = torch.randn(n_workers - n_byzantine, dim) + malicious = SignFlipAttack.generate(honest, f=n_byzantine, scale=1.5) + all_grads = torch.cat([honest, malicious], dim=0) + + result = MultiKrum.aggregate(all_grads, n=n_workers, f=n_byzantine) + baseline = Average.aggregate(all_grads) + + print(f"MultiKrum: {result.norm():.4f}") + print(f"Average: {baseline.norm():.4f}") + +With a SignFlip attack, the Average gradient norm is much larger than +MultiKrum's because Average includes the flipped values directly, +while MultiKrum discards the outlier gradients before averaging. + +Next steps +---------- + +* :doc:`working_with_models`: how aggregators access flat gradient tensors + and the standard models bundled with Krum. +* :doc:`centralised_simulation_walkthrough`: run aggregators and attacks + inside a full training loop. +* :doc:`structured_experiments`: compare configurations systematically + with ``Orchestrator`` and ``Metric``. +* :doc:`implement_aggregator`: write your own aggregation rule. +* :doc:`implement_attack`: write your own Byzantine attack. +* :doc:`/reference/primitives/aggregators/index`: full API reference. +* :doc:`/reference/primitives/attacks/index`: full API reference. diff --git a/docs/tutorials/working_with_models.rst b/docs/tutorials/working_with_models.rst new file mode 100644 index 00000000..f9c4f5ac --- /dev/null +++ b/docs/tutorials/working_with_models.rst @@ -0,0 +1,169 @@ +Working with models +=================== + +**Problem:** Aggregators need flat 1-D gradient tensors, but copying +parameters every round is slow and wasteful. How do you efficiently +access and manipulate model parameters and gradients as flat tensors? + +This tutorial covers the ``Model`` wrapper (zero-copy flat tensor views +of PyTorch parameters and gradients) and the standard models bundled with +Krum. + +The Model wrapper +----------------- + +Krum's aggregators operate on flat gradient tensors. The +:class:`~krum.primitives.models.Model` class wraps any ``nn.Module`` and +provides zero-copy flat views: + +.. code-block:: python + + from krum.primitives.models import Model + import torch.nn as nn + + module = nn.Linear(10, 5) + model = Model(module) + + # Flat parameter view (zero-copy, lazy-initialized) + flat_params = model.parameters # shape: (55,) + + # Flat gradients after backward() + loss = module(torch.randn(3, 10)).sum() + loss.backward() + flat_grads = model.gradients # shape: (55,) + + # Write aggregated gradients back (zero-copy relink) + model.gradients = aggregated_flat + +The flat tensors are **views** into the original parameter and gradient +storage, so zero-copy applies to both and no memory is copied. + +How zero-copy works +------------------- + +Krum's aggregators need a flat ``(n, d)`` tensor of all worker gradients. +Naively, you would copy each parameter's data into a flat vector: + +.. code-block:: python + + flat = torch.nn.utils.parameters_to_vector(module.parameters()) + # flat is a *copy*: 80,000 new floats for a small MLP + +With 10 workers and a CNN, copying every gradient per round adds up +fast. ``Model`` avoids this by building a flat **view** that shares the +original storage: + +.. code-block:: python + + model = Model(module) + + # model.parameters is a view that shares the same memory + flat = model.parameters + flat[:] = 0 # zeros every parameter in module instantly + assert module.weight.count_nonzero() == 0 # the module sees it too + +Under the hood, ``Model._relink()`` replaces each parameter's ``.data`` +with a slice of a single contiguous buffer: + +.. code-block:: text + + Before: param_0.data ──→ [w0 w1 w2 ...] + param_1.data ──→ [b0 b1 ...] + + After: flat ──→ [w0 w1 w2 ... b0 b1 ...] ← one allocation + param_0.data ──→ ↑ slice + param_1.data ──→ ↑ slice + +Because every parameter points into the same storage, reading or writing +``flat`` propagates to the module, and vice versa, with zero +data movement. + +This matters for performance: for a typical CNN with 1.2 million +parameters, a flat view costs one allocation of 4.8 MB, created once, +rather than 4.8 MB of copying every aggregation round. + +The same mechanism applies to gradients: ``model.gradients`` gives a +flat view of every ``param.grad``, so an aggregator's output can be +written back without copying: + +.. code-block:: python + + model.gradients = aggregated_flat # all .grad tensors relinked in place + +The one caveat is that the view breaks when PyTorch replaces a +parameter's ``.data`` or ``.grad`` (e.g. after +``zero_grad(set_to_none=True)``). The relink pattern below restores it. + +The relink pattern +------------------ + +``zero_grad(set_to_none=True)`` (the default in recent PyTorch versions) +replaces each ``.grad`` with ``None``, breaking the cached flat gradient view. +After calling ``zero_grad()``, access ``.gradients`` via +``relink_gradients()`` to restore the link in a single call: + +.. code-block:: python + + optimizer.zero_grad() # drops .grad tensors + grads = model.relink_gradients() # re-link + get flat view + grads[:] = 0 # equivalent to zero_grad + +The same pattern applies to :meth:`~krum.primitives.models.Model.relink_parameters` +when a parameter's ``.data`` has been replaced externally. + +Both methods return the flat tensor directly, so no further property access is +needed. + +Standard models +--------------- + +Krum provides standard models from the Byzantine resilience literature: + +.. code-block:: python + + from krum.primitives.models.mlp import Krum2017MLPMnist, Krum2017MLPSpambase, Monna2023SmallMnist + from krum.primitives.models.cnn import Krum2017CNN, Monna2023CNNMnist, Monna2023CNNCifar10 + + # MLP for MNIST (784, 100, 10). Krum NIPS 2017. + mlp = Krum2017MLPMnist() + + # MLP for Spambase (57, 20, 20, 2). Krum NIPS 2017. + spambase = Krum2017MLPSpambase() + + # CNN for CIFAR-10 (3, 32, 32, 10). Hidden Vulnerability ICML 2018. + cnn = Krum2017CNN() + + # Small MLP for MNIST (784, 128, 10). MONNA ICML 2023. + small_mnist = Monna2023SmallMnist() + + # CNN for MNIST. MONNA ICML 2023. + cnn_mnist = Monna2023CNNMnist() + + # CNN for CIFAR-10. MONNA ICML 2023. + cnn_cifar = Monna2023CNNCifar10() + +These models can be wrapped with ``Model`` for zero-copy flat views: + +.. code-block:: python + + from krum.primitives.models import Model + from krum.primitives.models.mlp import Krum2017MLPMnist + + model = Model(Krum2017MLPMnist()) + flat_params = model.parameters # shape: (d,) where d ≈ 80,000 + +All models take no constructor arguments and instantiate fixed +architectures matching their respective papers. See +:doc:`/reference/primitives/models/index` for the full list with +architectural details. + +Next steps +---------- + +* :doc:`using_aggregators_attacks`: see how ``Model`` gradient views + connect to aggregation rules. +* :doc:`centralised_simulation_walkthrough`: use these models in a + full simulation. +* :doc:`implement_aggregator`: write an aggregation rule that operates + on the flat gradient tensors from a ``Model``. +* :doc:`/reference/primitives/models/index`: ``Model`` API reference. diff --git a/pyproject.toml b/pyproject.toml index 732ed041..4dbeefc4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,25 +31,28 @@ classifiers = [ ] # Runtime dependencies (pulled by pip install krum) dependencies = [ - "matplotlib>=3.10.9", - "numpy>=2.2.6", - "pandas>=2.0.0", - "torch>=2.11.0", - "torchvision>=0.26.0", + "pandas>=2.2.0", + "torch>=2.13.0", + "torchvision>=0.28.0", ] -# Development and documentation dependencies (pip install krum[dev]) +# Extra dependencies for running experiments (pip install krum[experiments]) [project.optional-dependencies] +experiments = [ + "matplotlib>=3.10.0", + "numpy>=2.0.0", + "seaborn>=0.13.0", +] dev = [ "pre-commit>=4.0.0", - "ruff>=0.15.16", - "ty>=0.0.44", + "ruff>=0.15.22", + "ty>=0.0.61", "sphinx>=8.1.3", "sphinx-copybutton>=0.5.2", "sphinx-favicon>=1.0.1", "sphinx-togglebutton>=0.3.2", "sphinx-contributors>=0.3.0", - "shibuya>=2026.1.9", + "shibuya>=2026.7.12", ] [project.urls] diff --git a/uv.lock b/uv.lock index 492b1c3b..590a4220 100644 --- a/uv.lock +++ b/uv.lock @@ -385,45 +385,51 @@ wheels = [ [[package]] name = "cuda-toolkit" -version = "13.0.2" +version = "13.0.3.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, ] [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufft = [ - { name = "nvidia-cufft", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cufft", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] curand = [ - { name = "nvidia-curand", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-curand", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, + { name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and platform_machine == 'AMD64' and sys_platform == 'win32') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] [[package]] @@ -726,9 +732,6 @@ name = "krum" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "matplotlib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "torch" }, @@ -747,6 +750,12 @@ dev = [ { name = "sphinx-togglebutton" }, { name = "ty" }, ] +experiments = [ + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "seaborn" }, +] [package.dev-dependencies] dev = [ @@ -757,22 +766,23 @@ dev = [ [package.metadata] requires-dist = [ - { name = "matplotlib", specifier = ">=3.10.9" }, - { name = "numpy", specifier = ">=2.2.6" }, - { name = "pandas", specifier = ">=2.0.0" }, + { name = "matplotlib", marker = "extra == 'experiments'", specifier = ">=3.10.0" }, + { name = "numpy", marker = "extra == 'experiments'", specifier = ">=2.0.0" }, + { name = "pandas", specifier = ">=2.2.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.16" }, - { name = "shibuya", marker = "extra == 'dev'", specifier = ">=2026.1.9" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.22" }, + { name = "seaborn", marker = "extra == 'experiments'", specifier = ">=0.13.0" }, + { name = "shibuya", marker = "extra == 'dev'", specifier = ">=2026.7.12" }, { name = "sphinx", marker = "extra == 'dev'", specifier = ">=8.1.3" }, { name = "sphinx-contributors", marker = "extra == 'dev'", specifier = ">=0.3.0" }, { name = "sphinx-copybutton", marker = "extra == 'dev'", specifier = ">=0.5.2" }, { name = "sphinx-favicon", marker = "extra == 'dev'", specifier = ">=1.0.1" }, { name = "sphinx-togglebutton", marker = "extra == 'dev'", specifier = ">=0.3.2" }, - { name = "torch", specifier = ">=2.11.0" }, - { name = "torchvision", specifier = ">=0.26.0" }, - { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.44" }, + { name = "torch", specifier = ">=2.13.0" }, + { name = "torchvision", specifier = ">=0.28.0" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.61" }, ] -provides-extras = ["dev"] +provides-extras = ["experiments", "dev"] [package.metadata.requires-dev] dev = [ @@ -1147,11 +1157,14 @@ wheels = [ [[package]] name = "nvidia-cublas" -version = "13.1.0.3" +version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, +] wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, - { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, ] [[package]] @@ -1183,14 +1196,14 @@ wheels = [ [[package]] name = "nvidia-cudnn-cu13" -version = "9.19.0.56" +version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cublas", marker = "(python_full_version < '3.11' and sys_platform == 'emscripten') or (python_full_version < '3.11' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, - { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, ] [[package]] @@ -1251,20 +1264,20 @@ wheels = [ [[package]] name = "nvidia-cusparselt-cu13" -version = "0.8.0" +version = "0.8.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, - { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, ] [[package]] name = "nvidia-nccl-cu13" -version = "2.28.9" +version = "2.29.7" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, ] [[package]] @@ -1733,27 +1746,43 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, - { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, - { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, - { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, - { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, - { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, - { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, - { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, - { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, - { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, - { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, ] [[package]] @@ -1767,15 +1796,15 @@ wheels = [ [[package]] name = "shibuya" -version = "2026.1.9" +version = "2026.7.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments-styles" }, { name = "sphinx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/b94cb04adbb984973fe83fd670dd066514610241d829723f678366e691d2/shibuya-2026.1.9.tar.gz", hash = "sha256:b389f10fd9c07b048e940f32d1e1ac096a2d49736389173ac771b37a10b51fdf", size = 86002, upload-time = "2026-01-09T02:19:14.365Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/d2/8017988e02791162b286576116fedd200ed33e8d8af3579ab3587df3d3d0/shibuya-2026.7.12.tar.gz", hash = "sha256:9496606f5b95595511ad86b773d74b4b8d694a4dcc2f848e6ec34e2ad71d9060", size = 86545, upload-time = "2026-07-11T15:41:16.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/ae/06d7dfc5633c7250fefc61fd624990aa2c37e3495c08a2f23968b1acb23e/shibuya-2026.1.9-py3-none-any.whl", hash = "sha256:b58a3cc6e5619c71d00fcf0be4a3060c87040c2a62a1b3f1a93a6a41ca8eaf45", size = 103389, upload-time = "2026-01-09T02:19:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/84/70/9ca2b8187666be1bcfa82b938107247159f5b566a22b5e43be27df45039d/shibuya-2026.7.12-py3-none-any.whl", hash = "sha256:0a2c0c109e7cc47fc650b01feb671d267c0ffe2e39c98f3529e32d723a9c9d9b", size = 104092, upload-time = "2026-07-11T15:41:14.936Z" }, ] [[package]] @@ -2061,7 +2090,7 @@ wheels = [ [[package]] name = "torch" -version = "2.11.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, @@ -2081,39 +2110,35 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/f2/c1690994afe461aae2d0cac62251e6802a703dec0a6c549c02ecd0de92a9/torch-2.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2c0d7fcfbc0c4e8bb5ebc3907cbc0c6a0da1b8f82b1fc6e14e914fa0b9baf74e", size = 80526521, upload-time = "2026-03-23T18:12:06.86Z" }, - { url = "https://files.pythonhosted.org/packages/a4/f0/98ae802fa8c09d3149b0c8690741f3f5753c90e779bd28c9613257295945/torch-2.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4cf8687f4aec3900f748d553483ef40e0ac38411c3c48d0a86a438f6d7a99b18", size = 419723025, upload-time = "2026-03-23T18:11:43.774Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/18a9b10b4bd34f12d4e561c52b0ae7158707b8193c6cfc0aad2b48167090/torch-2.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1b32ceda909818a03b112006709b02be1877240c31750a8d9c6b7bf5f2d8a6e5", size = 530589207, upload-time = "2026-03-23T18:11:23.756Z" }, - { url = "https://files.pythonhosted.org/packages/35/40/2d532e8c0e23705be9d1debce5bc37b68d59a39bda7584c26fe9668076fe/torch-2.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3c712ae6fb8e7a949051a953fc412fe0a6940337336c3b6f905e905dac5157f", size = 114518313, upload-time = "2026-03-23T18:11:58.281Z" }, - { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, - { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, - { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, - { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, - { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, - { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, - { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, - { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, - { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, - { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, - { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, - { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, - { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, - { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e7/19894fdb51c7dbaf94f5a79bb0871da0992e8e4241e579cb006da46d2e58/torch-2.13.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:94f0de129916f77b8dc2c7a8eff644cfeddfe59e39c9f55e9f6e17543410281d", size = 111178962, upload-time = "2026-07-08T16:05:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/d1/5c/b1d5de470c54e339b30a92d96683a71bcebd78f5f2a7fc714cd6dc6bbd68/torch-2.13.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0ab4b69f3ee03a62a002cfbf77b1ca5e88aceb4ea64cb4388bb28f638ddbb045", size = 427198333, upload-time = "2026-07-08T16:05:36.847Z" }, + { url = "https://files.pythonhosted.org/packages/50/c0/68a84105e1fcb8970144b388ff3d3e5dc15a3be28c1e247841f7d7247e41/torch-2.13.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:c78b7b4d04461855a764cf01bae9a462bb88bc93defcfa11235cbc8fdf3e12c4", size = 526555154, upload-time = "2026-07-08T16:05:06.507Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c9/0bb9d097b03cbaf96bb75b15e867347b8e41bfcdfe0539452d17d9e63993/torch-2.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:2bd30b6b730d987fa386ce3898933762c5cb8cc82eb0535211d787cc3ce2dfeb", size = 122015602, upload-time = "2026-07-08T16:05:45.25Z" }, + { url = "https://files.pythonhosted.org/packages/5b/fe/cba54dc58523434919b66f13a667e36e436deddd77ca519e96553617d4ec/torch-2.13.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:e76f9bcecc52b8ff711239a2f7547d5353df95878ab232f0773c1d95928b92f8", size = 111187938, upload-time = "2026-07-08T16:05:17.065Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/1e3160e18e12aa3038390efab3ce02b36a9d4d6a527ecdd8520dca2e68d8/torch-2.13.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:092790c696a760c729fd5722835f50b9d81fd7c8f141571f3f3cf4081a8f664c", size = 427199369, upload-time = "2026-07-08T16:04:51.054Z" }, + { url = "https://files.pythonhosted.org/packages/01/79/1f2d34ad7034ee1c7ffc1cf8bf0f8213af2a81df6ecdb3997ecec107c09d/torch-2.13.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:60fcdcb2f3876e21146cb4524ef06397d727ca9ad5f020818547e25075fe3cb7", size = 526574961, upload-time = "2026-07-08T16:04:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fd/0f2ce40f58aefbdb3392f9acce3c8171940943ae2d661f70558bfa73befb/torch-2.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a0d8b11f16a48d60e2015d8213aa0390744cbebb98e58b62b3514dddc656e330", size = 122015870, upload-time = "2026-07-08T16:05:27.59Z" }, + { 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/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, + { 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/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, + { 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/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, ] [[package]] name = "torchvision" -version = "0.26.0" +version = "0.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -2122,80 +2147,74 @@ dependencies = [ { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/74/b4/cdfee31e0402ea035135462cb0ab496e974d56fab6b4e7a1f0cbccb8cd28/torchvision-0.26.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a06d4772a8e13e772906ed736cc53ec6639e5e60554f8e5fa6ca165aabebc464", size = 1863503, upload-time = "2026-03-23T18:13:01.384Z" }, - { url = "https://files.pythonhosted.org/packages/e4/74/11fee109841e80ad14e5ca2d80bff6b10eb11b7838ff06f35bfeaa9f7251/torchvision-0.26.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2adfbe438473236191ff077a4a9a0c767436879c89628aa97137e959b0c11a94", size = 7766423, upload-time = "2026-03-23T18:12:56.049Z" }, - { url = "https://files.pythonhosted.org/packages/5e/00/24d8c7845c3f270153fb81395a5135b2778e2538e81d14c6aea5106c689c/torchvision-0.26.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b6f9ad1ecc0eab52647298b379ee9426845f8903703e6127973f8f3d049a798b", size = 7518249, upload-time = "2026-03-23T18:12:51.743Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ed/e53cd7c0da7ae002e5e929c1796ebbe7ec0c700c29f7a0a6696497fb3d8b/torchvision-0.26.0-cp310-cp310-win_amd64.whl", hash = "sha256:f13f12b3791a266de2d599cb8162925261622a037d87fc03132848343cf68f75", size = 3669784, upload-time = "2026-03-23T18:12:49.949Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, - { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, - { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a9/c272623a0f735c35f0f6cd6dc74784d4f970e800cf063bb76687895a2ab9/torchvision-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7993c01648e7c61d191b018e84d38fe0825c8fcb2720cd0f37caf7ba14404aa1", size = 4255155, upload-time = "2026-03-23T18:12:32.652Z" }, - { url = "https://files.pythonhosted.org/packages/da/80/0762f77f53605d10c9477be39bb47722cc8e383bbbc2531471ce0e396c07/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", size = 1860809, upload-time = "2026-03-23T18:12:47.629Z" }, - { url = "https://files.pythonhosted.org/packages/e6/81/0b3e58d1478c660a5af4268713486b2df7203f35abd9195fea87348a5178/torchvision-0.26.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a39c7a26538c41fda453f9a9692b5ff9b35a5437db1d94f3027f6f509c160eac", size = 7727494, upload-time = "2026-03-23T18:12:46.062Z" }, - { url = "https://files.pythonhosted.org/packages/b6/dc/d9ab5d29115aa05e12e30f1397a3eeae1d88a511241dc3bce48dc4342675/torchvision-0.26.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7e6213620bbf97742e5f79832f9e9d769e6cf0f744c5b53dad80b76db633691", size = 7521747, upload-time = "2026-03-23T18:12:36.815Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1b/f1bc86a918c5f6feab1eeff11982e2060f4704332e96185463d27855bdf5/torchvision-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:4280c35ec8cba1fcc8294fb87e136924708726864c379e4c54494797d86bc474", size = 4319880, upload-time = "2026-03-23T18:12:38.168Z" }, - { url = "https://files.pythonhosted.org/packages/66/28/b4ad0a723ed95b003454caffcc41894b34bd8379df340848cae2c33871de/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", size = 1951973, upload-time = "2026-03-23T18:12:48.781Z" }, - { url = "https://files.pythonhosted.org/packages/71/e2/7a89096e6cf2f3336353b5338ba925e0addf9d8601920340e6bdf47e8eb3/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3daf9cc149cf3cdcbd4df9c59dae69ffca86c6823250442c3bbfd63fc2e26c61", size = 7728679, upload-time = "2026-03-23T18:12:26.196Z" }, - { url = "https://files.pythonhosted.org/packages/69/1d/4e1eebc17d18ce080a11dcf3df3f8f717f0efdfa00983f06e8ba79259f61/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82c3965eca27e86a316e31e4c3e5a16d353e0bcbe0ef8efa2e66502c54493c4b", size = 7609138, upload-time = "2026-03-23T18:12:35.327Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a4/f1155e943ae5b32400d7000adc81c79bb0392b16ceb33bcf13e02e48cced/torchvision-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ebc043cc5a4f0bf22e7680806dbba37ffb19e70f6953bbb44ed1a90aeb5c9bea", size = 4248202, upload-time = "2026-03-23T18:12:41.423Z" }, - { url = "https://files.pythonhosted.org/packages/7f/c8/9bffa9c7f7bdf95b2a0a2dc535c290b9f1cc580c3fb3033ab1246ffffdeb/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3", size = 1860813, upload-time = "2026-03-23T18:12:39.636Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ac/48f28ffd227991f2e14f4392dde7e8dc14352bb9428c1ef4a4bbf5f7ed85/torchvision-0.26.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9a904f2131cbfadab4df828088a9f66291ad33f49ff853872aed1f86848ef776", size = 7727777, upload-time = "2026-03-23T18:12:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/a4/21/a2266f7f1b0e58e624ff15fd6f01041f59182c49551ece0db9a183071329/torchvision-0.26.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0f3e572efe62ad645017ea847e0b5e4f2f638d4e39f05bc011d1eb9ac68d4806", size = 7522174, upload-time = "2026-03-23T18:12:29.565Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ba/1666f90bc0bdd77aaa11dcc42bb9f621a9c3668819c32430452e3d404730/torchvision-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:114bec0c0e98aa4ba446f63e2fe7a2cbca37b39ac933987ee4804f65de121800", size = 4348469, upload-time = "2026-03-23T18:12:24.44Z" }, - { url = "https://files.pythonhosted.org/packages/45/8f/1f0402ac55c2ae15651ff831957d083fe70b2d12282e72612a30ba601512/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da", size = 1860826, upload-time = "2026-03-23T18:12:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6a/18a582fe3c5ee26f49b5c9fb21ad8016b4d1c06d10178894a58653946fda/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7058c5878262937e876f20c25867b33724586aa4499e2853b2d52b99a5e51953", size = 7729089, upload-time = "2026-03-23T18:12:31.394Z" }, - { url = "https://files.pythonhosted.org/packages/c5/9b/f7e119b59499edc00c55c03adc9ec3bd96144d9b81c46852c431f9c64a9a/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8008474855623c6ba52876589dc52df0aa66e518c25eca841445348e5f79844c", size = 7522704, upload-time = "2026-03-23T18:12:20.301Z" }, - { url = "https://files.pythonhosted.org/packages/d0/6a/09f3844c10643f6c0de5d95abc863420cfaf194c88c7dffd0ac523e2015f/torchvision-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e9d0e022c19a78552fb055d0414d47fecb4a649309b9968573daea160ba6869c", size = 4454275, upload-time = "2026-03-23T18:12:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/b4/df/1ba039ad6cfe6e69209c36766b9b6e8c6fe92481c6d4e4ca52296f5f699d/torchvision-0.28.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:2a1ef4b6f4bf5828b48cfad97372c8982db906830884b2868ba5c3df937a7d81", size = 1856019, upload-time = "2026-07-08T16:07:59.283Z" }, + { url = "https://files.pythonhosted.org/packages/88/ea/5c70ecf86f8e95174a85061cea78683a7bb7f422f09c3f3d4f30b7600fa9/torchvision-0.28.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:546fd85345cf8652f6cd099d4f9884b0ca5c2f3fae78689a21dd2f35ea6b622f", size = 7838211, upload-time = "2026-07-08T16:07:27.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/2f7ff1997d793e45d85fafa8374ee25348b7dae9ac521ba8751d7e1c75d5/torchvision-0.28.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6dfb0f45e2b4ceb4e76f158c3fbb5f44387099f3c466e3423a09ab665a194aba", size = 7669419, upload-time = "2026-07-08T16:07:41.648Z" }, + { url = "https://files.pythonhosted.org/packages/42/d0/2b3c30834ff23acd3854d0ff59bc580711f4b36d725de40105a852ed3719/torchvision-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:7fad44dc9582570c7d92c4487d36ac46998f40cc39b438e8b8f5111a935ce4e8", size = 3500355, upload-time = "2026-07-08T16:07:56.865Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b2/1e010052079e4c577007b789db336ea7075f1a426e84d17121fbc3745516/torchvision-0.28.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:83fe6c020866a85acd7d97deccc45ff11d66daf42916d04396a4309c66c0ccb8", size = 1856017, upload-time = "2026-07-08T16:07:55.533Z" }, + { url = "https://files.pythonhosted.org/packages/27/be/1b9c5de9c655ca2df4a74100fa671a7b848532ff787e077ccde14a7dea2a/torchvision-0.28.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5a38bc6da3d72621be003400b66f66a2b4c6d644fde05f680c2cb7ca8cf8dd6c", size = 7841822, upload-time = "2026-07-08T16:07:49.207Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9b/f1e68e861d4462e3e195a642c2b448e7b7d3fad5f209487162b9a2133d9b/torchvision-0.28.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7e80f543b22503d9415e126db5f0ff3917036925e38560ee6b9ae38c571a4002", size = 7670718, upload-time = "2026-07-08T16:07:46.525Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/1494610ff54cbb154beb55033cc2cd50f3de04dac132fa2dd00e4f2b2556/torchvision-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:9a45ea67235d965ef52187130d20002a4de20c54ea3d927a24286961d268dc37", size = 3814319, upload-time = "2026-07-08T16:07:37.153Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/c1cab1ecbb3ff1a380a3f99283db1dee61b8afe354f6352c643b65937130/torchvision-0.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee", size = 1856020, upload-time = "2026-07-08T16:07:52.182Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/95233776e2def960e5abb7a07931230a545f43717a56a1e1140162033598/torchvision-0.28.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a", size = 7842127, upload-time = "2026-07-08T16:07:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/93/e4/e9b2495d0d57b9f60d63c57d0a910410a81b4b073bf70917bef815291119/torchvision-0.28.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940", size = 7675040, upload-time = "2026-07-08T16:07:58.017Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9c/55ed9cb6dfe3ee9c837df5cd0e758372e5829aa38b8dd71343aa632cc4e2/torchvision-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d", size = 4085785, upload-time = "2026-07-08T16:07:50.928Z" }, + { url = "https://files.pythonhosted.org/packages/20/55/08a726c14c67b37c8aca04b077766909f1c7ed23f76116884fe63b9bd033/torchvision-0.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123", size = 1856021, upload-time = "2026-07-08T16:07:45.386Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/40beacd53809194f5259e590d1afaeaa8ad57da15f77c646e6560bcc4616/torchvision-0.28.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf", size = 7797014, upload-time = "2026-07-08T16:07:43.04Z" }, + { url = "https://files.pythonhosted.org/packages/32/db/062cdb5a84380a60439775311fff34d89229760d2a50680393dc18699956/torchvision-0.28.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237", size = 7674669, upload-time = "2026-07-08T16:07:38.91Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a6/b4081e2d04e1541abf82785ac9e5178a494c19330391f551356c8c18b7b3/torchvision-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b", size = 4157380, upload-time = "2026-07-08T16:07:40.22Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b9/da40eca5bbe9596c12ae9899ab7abaf887f5e20f29d08b924b4633714821/torchvision-0.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b", size = 1856014, upload-time = "2026-07-08T16:07:44.282Z" }, + { url = "https://files.pythonhosted.org/packages/06/d6/313aafd3df4eaf5f330211bd4e75b7598bddbfee4f55580d3b58536e1b20/torchvision-0.28.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5", size = 7796873, upload-time = "2026-07-08T16:07:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/41/31f8e959ab8f942600b6357f8999c21d779d5fd3304b0fd204ff4b518239/torchvision-0.28.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd", size = 7674634, upload-time = "2026-07-08T16:07:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/15/15/4c5115253fd470672cdac0a1cf139e06b4f3e29d041238a2b255937f63be/torchvision-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769", size = 4184005, upload-time = "2026-07-08T16:07:35.805Z" }, + { url = "https://files.pythonhosted.org/packages/6a/80/822a6163da716f8a78141cf6678d74e26a572285d4ea866ef8aa657bb307/torchvision-0.28.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49", size = 1856011, upload-time = "2026-07-08T16:07:33.404Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d1/cd3f9463b39a790ec8c0c2f6e6c8061edb1562114d04fcdfa786ed889345/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542", size = 7796742, upload-time = "2026-07-08T16:07:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/3e0a7ad18e99831e2d7f4713d3be717b7159ff5a920862dd5c23c454aa71/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204", size = 7675526, upload-time = "2026-07-08T16:07:34.572Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/23aea03b28297bc66a4461f55ae4296368a9d85fa9a454bafcb2a5348bd7/torchvision-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47", size = 4291452, upload-time = "2026-07-08T16:07:32.236Z" }, ] [[package]] name = "triton" -version = "3.6.0" +version = "3.7.1" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/ba/b1b04f4b291a3205d95ebd24465de0e5bf010a2df27a4e58a9b5f039d8f2/triton-3.6.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c723cfb12f6842a0ae94ac307dba7e7a44741d720a40cf0e270ed4a4e3be781", size = 175972180, upload-time = "2026-01-20T16:15:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, - { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, - { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, - { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, - { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, - { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, - { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, - { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, - { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, - { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ea/629cc37436ca5df93ce98956d09cd2ca1498bfee8ef4972d2fe48b9f958c/triton-3.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3daf64305d6cea88d3334c65ebc9bcd0c64c9564a977084366aa768d57cbcf64", size = 184551013, upload-time = "2026-06-17T20:03:37.551Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/c79c34311625227a288df3e483fc5cdf3d596624cbd4b4758c4cbdc14af3/triton-3.7.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee89fbf782ec2ad50391dd1cf26cbea4f4467154c37f4773026da8fc31c0f58e", size = 197596267, upload-time = "2026-06-17T19:53:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, ] [[package]] name = "ty" -version = "0.0.44" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/f4/fbb120226e4f239652525a664bad976a23fea58c646d1323f2296fee8a61/ty-0.0.44.tar.gz", hash = "sha256:5886229830ab77022842a1c55d2ef57405621a91fc465969fa6d538661898173", size = 5803665, upload-time = "2026-06-05T03:33:48.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/c6/b5b8c4762efb4d85401652658786506867553ecfc2beac3bcf361a15937f/ty-0.0.44-py3-none-linux_armv6l.whl", hash = "sha256:272d31e7ad49b1dc5e8465a9fe700354e14c755b40d9c75f08f031d786903df3", size = 11607267, upload-time = "2026-06-05T03:33:27.154Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5c/f4b405570737f44ab0fc4214117fe43353f8f0825a1823d9e99e9c8e57be/ty-0.0.44-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b92c4ddd7a3daf2049715edec9dc70cf6fd31a5a318ee647258f90dd75495eed", size = 11382826, upload-time = "2026-06-05T03:33:54.374Z" }, - { url = "https://files.pythonhosted.org/packages/9d/aa/fb9835aa492b148d7754cb4c3db07f31a7e2e09f0d8e0e8e297f01125dd2/ty-0.0.44-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4d42cfd84a690f6654b2a4f0515027c21b692cf2512d32e6433f754893a95609", size = 10809741, upload-time = "2026-06-05T03:33:33.22Z" }, - { url = "https://files.pythonhosted.org/packages/47/f5/0b20ba6b66837a5a37bab7f74ac0732c66e766b5f0b2d55b30816b15f348/ty-0.0.44-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc47ae87e4cb7db2a9166bb23b78a905c3626e523296ec5bccf36b5e89bda6b", size = 11318153, upload-time = "2026-06-05T03:34:09.403Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bb/b82ea730774a4f950f06d355fbc120d51eac7da23b57fc79ef6ff7c79cbb/ty-0.0.44-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46d867e80f16f421ac72c9a85240dbf050d62d9b3fbd10a8b5b082fb21679e0b", size = 11403108, upload-time = "2026-06-05T03:33:57.745Z" }, - { url = "https://files.pythonhosted.org/packages/8b/41/e2c83856165291049c702eda4e2ef3d3ebd875e8a0a77b8cc4ef3156aa1c/ty-0.0.44-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:411f5de0f96a4e4e5cccc3e0d55954c41f6a99ee6ca1fe5a7226cbc68406e053", size = 11944815, upload-time = "2026-06-05T03:34:15.793Z" }, - { url = "https://files.pythonhosted.org/packages/66/95/1fa6a101eb9d5bec042b87e5ca9c8fc349b75961beca6306f95af5cd5539/ty-0.0.44-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b15f01ecb4e2b46c05a1769293f9d32c3d4a1e4e7dfccf37c604d705dc3e3f4", size = 12476121, upload-time = "2026-06-05T03:33:51.529Z" }, - { url = "https://files.pythonhosted.org/packages/72/6a/da4b45b1229d39207c6140681c2aaf4f5691bcb1dc830b84450ca25c8f57/ty-0.0.44-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edd32b7467af509c99c0244c2226a4e4c03400699003ec33373282ab931654d9", size = 12091340, upload-time = "2026-06-05T03:33:36.289Z" }, - { url = "https://files.pythonhosted.org/packages/16/c7/e1c9260ea5188195962ff1214ace418b5d69187e8fa7c0a1ec4994b8071b/ty-0.0.44-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:503a585f4007387c3afc58bae23a7ca1b9f236cbdb1a881dc36110655ceb1937", size = 11986201, upload-time = "2026-06-05T03:34:00.624Z" }, - { url = "https://files.pythonhosted.org/packages/92/f9/312bb112da9b1a7da295bb0426be85e72ad48da4e4266c36d77256b4058d/ty-0.0.44-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d28bcfa83243d77c2316944e8cf197f73597bf17d1ddc047d0b10a762531252", size = 12168475, upload-time = "2026-06-05T03:33:30.386Z" }, - { url = "https://files.pythonhosted.org/packages/02/de/64978d603f6c3e5dd7cb97eca2214567d8ad0c85fa4a7435b7852ae4b779/ty-0.0.44-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:56fd2dd0192def189715b25f5338f6222fb827884dc34111e50aa1c4e061cee5", size = 11292937, upload-time = "2026-06-05T03:34:06.448Z" }, - { url = "https://files.pythonhosted.org/packages/64/63/a625d8a3c71dcaa01988d330f849c465fe72ead4b0bbab44fe4bd6e672b5/ty-0.0.44-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7f8d990489032de1984e73c159f3e760d754cf83a602b874827d943821f63595", size = 11421560, upload-time = "2026-06-05T03:33:23.995Z" }, - { url = "https://files.pythonhosted.org/packages/99/96/61aeba0e629b0c91bd316ff94d00e38817ec493ae4316f39508988daa287/ty-0.0.44-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f61ffe72996a755432922fe90b28db593f572eb5cbf48e3ef4e67b282533d1b0", size = 11580282, upload-time = "2026-06-05T03:34:03.308Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f7/256e1538ce21cab67b381201444c42454de69d310059c4929d92a0ee9c48/ty-0.0.44-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2b237a143bac4f30cec9257d45f01e72da97030a80a09a2b69cfef065f09c37f", size = 12085723, upload-time = "2026-06-05T03:33:45.953Z" }, - { url = "https://files.pythonhosted.org/packages/d3/76/ec3957c10872643a98db7a7895101ad89c5b7cba4bc6c4aebbbfc91756cc/ty-0.0.44-py3-none-win32.whl", hash = "sha256:6a24586c65419223ac5bab4822d49ab493a5d19ea2a897514284c232b9d6166a", size = 10892978, upload-time = "2026-06-05T03:34:12.603Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7d/ba24050432196e7d7f03945e5c379951593c48e04e5c5d5275cfc4624791/ty-0.0.44-py3-none-win_amd64.whl", hash = "sha256:8cccb27e348c89a9733fbad1b2efadfbad79b107c7e52adb52dfd8a70156a38d", size = 11987058, upload-time = "2026-06-05T03:33:42.692Z" }, - { url = "https://files.pythonhosted.org/packages/71/34/16ec3f1fec75292d9c56a8b5fef037ceaba85a5c30562206c1a245a00a67/ty-0.0.44-py3-none-win_arm64.whl", hash = "sha256:58049504e7a12bf1957f24a5384a332c94d5590127083a80db5e5a1bed34190b", size = 11329961, upload-time = "2026-06-05T03:33:39.427Z" }, +version = "0.0.61" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/63/6944925d0fe9a4bb9cc744e6c045a42bbd2ee4654c103190674577a36c3f/ty-0.0.61.tar.gz", hash = "sha256:acbf0d914cc7e2e57ccc440036af36114819e2a604a5ffb554e72e4ca7dd65a2", size = 6234957, upload-time = "2026-07-18T01:39:54.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/cf/044f31523e2768e3e64b0ca2ec32f70b3a731d4a2caa6ea110baf26e251c/ty-0.0.61-py3-none-linux_armv6l.whl", hash = "sha256:148779b8675eac93f40ec58bd70037fe67537117f20a23272264f8f136d41336", size = 11891448, upload-time = "2026-07-18T01:39:18.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/55/558cfe76b65d91d1854bbfac336020bd42fd887caa632d845d13c0c539eb/ty-0.0.61-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:08217382b3385808ee7288501ea3214b32631b08d1fd091ece6799b0c95264c5", size = 11602442, upload-time = "2026-07-18T01:39:20.914Z" }, + { url = "https://files.pythonhosted.org/packages/27/be/78c0ae6634cd606a68e5b46b338db427a48a1800c96a749b2d2f7a702e03/ty-0.0.61-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d99c729011b47dec20e78a32ac9c8f6defd4cf62f7bb851bbccf70dde6cee50", size = 11125286, upload-time = "2026-07-18T01:39:22.893Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/a40793962f1b6337938ddb0bca7496b54e70879e23b4d2cc8dfd7e5d1af3/ty-0.0.61-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cda607978ae271b77e51c947663218bce635c3507e256865444b10c37cdb60d", size = 11663403, upload-time = "2026-07-18T01:39:25.017Z" }, + { url = "https://files.pythonhosted.org/packages/98/c1/7879244da5b30407dc368946d36be5024380073408b079f144ffe034030e/ty-0.0.61-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d78f160a0f9434d570cdcdbc4dafba1f6aac3c47a32f9f63995b3cb55ffe4b6", size = 11715250, upload-time = "2026-07-18T01:39:27.045Z" }, + { url = "https://files.pythonhosted.org/packages/35/c4/8a4637cd58abd37f315dd515e24c582986cb1bfdf2edc4786882f5a4f69a/ty-0.0.61-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09aeab4800b36e93e4ce918699004da642d74988cac920b7592a6a2b9be6611c", size = 12393876, upload-time = "2026-07-18T01:39:29.197Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/27e7c640b1272743503229aa17ae2167a538040c4716a2fa1777c2b34fea/ty-0.0.61-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dccc8136df44142a109953a168be17b4915c99876b047d0b6672c31dae939bdf", size = 12958187, upload-time = "2026-07-18T01:39:31.308Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f5/70eaaefb6081fb0a8115cff66fbfaa20dafac8c646df2477adad95a59de2/ty-0.0.61-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:220760c2d13a887d027ee1093172c24ac35b6e634805329c93a30908ae4d3f5c", size = 12560101, upload-time = "2026-07-18T01:39:33.35Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/17bae3b6429b5c479dc6c1e344d34e1f79efbc27531f15f3ee5b5da63745/ty-0.0.61-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:effefbb89da7128d18059529d1c2ea390fe7f1f3882690d257ca2143d49a0c34", size = 12225389, upload-time = "2026-07-18T01:39:35.436Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/2ac380ba20d6395542c8df1d6fa4f00e2aead784c2e6aaefa1e02ed0610c/ty-0.0.61-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ba8b28a5ef811d5bb6461e37d76110c06fd20487474865c323d3d18b08b972b2", size = 12548403, upload-time = "2026-07-18T01:39:37.556Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e5/7da4b73e825e1a9808c26d68b0156e9a37aede1846191210dfffb8c64042/ty-0.0.61-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:88ecd6d9b05e8174b1860dac9bd3e188d6cef5702b0d3239fd9f94f6ac73a29d", size = 11621813, upload-time = "2026-07-18T01:39:39.919Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/5b58015e998cd0d89b17a463b6321421457d86d987574e8dac65ddfceba3/ty-0.0.61-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb0cdfe4c48542ffb9a1139825dfa3d4aae49e96e966682ef7da762ab97831ff", size = 11734101, upload-time = "2026-07-18T01:39:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/21/294f4cc819b7b12ed659fd860e5cdfbd592d4c768c8f23596685dbc43e6b/ty-0.0.61-py3-none-musllinux_1_2_i686.whl", hash = "sha256:dff03873c0c3d0b44738f8b6d403b0756a31cf54c65136397df7624c6159b1f0", size = 11988401, upload-time = "2026-07-18T01:39:44.183Z" }, + { url = "https://files.pythonhosted.org/packages/2e/26/0f96f79fdac118521a9771e9eef3f9b3f447d647b2c77953e80a1715c7e8/ty-0.0.61-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a9210e80e3d41c1dfc751e9e8e0980272f475031fafd0fb0f48aee233c78da03", size = 12330624, upload-time = "2026-07-18T01:39:46.662Z" }, + { url = "https://files.pythonhosted.org/packages/e3/08/1e62d1bca5c0cebdc7a34db1f4b61557aab85961cedd56953dd2c32d3e66/ty-0.0.61-py3-none-win32.whl", hash = "sha256:e3e1fe06f49a5492a922a5df2739834aa5ee978c7dd10414119dc8755cc40c9c", size = 11313991, upload-time = "2026-07-18T01:39:48.761Z" }, + { url = "https://files.pythonhosted.org/packages/26/f1/d8e33b3aeb36b73d81ae34d10e46ec4abf506d68f4e0a1491a76a593dd42/ty-0.0.61-py3-none-win_amd64.whl", hash = "sha256:25f2291169e0298fcdbba1b1fea64f8207a6c1908dddef32346fd5e3e6ac9221", size = 12311717, upload-time = "2026-07-18T01:39:50.881Z" }, + { url = "https://files.pythonhosted.org/packages/e1/14/7caec26d93a943c0e7d15eb7374644508d08cbd387d112b722b12d14e044/ty-0.0.61-py3-none-win_arm64.whl", hash = "sha256:3e496f7698bc4b5bbb1eb66d8b5799ba87596d88d36604ca359083893fa2fc49", size = 11693485, upload-time = "2026-07-18T01:39:52.73Z" }, ] [[package]]