diff --git a/pyproject.toml b/pyproject.toml index 77b241399..21cbe1a8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ backend = [ "bitsandbytes>=0.45.2", "unsloth==2026.3.3", "unsloth-zoo==2026.3.1", - "torch==2.10.0", + "torch>=2.11.0", "torchao==0.16.0", "accelerate==1.7.0", "awscli>=1.38.1", @@ -43,8 +43,10 @@ backend = [ ] megatron = [ "numpy<2", - "torch==2.10.0", - "quack-kernels==0.2.5", + "torch>=2.11.0", + "flash-attn-4==4.0.0b5", + "ninja>=1.11.1", + "quack-kernels==0.3.7", "apex", "transformer-engine==2.11.0", "transformer-engine-cu12==2.11.0", @@ -53,6 +55,7 @@ megatron = [ "pybind11>=2.13.6", "megatron-bridge==0.4.0rc0", "deep-ep==1.2.1 ; sys_platform == 'linux'", + "tilelang==0.1.10 ; sys_platform == 'linux' and platform_machine == 'x86_64'", "causal-conv1d==1.6.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_full_version < '3.12'", "mamba-ssm==2.3.1 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_full_version < '3.12'", "nvidia-ml-py==13.580.82", @@ -76,7 +79,7 @@ tinker = [ "protobuf>=6.31.1", "tinker-cookbook>=0.4.1,<0.5", "tinker>=0.21.0,<0.22", - "torch==2.10.0", + "torch>=2.11.0", "transformers==5.2.0", "uvicorn>=0.35.0", "datrie>=0.8.3", @@ -152,17 +155,19 @@ override-dependencies = [ "megatron-core==0.17.0", "numpy<2", "nvidia-resiliency-ext<0.5", - "quack-kernels==0.2.5", + "quack-kernels==0.3.7", "transformer-engine==2.11.0", + "transformers==5.2.0", + "torch==2.11.0", ] exclude-dependencies = ["pynvml", "emerging-optimizers"] no-build-isolation-package = ["apex", "transformer-engine", "transformer-engine-cu12", "transformer-engine-torch", "megatron-bridge", "deep-ep", "nv-grouped-gemm"] [tool.uv.extra-build-dependencies] -apex = ["torch>=2.8.0"] -deep-ep = ["torch>=2.8.0"] -nv-grouped-gemm = ["torch>=2.8.0"] -transformer-engine-torch = ["torch>=2.8.0"] +apex = ["torch>=2.11.0"] +deep-ep = ["torch>=2.11.0"] +nv-grouped-gemm = ["torch>=2.11.0"] +transformer-engine-torch = ["torch>=2.11.0"] [tool.uv.extra-build-variables] apex = { APEX_CPP_EXT = "1", APEX_CUDA_EXT = "1", APEX_FAST_LAYER_NORM = "1", APEX_PARALLEL_BUILD = "16", NVCC_APPEND_FLAGS = "--threads 4" } @@ -180,7 +185,7 @@ requires-dist = [] [[tool.uv.dependency-metadata]] name = "transformer-engine-torch" -version = "0.5.18" +version = "2.11.0" requires-dist = [ "einops", "onnx", @@ -266,8 +271,15 @@ dev = [ ] [tool.uv.sources] +torch = { index = "pytorch-cu128" } apex = { git = "https://github.com/NVIDIA/apex.git", rev = "25.09" } deep-ep = { git = "https://github.com/deepseek-ai/DeepEP.git", rev = "v1.2.1" } +flash-attn-4 = { url = "https://files.pythonhosted.org/packages/24/f7/01ee2576ce41f9884d291ee21861ef194afc0b2b1ce3bd175fc7a6e1b133/flash_attn_4-4.0.0b5-py3-none-any.whl" } megatron-bridge = { git = "https://github.com/NVIDIA-NeMo/Megatron-Bridge.git", rev = "e049cc00c24d03e2ae45d2608c7a44e2d2364e3d" } panza = { git = "https://github.com/corbt/panza.git" } transformer-engine-torch = { git = "https://github.com/NVIDIA/TransformerEngine.git", rev = "v2.11", subdirectory = "transformer_engine/pytorch" } + +[[tool.uv.index]] +name = "pytorch-cu128" +url = "https://download.pytorch.org/whl/cu128" +explicit = true diff --git a/scripts/bump_version.py b/scripts/bump_version.py index 1f6000fdf..57ce7ba16 100755 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -13,15 +13,19 @@ import subprocess import sys +PROJECT_VERSION_RE = re.compile( + r'(?ms)^(\[project\]\s+.*?^version = ")(\d+\.\d+\.\d+)(")' +) + def get_current_version(): """Extract current version from pyproject.toml.""" pyproject_path = Path(__file__).parent.parent / "pyproject.toml" content = pyproject_path.read_text() - match = re.search(r'version = "(\d+\.\d+\.\d+)"', content) + match = PROJECT_VERSION_RE.search(content) if not match: - raise ValueError("Could not find version in pyproject.toml") - return match.group(1) + raise ValueError("Could not find [project] version in pyproject.toml") + return match.group(2) def bump_version(current_version, bump_type): @@ -43,10 +47,11 @@ def update_version(new_version): pyproject_path = Path(__file__).parent.parent / "pyproject.toml" content = pyproject_path.read_text() - # Update version - new_content = re.sub( - r'version = "\d+\.\d+\.\d+"', f'version = "{new_version}"', content + new_content, count = PROJECT_VERSION_RE.subn( + rf"\g<1>{new_version}\3", content, count=1 ) + if count != 1: + raise ValueError("Could not update [project] version in pyproject.toml") pyproject_path.write_text(new_content) diff --git a/src/art/__init__.py b/src/art/__init__.py index b96c591b6..d763b1a4c 100644 --- a/src/art/__init__.py +++ b/src/art/__init__.py @@ -45,8 +45,12 @@ import transformers try: - from .transformers.patches import patch_preprocess_mask_arguments + from .transformers.patches import ( + disable_broken_torchvision_for_transformers, + patch_preprocess_mask_arguments, + ) + disable_broken_torchvision_for_transformers() patch_preprocess_mask_arguments() except Exception: pass @@ -65,6 +69,7 @@ from .trajectories import Trajectory, TrajectoryGroup from .types import ( LocalTrainResult, + MegatronTopologyConfig, Messages, MessagesAndChoices, ServerlessTrainResult, @@ -87,6 +92,7 @@ "LocalBackend", "LocalTrainResult", "LoRAConfig", + "MegatronTopologyConfig", "ServerlessBackend", "ServerlessTrainResult", "Messages", diff --git a/src/art/_backend_training.py b/src/art/_backend_training.py index 6310a31ed..92e013f00 100644 --- a/src/art/_backend_training.py +++ b/src/art/_backend_training.py @@ -9,7 +9,7 @@ summarize_trajectory_groups, ) from .trajectories import TrajectoryGroup -from .types import TrainConfig +from .types import MegatronTopologyConfig, TrainConfig def build_rl_train_configs( @@ -34,6 +34,7 @@ def build_rl_train_configs( scale_learning_rate_by_reward_std_dev: bool | None = None, logprob_calculation_chunk_size: int | None = None, packed_sequence_length: int | None = None, + megatron_topology: MegatronTopologyConfig | dict[str, int | None] | None = None, num_trajectories_learning_rate_multiplier_power: float | None = None, kl_ref_adapter_path: str | None = None, ) -> tuple[TrainConfig, dev.TrainConfig]: @@ -65,6 +66,10 @@ def build_rl_train_configs( dev_config["logprob_calculation_chunk_size"] = logprob_calculation_chunk_size if packed_sequence_length is not None: dev_config["packed_sequence_length"] = packed_sequence_length + if megatron_topology is not None: + dev_config["megatron_topology"] = MegatronTopologyConfig.model_validate( + megatron_topology + ).model_dump(mode="json") if num_trajectories_learning_rate_multiplier_power is not None: dev_config["num_trajectories_learning_rate_multiplier_power"] = ( num_trajectories_learning_rate_multiplier_power diff --git a/src/art/dev/get_model_config.py b/src/art/dev/get_model_config.py index 697171c9d..c48f2cbd3 100644 --- a/src/art/dev/get_model_config.py +++ b/src/art/dev/get_model_config.py @@ -95,4 +95,6 @@ def get_model_config( result["trainer_gpu_ids"] = config["trainer_gpu_ids"] if "inference_gpu_ids" in config: result["inference_gpu_ids"] = config["inference_gpu_ids"] + if "megatron_topology" in config: + result["megatron_topology"] = config["megatron_topology"] return result diff --git a/src/art/dev/model.py b/src/art/dev/model.py index a042c2d47..dc5624dbd 100644 --- a/src/art/dev/model.py +++ b/src/art/dev/model.py @@ -1,10 +1,13 @@ from enum import Enum -from typing import Literal, NoReturn +from typing import TYPE_CHECKING, Literal, NoReturn from typing_extensions import Required, TypedDict from .engine import EngineArgs +if TYPE_CHECKING: + from ..types import MegatronTopologyConfig + RolloutWeightsMode = Literal["lora", "merged"] @@ -135,6 +138,7 @@ class InternalModelConfig(TypedDict, total=False): chat_template_content_format: vLLM chat template content format. chat_template_tool_schema_format: Tool schema rendering format used for local training tokenization. + megatron_topology: Fixed Megatron parallel topology for this model. allow_unvalidated_arch: Permit model-support validation workflows to run architectures that are not yet in the supported-model registry. """ @@ -152,6 +156,7 @@ class InternalModelConfig(TypedDict, total=False): chat_template_path: str chat_template_content_format: str chat_template_tool_schema_format: Literal["default", "vllm_openai"] + megatron_topology: "MegatronTopologyConfig | dict[str, int | None]" allow_unvalidated_arch: bool diff --git a/src/art/dev/train.py b/src/art/dev/train.py index d22bdfee6..c9819b4b3 100644 --- a/src/art/dev/train.py +++ b/src/art/dev/train.py @@ -25,6 +25,10 @@ class TrainConfig(TypedDict, total=False): logprob_calculation_chunk_size: int mask_prob_ratio: bool max_negative_advantage_importance_sampling_weight: float + megatron_topology: dict[ + Literal["tp", "cp", "ep", "pp", "vpp", "etp"], + int | None, + ] moe_routing_replay_bundle: "MoeRoutingReplayBundle | None" moe_routing_replay_path: str | None moe_routing_replay_strict: bool diff --git a/src/art/local/backend.py b/src/art/local/backend.py index fad1fe3f8..37cb6f882 100644 --- a/src/art/local/backend.py +++ b/src/art/local/backend.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from contextlib import asynccontextmanager import gc import hashlib @@ -9,7 +11,7 @@ import socket import time from types import TracebackType -from typing import Any, AsyncIterator, Iterable, Literal, cast +from typing import TYPE_CHECKING, Any, AsyncIterator, Iterable, Literal, cast import warnings logger = logging.getLogger(__name__) @@ -22,11 +24,13 @@ import polars as pl import torch from tqdm import auto as tqdm -from transformers import AutoImageProcessor, AutoTokenizer -from transformers.image_processing_utils import BaseImageProcessor +from transformers import AutoTokenizer from transformers.tokenization_utils_base import PreTrainedTokenizerBase from typing_extensions import Self +if TYPE_CHECKING: + from transformers.image_processing_utils import BaseImageProcessor + from art.utils.output_dirs import ( get_default_art_path, get_model_dir, @@ -66,7 +70,13 @@ tokenize_trajectory_groups, ) from ..trajectories import Trajectory, TrajectoryGroup -from ..types import LocalTrainResult, Message, TrainConfig, TrainSFTConfig +from ..types import ( + LocalTrainResult, + MegatronTopologyConfig, + Message, + TrainConfig, + TrainSFTConfig, +) from ..utils import format_message, get_model_step from .adapter_leases import ( AdapterLeaseManager, @@ -410,6 +420,16 @@ async def adapter_lease( async with pin_inference_step(model.name, step), manager.lease(step): yield + @asynccontextmanager + async def adapter_retention_lease( + self, + model: AnyTrainableModel, + step: int, + ) -> AsyncIterator[None]: + manager = self._adapter_lease_manager(model.name) + async with manager.lease(step): + yield + async def prune_model_adapters( self, model: AnyTrainableModel, @@ -491,6 +511,8 @@ def _get_packed_tensors( self._tokenizers[tokenizer_key] = tokenizer if model.base_model not in self._image_processors: try: + from transformers import AutoImageProcessor + self._image_processors[model.base_model] = ( AutoImageProcessor.from_pretrained(model.base_model, use_fast=True) ) @@ -704,6 +726,7 @@ async def train( # type: ignore[override] scale_learning_rate_by_reward_std_dev: bool = False, logprob_calculation_chunk_size: int = 1024, packed_sequence_length: int | None = None, + megatron_topology: MegatronTopologyConfig | None = None, num_trajectories_learning_rate_multiplier_power: float = 0.0, # Checkpoint behavior save_checkpoint: bool = True, @@ -764,6 +787,9 @@ async def train( # type: ignore[override] packed_sequence_length: Packed sequence length to use for training. When unset, Unsloth keeps the current max-length-rounded-to-2048 behavior. Required for Megatron. + megatron_topology: Parallel topology for Megatron training. When + provided, ART uses it to configure Megatron TP/CP/EP/PP/VPP/ETP + before launching the Megatron runtime. num_trajectories_learning_rate_multiplier_power: Power for learning rate multiplier based on number of trajectories. save_checkpoint: Whether to save a checkpoint after training. @@ -824,6 +850,7 @@ async def train( # type: ignore[override] scale_learning_rate_by_reward_std_dev=scale_learning_rate_by_reward_std_dev, logprob_calculation_chunk_size=logprob_calculation_chunk_size, packed_sequence_length=packed_sequence_length, + megatron_topology=megatron_topology, num_trajectories_learning_rate_multiplier_power=num_trajectories_learning_rate_multiplier_power, kl_ref_adapter_path=resolved_kl_ref_adapter_path, ) diff --git a/src/art/loss.py b/src/art/loss.py index 7a195f5e6..6a4096e68 100644 --- a/src/art/loss.py +++ b/src/art/loss.py @@ -9,6 +9,11 @@ if TYPE_CHECKING: from art.preprocessing.inputs import TrainInputs + from art.preprocessing.pack import PackedTensors + + PackedLossInput = PackedTensors | TrainInputs +else: + PackedLossInput = object class Loss(BaseModel): @@ -21,29 +26,95 @@ class Loss(BaseModel): kl_policy_ref: torch.Tensor | None = None +class AlignedLossInputs(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + assistant_mask: torch.Tensor + old_logprobs: torch.Tensor + advantages: torch.Tensor + weights: torch.Tensor + group_ids: torch.Tensor + original_logprobs: torch.Tensor | None = None + entropies_are_aligned: bool = False + + def align_inputs(self) -> "AlignedLossInputs": + return self + + def group_mean(self, values: torch.Tensor, by: torch.Tensor) -> torch.Tensor: + return group_aggregate(values, by=by, reduce="mean") + + def masked_mean(self, values: torch.Tensor, mask: torch.Tensor) -> torch.Tensor: + return values.sum() / (mask.sum() + 1e-18) + + def denominator(self, mask: torch.Tensor, reduction: Literal["mean", "sum"]): + if reduction == "sum": + return 1.0 + return mask.sum() + 1e-18 + + def aligned_entropies(self, entropies: torch.Tensor | None) -> torch.Tensor | None: + if entropies is None: + return None + if self.entropies_are_aligned: + return entropies + return shift_tensor(entropies, 0.0) + + +class LossInputs(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + inputs: PackedLossInput + + def align_inputs(self) -> AlignedLossInputs: + inputs = self.inputs + return AlignedLossInputs( + assistant_mask=shift_tensor(inputs["assistant_mask"], False), + old_logprobs=shift_tensor(inputs["logprobs"], float("nan")), + advantages=shift_tensor(inputs["advantages"], 0.0), + weights=shift_tensor(inputs["weights"], 0.0), + group_ids=shift_tensor(inputs["group_ids"], 0), + original_logprobs=( + shift_tensor(inputs["original_logprobs"], 0.0) # ty: ignore[invalid-key] + if "original_logprobs" in inputs + else None + ), + ) + + +def compute_probs_corr( + old_logprobs: torch.Tensor, + new_logprobs: torch.Tensor, +) -> torch.Tensor: + old_logprobs_mask = ~torch.isnan(old_logprobs) + old_probs = torch.exp(old_logprobs[old_logprobs_mask]) + new_probs = torch.exp(new_logprobs[old_logprobs_mask]) + if old_probs.numel() < 2: + return new_logprobs.new_zeros(()) + old_std = old_probs.std(unbiased=False) + new_std = new_probs.std(unbiased=False) + if ( + not torch.isfinite(old_std).item() + or not torch.isfinite(new_std).item() + or old_std.item() == 0.0 + or new_std.item() == 0.0 + ): + return new_logprobs.new_zeros(()) + return torch.corrcoef(torch.stack([old_probs, new_probs]))[0, 1] + + def loss_fn( - inputs: "TrainInputs", + inputs: "LossInputs | AlignedLossInputs", new_logprobs: torch.Tensor, ref_logprobs: torch.Tensor | None, entropies: torch.Tensor | None, experimental_config: dev.TrainConfig, reduction: Literal["mean", "sum"] = "mean", ) -> Loss: - old_logprobs = shift_tensor(inputs["logprobs"], float("nan")) - advantages = shift_tensor(inputs["advantages"], 0.0) - assistant_mask = shift_tensor(inputs["assistant_mask"], False).to( - new_logprobs.dtype - ) - weights = shift_tensor(inputs["weights"], 0.0) - old_logprobs_mask = ~torch.isnan(old_logprobs) - probs_corr = torch.corrcoef( - torch.stack( - [ - torch.exp(old_logprobs[old_logprobs_mask]), - torch.exp(new_logprobs[old_logprobs_mask]), - ] - ) - )[0, 1] + aligned_inputs = inputs.align_inputs() + old_logprobs = aligned_inputs.old_logprobs + advantages = aligned_inputs.advantages + assistant_mask = aligned_inputs.assistant_mask.to(new_logprobs.dtype) + weights = aligned_inputs.weights + probs_corr = compute_probs_corr(old_logprobs, new_logprobs) # Assume missing old logprobs were sampled under the current policy old_logprobs = torch.where( torch.isnan(old_logprobs), @@ -57,10 +128,9 @@ def loss_fn( prob_ratio = torch.exp(logprob_diff) if importance_sampling_level != "token": sequence_prob_ratio = torch.exp( - group_aggregate( + aligned_inputs.group_mean( logprob_diff, - by=shift_tensor(inputs["group_ids"], 0) * assistant_mask, - reduce="mean", + by=aligned_inputs.group_ids * assistant_mask, ) ) if importance_sampling_level == "sequence": @@ -93,12 +163,12 @@ def loss_fn( 0.0, ) if tau := experimental_config.get("kimi_k2_tau", None): - advantages -= tau * logprob_diff.detach() + advantages = advantages - tau * logprob_diff.detach() kl_policy_ref: torch.Tensor | None = None kl_penalty_coef = experimental_config.get("kl_penalty_coef", 0.0) if kl_penalty_coef > 0 and ref_logprobs is not None: kl_per_token = (new_logprobs - ref_logprobs).detach() * assistant_mask - avg_kl = kl_per_token.sum() / (assistant_mask.sum() + 1e-6) + avg_kl = aligned_inputs.masked_mean(kl_per_token, assistant_mask) kl_penalty = kl_penalty_coef * (avg_kl - kl_per_token) * assistant_mask advantages = advantages + kl_penalty kl_policy_ref = avg_kl @@ -115,8 +185,8 @@ def loss_fn( * new_logprobs ) if upper_bound := experimental_config.get("truncated_importance_sampling", None): - if "original_logprobs" in inputs: - original_logprobs = shift_tensor(inputs["original_logprobs"], 0.0) # ty:ignore[invalid-key] + if aligned_inputs.original_logprobs is not None: + original_logprobs = aligned_inputs.original_logprobs original_logprobs = torch.where( torch.isnan(original_logprobs), new_logprobs.detach(), @@ -126,12 +196,12 @@ def loss_fn( prob_ratio = torch.exp(logprob_diff) policy_loss *= torch.clamp(prob_ratio, max=upper_bound).detach() policy_loss = policy_loss * weights * assistant_mask - denominator = assistant_mask.sum() + 1e-6 if reduction == "mean" else 1.0 + denominator = aligned_inputs.denominator(assistant_mask, reduction) reduced_policy_loss = policy_loss.sum() / denominator # Compute reduced entropy for the current step. - if entropies is not None: - shifted_entropies = shift_tensor(entropies, 0.0) - entropy = (shifted_entropies * weights * assistant_mask).sum() / denominator + aligned_entropies = aligned_inputs.aligned_entropies(entropies) + if aligned_entropies is not None: + entropy = (aligned_entropies * weights * assistant_mask).sum() / denominator else: entropy = None return Loss( diff --git a/src/art/megatron/__init__.py b/src/art/megatron/__init__.py index 720e3a88f..3c2e5e5b9 100644 --- a/src/art/megatron/__init__.py +++ b/src/art/megatron/__init__.py @@ -5,7 +5,7 @@ def __getattr__(name: str) -> Any: if name == "MegatronBackend": - from .runtime.backend import MegatronBackend + from .backend import MegatronBackend return MegatronBackend raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/art/megatron/runtime/backend.py b/src/art/megatron/backend.py similarity index 83% rename from src/art/megatron/runtime/backend.py rename to src/art/megatron/backend.py index ea64cfd1d..14e5d2e31 100644 --- a/src/art/megatron/runtime/backend.py +++ b/src/art/megatron/backend.py @@ -1,9 +1,9 @@ from mp_actors import move_to_child_process -from ...local.backend import LocalBackend -from ...local.service import ModelService -from ...model import TrainableModel -from ...utils.output_dirs import get_model_dir +from ..local.backend import LocalBackend +from ..local.service import ModelService +from ..model import TrainableModel +from ..utils.output_dirs import get_model_dir class MegatronBackend(LocalBackend): @@ -22,11 +22,10 @@ def __init__( self._requires_explicit_packed_sequence_length = True self._packed_sequence_length_requires_chunk_alignment = False self._supports_result_packing = True - self._default_chat_template_tool_schema_format = "vllm_openai" async def _get_service(self, model: TrainableModel) -> ModelService: - from ...dev.get_model_config import get_model_config - from ..service import MegatronService + from ..dev.get_model_config import get_model_config + from .service import MegatronService if model.name not in self._services: config = get_model_config( diff --git a/src/art/megatron/compile_workarounds.py b/src/art/megatron/compile_workarounds.py index 70e11bcf9..a6d9d916f 100644 --- a/src/art/megatron/compile_workarounds.py +++ b/src/art/megatron/compile_workarounds.py @@ -1,13 +1,16 @@ from __future__ import annotations import os -from typing import Any +from typing import Any, cast import torch from art.megatron.model_support.spec import CompileWorkaroundConfig _INSTALLED_CONFIG: tuple[frozenset[str], str] | None = None +_SELF_ATTN_LINEAR_PROJ_REDUCE_SCATTER_WORKAROUND_FLAG = ( + "disable_compile_self_attn_linear_proj_reduce_scatter" +) def _require_attr(obj: Any, name: str) -> Any: @@ -20,6 +23,9 @@ def _require_attr(obj: Any, name: str) -> Any: def _disable(fn): + if getattr(fn, "__art_compile_disabled__", False): + return fn + fn = getattr(fn, "_torchdynamo_orig_callable", fn) if getattr(fn, "__art_compile_disabled__", False): return fn wrapped = torch.compiler.disable(fn) @@ -42,6 +48,131 @@ def _selected_workaround_flags( return {part.strip() for part in raw.split(",") if part.strip()} +def _install_context_parallel_attention_workaround() -> None: + from art.megatron.context_parallel import core_attention, executor + + # CP attention owns custom comm and side-stream lifetime management. Keep + # that wrapper eager; the inner flex attention kernels compile separately. + executor.run_context_parallel = _disable(executor.run_context_parallel) + core_attention.run_context_parallel = _disable(core_attention.run_context_parallel) + core_attention.ArtContextParallelCoreAttention.forward = _disable( + core_attention.ArtContextParallelCoreAttention.forward + ) + + +def _install_self_attn_linear_proj_reduce_scatter_workaround() -> None: + from megatron.core.tensor_parallel import mappings + + from art.megatron import lora as art_lora + + # SelfAttentionLinearProjLoRA imports this symbol directly from + # art.megatron.lora, so rebinding only megatron.core.tensor_parallel.mappings + # leaves the compiled LoRA path untouched. + wrapped = _disable(mappings.reduce_scatter_to_sequence_parallel_region) + mappings.reduce_scatter_to_sequence_parallel_region = wrapped # type: ignore[assignment] + art_lora.reduce_scatter_to_sequence_parallel_region = wrapped # type: ignore[assignment] + + +class _WeightedSwiGLUNoInnerForwardCast(torch.autograd.Function): + @staticmethod + def forward( + ctx: Any, + input: torch.Tensor, + weights: torch.Tensor, + fp8_input_store: bool, + ) -> torch.Tensor: + input_for_backward = input.to(torch.float8_e4m3fn) if fp8_input_store else input + ctx.save_for_backward(input_for_backward, weights) + ctx.ori_input_dtype = input.dtype + ctx.fp8_input_store = fp8_input_store + x_glu, x_linear = torch.chunk(input, 2, dim=-1) + return torch.nn.functional.silu(x_glu) * x_linear * weights + + @staticmethod + def backward( + ctx: Any, + *grad_outputs: Any, + ) -> tuple[torch.Tensor, torch.Tensor, None]: + from megatron.core.fusions import fused_bias_swiglu + + grad_output = cast(torch.Tensor, grad_outputs[0]) + input, weights = ctx.saved_tensors + input = input.to(ctx.ori_input_dtype) if ctx.fp8_input_store else input + input_grad, weights_grad = fused_bias_swiglu.weighted_swiglu_back( + grad_output, + input, + weights, + ) + return input_grad, weights_grad, None + + +def _install_weighted_bias_swiglu_no_inner_forward_cast_workaround() -> None: + from megatron.core.fusions import fused_bias_swiglu + from megatron.core.transformer import mlp + from megatron.core.transformer.moe import experts + + if getattr( + fused_bias_swiglu.weighted_bias_swiglu_impl, + "__art_no_inner_forward_cast__", + False, + ): + return + + def _empty_weighted_swiglu_output( + input: torch.Tensor, + bias: torch.Tensor | None, + weights: torch.Tensor, + ) -> torch.Tensor: + output_shape = (*input.shape[:-1], int(input.shape[-1]) // 2) + zero = input.sum() * 0.0 + weights.to(dtype=input.dtype).sum() * 0.0 + if bias is not None: + zero = zero + bias.to(dtype=input.dtype).sum() * 0.0 + return zero.expand(output_shape).clone() + + def _weighted_bias_swiglu_no_inner_forward_cast( + input: torch.Tensor, + bias: torch.Tensor | None, + weights: torch.Tensor, + fp8_input_store: bool = False, + ) -> torch.Tensor: + if int(input.numel()) == 0: + return _empty_weighted_swiglu_output(input, bias=bias, weights=weights) + if bias is not None: + raise NotImplementedError( + "Bias is not supported for weighted swiglu fusion" + ) + original_shape = input.shape + output = _WeightedSwiGLUNoInnerForwardCast.apply( + input.view(-1, original_shape[-1]), + weights, + fp8_input_store, + ).to(input.dtype) + return ( + output + if len(original_shape) == 2 + else output.view(*original_shape[:-1], -1) + ) + + setattr( + _weighted_bias_swiglu_no_inner_forward_cast, + "__art_no_inner_forward_cast__", + True, + ) + setattr( + fused_bias_swiglu, + "weighted_bias_swiglu_impl", + _weighted_bias_swiglu_no_inner_forward_cast, + ) + setattr( + mlp, "weighted_bias_swiglu_impl", _weighted_bias_swiglu_no_inner_forward_cast + ) + setattr( + experts, + "weighted_bias_swiglu_impl", + _weighted_bias_swiglu_no_inner_forward_cast, + ) + + def install_torch_compile_workarounds( config: CompileWorkaroundConfig | None = None, ) -> None: @@ -74,6 +205,13 @@ def _sync_dealloc_fake( if "already has a fake impl registered" not in str(exc): raise + if "context_parallel_attention" in flags: + _install_context_parallel_attention_workaround() + if _SELF_ATTN_LINEAR_PROJ_REDUCE_SCATTER_WORKAROUND_FLAG in flags: + _install_self_attn_linear_proj_reduce_scatter_workaround() + if "weighted_bias_swiglu_no_inner_forward_cast" in flags: + _install_weighted_bias_swiglu_no_inner_forward_cast_workaround() + deepep_flags = {"deepep_permute_restore", "deepep_dispatch_combine"} & flags if deepep_flags: deepep_manager = _require_attr(token_dispatcher, "_DeepepManager") @@ -156,12 +294,6 @@ def _sync_dealloc_fake( moe_layer.MoELayer.preprocess = _disable(moe_layer.MoELayer.preprocess) if "moe_forward" in flags: moe_layer.MoELayer.forward = _disable(moe_layer.MoELayer.forward) - if "moe_routed_experts_compute" in flags: - moe_layer.MoELayer.routed_experts_compute = _disable( - moe_layer.MoELayer.routed_experts_compute - ) - if "grouped_mlp_forward" in flags: - _disable_attr(_require_attr(moe_experts, "GroupedMLP"), "forward") if "te_grouped_mlp_forward" in flags: moe_experts.TEGroupedMLP.forward = _disable(moe_experts.TEGroupedMLP.forward) _INSTALLED_CONFIG = installed_config diff --git a/src/art/megatron/context_parallel/__init__.py b/src/art/megatron/context_parallel/__init__.py index 4818a0639..995b0c425 100644 --- a/src/art/megatron/context_parallel/__init__.py +++ b/src/art/megatron/context_parallel/__init__.py @@ -1 +1,40 @@ -"""Minimal context-parallel shared types used by GDN planning.""" +from .builder import build_dense_reference_mask, build_shared_prefix_attention_spec +from .layout_index import TokenLayoutIndex +from .runtime import build_context_parallel_token_layout_index +from .types import ( + ArtContextParallelState, + AttnMaskKind, + AttnSlice, + ContextParallelConfig, + ContextParallelRuntimeKey, + ContextParallelRuntimePlan, + DispatchedPackedTensors, + FlexMaskSpec, + PackedBatchAttentionSpec, + PackedRowAttentionSpec, + ParallelTopology, + PreparedMegatronBatch, + SharedPrefixBuilderConfig, + TokenRange, +) + +__all__ = [ + "ArtContextParallelState", + "AttnMaskKind", + "AttnSlice", + "DispatchedPackedTensors", + "FlexMaskSpec", + "PackedBatchAttentionSpec", + "PackedRowAttentionSpec", + "ParallelTopology", + "PreparedMegatronBatch", + "SharedPrefixBuilderConfig", + "ContextParallelConfig", + "ContextParallelRuntimeKey", + "ContextParallelRuntimePlan", + "TokenRange", + "TokenLayoutIndex", + "build_dense_reference_mask", + "build_context_parallel_token_layout_index", + "build_shared_prefix_attention_spec", +] diff --git a/src/art/megatron/context_parallel/block_mask.py b/src/art/megatron/context_parallel/block_mask.py new file mode 100644 index 000000000..cf49ad278 --- /dev/null +++ b/src/art/megatron/context_parallel/block_mask.py @@ -0,0 +1,359 @@ +from __future__ import annotations + +import numpy as np +import torch +from torch.nn.attention.flex_attention import BlockMask + +from art.megatron.flex_attn.compiled import normalize_sparse_block_size + +from .types import AttnMaskKind, FlexMaskSpec + +_INVALID_Q_GROUP = -(1 << 63) +_INVALID_Q_PARENT = _INVALID_Q_GROUP + 1 +_INVALID_K_GROUP = _INVALID_Q_GROUP + 2 + + +def _build_exact_mask_mod( + *, + q_abs: np.ndarray, + k_abs: np.ndarray, + q_group: np.ndarray, + q_parent: np.ndarray, + k_group: np.ndarray, + device: torch.device, +): + q_abs_tensor = torch.as_tensor(q_abs, device=device, dtype=torch.int64) + k_abs_tensor = torch.as_tensor(k_abs, device=device, dtype=torch.int64) + q_group_tensor = torch.as_tensor(q_group, device=device, dtype=torch.int64) + q_parent_tensor = torch.as_tensor(q_parent, device=device, dtype=torch.int64) + k_group_tensor = torch.as_tensor(k_group, device=device, dtype=torch.int64) + + def mask_mod( + batch_idx: torch.Tensor, + head_idx: torch.Tensor, + query_idx: torch.Tensor, + kv_idx: torch.Tensor, + ) -> torch.Tensor: + del batch_idx, head_idx + q_abs_local = q_abs_tensor[query_idx] + k_abs_local = k_abs_tensor[kv_idx] + same_group = q_group_tensor[query_idx] == k_group_tensor[kv_idx] + parent_prefix = q_parent_tensor[query_idx] == k_group_tensor[kv_idx] + return (q_abs_local >= k_abs_local) & (same_group | parent_prefix) + + return mask_mod + + +def _dense_blocks_to_ordered( + blocks: np.ndarray, + *, + device: torch.device, +) -> tuple[torch.Tensor, torch.Tensor]: + counts = torch.from_numpy(blocks.sum(axis=-1).astype(np.int32)) + indices = torch.from_numpy( + np.argsort(-blocks.astype(np.int32), axis=-1, kind="stable").astype(np.int32) + ) + return ( + counts.view(1, 1, -1).to(device=device), + indices.view(1, 1, blocks.shape[0], blocks.shape[1]).to(device=device), + ) + + +def _select_with_invalid_np( + values: np.ndarray, + indices: np.ndarray, + *, + invalid_value: int, +) -> np.ndarray: + selected = np.full(indices.shape, invalid_value, dtype=np.int64) + valid = indices >= 0 + if bool(valid.any()): + selected[valid] = values[indices[valid]] + return selected + + +def _build_q_block_group_state( + *, + q_abs: np.ndarray, + q_group: np.ndarray, + q_parent: np.ndarray, + q_block: int, + q_blocks: int, +) -> tuple[np.ndarray, list[dict[int, int]], list[frozenset[int]]]: + q_min_by_block = np.empty((q_blocks,), dtype=np.int64) + q_allowed_max_by_group: list[dict[int, int]] = [] + q_all_allowed_groups: list[frozenset[int]] = [] + for block_idx in range(q_blocks): + start = block_idx * q_block + end = min((block_idx + 1) * q_block, int(q_abs.size)) + q = q_abs[start:end] + q_group_block = q_group[start:end] + q_parent_block = q_parent[start:end] + q_min_by_block[block_idx] = int(q.min()) if int(q.size) else 0 + max_by_group: dict[int, int] = {} + all_groups: list[int] = [] + for group_value in np.unique(np.concatenate((q_group_block, q_parent_block))): + allowed = (q_group_block == group_value) | (q_parent_block == group_value) + if bool(allowed.any()): + max_by_group[int(group_value)] = int(q[allowed].max()) + if bool(allowed.all()): + all_groups.append(int(group_value)) + q_allowed_max_by_group.append(max_by_group) + q_all_allowed_groups.append(frozenset(all_groups)) + return q_min_by_block, q_allowed_max_by_group, q_all_allowed_groups + + +def _build_k_block_group_state( + *, + k_abs: np.ndarray, + k_group: np.ndarray, + k_block: int, + k_blocks: int, +) -> tuple[np.ndarray, list[dict[int, int]], list[tuple[int, ...]]]: + k_max_by_block = np.empty((k_blocks,), dtype=np.int64) + k_min_by_group: list[dict[int, int]] = [] + k_groups_by_block: list[tuple[int, ...]] = [] + for block_idx in range(k_blocks): + start = block_idx * k_block + end = min((block_idx + 1) * k_block, int(k_abs.size)) + k = k_abs[start:end] + k_group_block = k_group[start:end] + k_max_by_block[block_idx] = int(k.max()) if int(k.size) else 0 + min_by_group: dict[int, int] = {} + for group_value in np.unique(k_group_block): + min_by_group[int(group_value)] = int(k[k_group_block == group_value].min()) + k_min_by_group.append(min_by_group) + k_groups_by_block.append(tuple(min_by_group)) + return k_max_by_block, k_min_by_group, k_groups_by_block + + +def _exact_block_state( + *, + q_idx: int, + k_idx: int, + q_min_by_block: np.ndarray, + q_allowed_max_by_group: list[dict[int, int]], + q_all_allowed_groups: list[frozenset[int]], + k_max_by_block: np.ndarray, + k_min_by_group: list[dict[int, int]], + k_groups_by_block: list[tuple[int, ...]], +) -> tuple[bool, bool]: + q_allowed_max = q_allowed_max_by_group[q_idx] + k_min = k_min_by_group[k_idx] + if not any( + q_allowed_max.get(k_group_value, _INVALID_Q_GROUP) >= min_k + for k_group_value, min_k in k_min.items() + ): + return False, False + if int(q_min_by_block[q_idx]) < int(k_max_by_block[k_idx]): + return True, False + q_all_allowed = q_all_allowed_groups[q_idx] + return True, all( + k_group_value in q_all_allowed for k_group_value in k_groups_by_block[k_idx] + ) + + +def _build_sparse_block_mask( + spec: FlexMaskSpec, + *, + device: torch.device, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + block_size: tuple[int, int], +) -> BlockMask: + q_block, k_block = block_size + q_blocks = (int(spec.q_len) + q_block - 1) // q_block + k_blocks = (int(spec.k_len) + k_block - 1) // k_block + partial_blocks = np.zeros((q_blocks, k_blocks), dtype=bool) + full_blocks = np.zeros((q_blocks, k_blocks), dtype=bool) + touch_counts = np.zeros((q_blocks, k_blocks), dtype=np.int16) + q_abs_tensor = spec.exact_mask.q_token_indices.detach().to( + device="cpu", + dtype=torch.int64, + ) + k_abs_tensor = spec.exact_mask.k_token_indices.detach().to( + device="cpu", + dtype=torch.int64, + ) + q_abs = q_abs_tensor.numpy() + k_abs = k_abs_tensor.numpy() + flat_group_ids = group_ids.detach().to(device="cpu", dtype=torch.int64).reshape(-1) + flat_parent_ids = ( + parent_ids.detach().to(device="cpu", dtype=torch.int64).reshape(-1) + ) + flat_group_ids_np = flat_group_ids.numpy() + flat_parent_ids_np = flat_parent_ids.numpy() + q_group = _select_with_invalid_np( + flat_group_ids_np, + q_abs, + invalid_value=_INVALID_Q_GROUP, + ) + q_parent = _select_with_invalid_np( + flat_parent_ids_np, + q_abs, + invalid_value=_INVALID_Q_PARENT, + ) + k_group = _select_with_invalid_np( + flat_group_ids_np, + k_abs, + invalid_value=_INVALID_K_GROUP, + ) + mask_mod = _build_exact_mask_mod( + q_abs=q_abs, + k_abs=k_abs, + q_group=q_group, + q_parent=q_parent, + k_group=k_group, + device=device, + ) + q_min_by_block, q_allowed_max_by_group, q_all_allowed_groups = ( + _build_q_block_group_state( + q_abs=q_abs, + q_group=q_group, + q_parent=q_parent, + q_block=q_block, + q_blocks=q_blocks, + ) + ) + k_max_by_block, k_min_by_group, k_groups_by_block = _build_k_block_group_state( + k_abs=k_abs, + k_group=k_group, + k_block=k_block, + k_blocks=k_blocks, + ) + if not spec.slices: + raise RuntimeError( + "Cannot build a CP attention block mask without stage slices" + ) + + for slice_ in spec.slices: + q_start = max(0, int(slice_.q_range.start)) + q_end = min(int(spec.q_len), int(slice_.q_range.end)) + k_start = max(0, int(slice_.k_range.start)) + k_end = min(int(spec.k_len), int(slice_.k_range.end)) + q_block_indices = np.arange( + q_start // q_block, + (q_end + q_block - 1) // q_block, + dtype=np.int64, + ) + k_block_indices = np.arange( + k_start // k_block, + (k_end + k_block - 1) // k_block, + dtype=np.int64, + ) + if int(q_block_indices.size) == 0 or int(k_block_indices.size) == 0: + continue + q_block_start = q_block_indices * q_block + q_block_end = np.minimum( + (q_block_indices + 1) * q_block, + int(spec.q_len), + ) + k_block_start = k_block_indices * k_block + k_block_end = np.minimum( + (k_block_indices + 1) * k_block, + int(spec.k_len), + ) + q_overlap_start = np.maximum( + q_block_start, + q_start, + ) + q_overlap_end = np.minimum( + q_block_end, + q_end, + ) + k_overlap_start = np.maximum( + k_block_start, + k_start, + ) + k_overlap_end = np.minimum( + k_block_end, + k_end, + ) + q_min = q_abs[q_overlap_start] + q_max = q_abs[q_overlap_end - 1] + k_min = k_abs[k_overlap_start] + k_max = k_abs[k_overlap_end - 1] + q_is_full = (q_overlap_start == q_block_start) & (q_overlap_end == q_block_end) + k_is_full = (k_overlap_start == k_block_start) & (k_overlap_end == k_block_end) + covers_block = q_is_full[:, None] & k_is_full[None, :] + if slice_.mask_kind == AttnMaskKind.FULL: + has_any = np.ones( + (int(q_block_indices.size), int(k_block_indices.size)), dtype=bool + ) + is_full = covers_block + else: + has_any = q_max[:, None] >= k_min[None, :] + is_full = covers_block & (q_min[:, None] >= k_max[None, :]) + + q_slice = slice(int(q_block_indices[0]), int(q_block_indices[-1]) + 1) + k_slice = slice(int(k_block_indices[0]), int(k_block_indices[-1]) + 1) + touch_counts[q_slice, k_slice] += has_any.astype(np.int16) + partial_blocks[q_slice, k_slice] |= has_any + full_blocks[q_slice, k_slice] |= is_full + + ambiguous = (touch_counts > 1) & partial_blocks & ~full_blocks + for q_idx, k_idx in np.argwhere(ambiguous): + has_any, is_full = _exact_block_state( + q_idx=int(q_idx), + k_idx=int(k_idx), + q_min_by_block=q_min_by_block, + q_allowed_max_by_group=q_allowed_max_by_group, + q_all_allowed_groups=q_all_allowed_groups, + k_max_by_block=k_max_by_block, + k_min_by_group=k_min_by_group, + k_groups_by_block=k_groups_by_block, + ) + partial_blocks[q_idx, k_idx] = False + full_blocks[q_idx, k_idx] = False + if is_full: + full_blocks[q_idx, k_idx] = True + elif has_any: + partial_blocks[q_idx, k_idx] = True + + partial_blocks &= ~full_blocks + kv_num_blocks, kv_indices = _dense_blocks_to_ordered( + partial_blocks, + device=device, + ) + full_kv_num_blocks, full_kv_indices = _dense_blocks_to_ordered( + full_blocks, + device=device, + ) + return BlockMask.from_kv_blocks( + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + BLOCK_SIZE=block_size, + mask_mod=mask_mod, + seq_lengths=(int(spec.q_len), int(spec.k_len)), + ) + + +def build_block_mask( + spec: FlexMaskSpec, + *, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + device: torch.device, +) -> BlockMask | None: + if spec.q_len <= 0 or spec.k_len <= 0: + return None + if int(spec.exact_mask.q_token_indices.numel()) != int(spec.q_len): + raise RuntimeError( + "Exact stage q-token metadata length mismatch: " + f"{int(spec.exact_mask.q_token_indices.numel())} != {int(spec.q_len)}" + ) + if int(spec.exact_mask.k_token_indices.numel()) != int(spec.k_len): + raise RuntimeError( + "Exact stage k-token metadata length mismatch: " + f"{int(spec.exact_mask.k_token_indices.numel())} != {int(spec.k_len)}" + ) + block_size = normalize_sparse_block_size(spec.block_size) + return _build_sparse_block_mask( + spec, + device=device, + group_ids=group_ids, + parent_ids=parent_ids, + block_size=block_size, + ) diff --git a/src/art/megatron/context_parallel/builder.py b/src/art/megatron/context_parallel/builder.py new file mode 100644 index 000000000..77ac1b623 --- /dev/null +++ b/src/art/megatron/context_parallel/builder.py @@ -0,0 +1,317 @@ +from __future__ import annotations + +import torch + +from .types import ( + AttnMaskKind, + AttnSlice, + PackedBatchAttentionSpec, + PackedRowAttentionSpec, + SharedPrefixBuilderConfig, + TokenRange, +) + + +def _valid_length( + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + *, + ignore_padding_group_id: int, +) -> int: + valid_mask = group_ids != ignore_padding_group_id + valid_count = int(valid_mask.sum().item()) + if valid_count == 0: + return 0 + if not bool(valid_mask[:valid_count].all().item()): + raise RuntimeError("Padding tokens must be a contiguous tail") + return _infer_terminal_padding_length( + group_ids[:valid_count], + parent_ids[:valid_count], + ) + + +def _infer_terminal_padding_length( + group_row: torch.Tensor, + parent_row: torch.Tensor, +) -> int: + if group_row.numel() == 0: + return 0 + runs = _scan_runs(group_row, parent_row) + if len(runs) < 2: + return int(group_row.numel()) + last_start, _last_end, last_group_id, last_parent_id = runs[-1] + if last_parent_id >= 0: + return int(group_row.numel()) + terminal_pair = (last_group_id, last_parent_id) + if any( + (group_id, parent_id) == terminal_pair + for _start, _end, group_id, parent_id in runs[:-1] + ): + return last_start + return int(group_row.numel()) + + +def _scan_runs( + group_row: torch.Tensor, + parent_row: torch.Tensor, +) -> list[tuple[int, int, int, int]]: + length = int(group_row.numel()) + if length == 0: + return [] + + group_changes = group_row[1:] != group_row[:-1] + parent_changes = parent_row[1:] != parent_row[:-1] + inconsistent_parent = torch.nonzero( + torch.logical_not(group_changes) & parent_changes, + as_tuple=False, + ).flatten() + if int(inconsistent_parent.numel()) > 0: + mismatch_index = int(inconsistent_parent[0].item()) + 1 + prior_boundaries = torch.nonzero( + group_changes[: mismatch_index - 1], + as_tuple=False, + ).flatten() + start = ( + 0 + if int(prior_boundaries.numel()) == 0 + else int(prior_boundaries[-1].item()) + 1 + ) + group_id = int(group_row[start].item()) + raise RuntimeError( + "Found one group run with inconsistent parent ids: " + f"group_id={group_id}, start={start}, end={mismatch_index}" + ) + + run_starts = torch.cat( + ( + torch.zeros(1, dtype=torch.int64, device=group_row.device), + torch.nonzero(group_changes, as_tuple=False).flatten() + 1, + ) + ) + run_ends = torch.cat( + ( + run_starts[1:], + torch.tensor([length], dtype=torch.int64, device=group_row.device), + ) + ) + starts = run_starts.to(device="cpu").tolist() + ends = run_ends.to(device="cpu").tolist() + group_ids = group_row.index_select(0, run_starts).to(device="cpu").tolist() + parent_ids = parent_row.index_select(0, run_starts).to(device="cpu").tolist() + return [ + (int(start), int(end), int(group_id), int(parent_id)) + for start, end, group_id, parent_id in zip( + starts, ends, group_ids, parent_ids, strict=True + ) + ] + + +def _sort_and_dedupe_slices(slices: list[AttnSlice]) -> tuple[AttnSlice, ...]: + sorted_slices = sorted( + slices, + key=lambda slice_: ( + int(slice_.row_index), + int(slice_.q_range.start), + int(slice_.q_range.end), + int(slice_.k_range.start), + int(slice_.k_range.end), + str(slice_.mask_kind), + -1 if slice_.family_index is None else int(slice_.family_index), + ), + ) + deduped: list[AttnSlice] = [] + last_key: tuple[int, int, int, int, int, str, int] | None = None + for slice_ in sorted_slices: + key = ( + int(slice_.row_index), + int(slice_.q_range.start), + int(slice_.q_range.end), + int(slice_.k_range.start), + int(slice_.k_range.end), + str(slice_.mask_kind), + -1 if slice_.family_index is None else int(slice_.family_index), + ) + if key == last_key: + continue + deduped.append(slice_) + last_key = key + return tuple(deduped) + + +def _is_prompt_run( + *, + start: int, + group_id: int, + parent_id: int, + ignore_padding_group_id: int, +) -> bool: + return group_id == parent_id or ( + start == 0 and parent_id == ignore_padding_group_id + ) + + +def build_shared_prefix_attention_spec( + *, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + config: SharedPrefixBuilderConfig = SharedPrefixBuilderConfig(), +) -> PackedBatchAttentionSpec: + if group_ids.shape != parent_ids.shape: + raise RuntimeError( + "group_ids and parent_ids must share shape, got " + f"{tuple(group_ids.shape)} vs {tuple(parent_ids.shape)}" + ) + if group_ids.ndim != 2: + raise RuntimeError( + "group_ids and parent_ids must be rank-2 packed tensors, got " + f"{group_ids.ndim}" + ) + if int(group_ids.shape[0]) != 1: + raise RuntimeError( + "ART shared-prefix attention spec currently supports exactly one packed sequence, " + f"got batch={int(group_ids.shape[0])}." + ) + + rows: list[PackedRowAttentionSpec] = [] + for row_index in range(group_ids.shape[0]): + group_row = group_ids[row_index] + parent_row = parent_ids[row_index] + valid_tokens = _valid_length( + group_row, + parent_row, + ignore_padding_group_id=config.ignore_padding_group_id, + ) + if valid_tokens == 0: + rows.append( + PackedRowAttentionSpec(row_index=row_index, valid_tokens=0, slices=()) + ) + continue + + group_row = group_row[:valid_tokens] + parent_row = parent_row[:valid_tokens] + runs = _scan_runs(group_row, parent_row) + + group_run_count: dict[int, int] = {} + prompt_by_group_id: dict[int, tuple[tuple[int, int], int]] = {} + completion_ranges_by_prompt: dict[int, list[tuple[int, int]]] = {} + + for start, end, group_id, parent_id in runs: + group_run_count[group_id] = group_run_count.get(group_id, 0) + 1 + if _is_prompt_run( + start=start, + group_id=group_id, + parent_id=parent_id, + ignore_padding_group_id=config.ignore_padding_group_id, + ): + if group_id in prompt_by_group_id: + raise RuntimeError( + f"Prompt group_id {group_id} appears more than once in row {row_index}" + ) + family_index = len(prompt_by_group_id) + prompt_by_group_id[group_id] = ( + (start, end), + family_index, + ) + completion_ranges_by_prompt[group_id] = [] + + if config.require_contiguous_group_runs: + repeated_groups = { + group_id: count + for group_id, count in group_run_count.items() + if count > 1 and group_id != config.ignore_padding_group_id + } + if repeated_groups: + raise RuntimeError( + "Shared-prefix builder requires contiguous group runs per row, " + f"found repeats in row {row_index}: {repeated_groups}" + ) + + for start, end, group_id, parent_id in runs: + if _is_prompt_run( + start=start, + group_id=group_id, + parent_id=parent_id, + ignore_padding_group_id=config.ignore_padding_group_id, + ): + continue + prompt_entry = prompt_by_group_id.get(parent_id) + if prompt_entry is None: + raise RuntimeError( + "Completion run points to a missing prompt run: " + f"row={row_index}, group_id={group_id}, parent_id={parent_id}" + ) + completion_ranges_by_prompt[parent_id].append((start, end)) + + row_slices: list[AttnSlice] = [] + for prompt_group_id, ( + (prompt_start, prompt_end), + family_index, + ) in prompt_by_group_id.items(): + prompt_range = TokenRange(start=prompt_start, end=prompt_end) + row_slices.append( + AttnSlice( + q_range=prompt_range, + k_range=prompt_range, + mask_kind=AttnMaskKind.CAUSAL, + row_index=row_index, + family_index=family_index, + ) + ) + for completion_start, completion_end in completion_ranges_by_prompt[ + prompt_group_id + ]: + completion_range = TokenRange( + start=completion_start, + end=completion_end, + ) + row_slices.append( + AttnSlice( + q_range=completion_range, + k_range=prompt_range, + mask_kind=AttnMaskKind.FULL, + row_index=row_index, + family_index=family_index, + ) + ) + row_slices.append( + AttnSlice( + q_range=completion_range, + k_range=completion_range, + mask_kind=AttnMaskKind.CAUSAL, + row_index=row_index, + family_index=family_index, + ) + ) + + rows.append( + PackedRowAttentionSpec( + row_index=row_index, + valid_tokens=valid_tokens, + slices=_sort_and_dedupe_slices(row_slices), + ) + ) + + return PackedBatchAttentionSpec(rows=tuple(rows)) + + +def build_dense_reference_mask( + *, + row_spec: PackedRowAttentionSpec, +) -> torch.Tensor: + dense = torch.zeros( + (row_spec.valid_tokens, row_spec.valid_tokens), + dtype=torch.bool, + ) + for slice_ in row_spec.slices: + q = slice_.q_range + k = slice_.k_range + if slice_.mask_kind is AttnMaskKind.FULL: + dense[q.start : q.end, k.start : k.end] = True + continue + for q_idx in range(q.start, q.end): + rel_q = q_idx - q.start + max_k = k.start + rel_q + if max_k < k.start: + continue + dense[q_idx, k.start : min(k.end, max_k + 1)] = True + return dense diff --git a/src/art/megatron/context_parallel/comm.py b/src/art/megatron/context_parallel/comm.py new file mode 100644 index 000000000..c1767a4dc --- /dev/null +++ b/src/art/megatron/context_parallel/comm.py @@ -0,0 +1,725 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Protocol, cast + +import torch +import torch.distributed as dist + +from .range_ops import ( + range_gather, + range_gather_head_major, + range_reduce_sum_, + range_reduce_sum_head_major_, +) +from .types import DkvReducePlan, KvFetchPlan, TokenRange + +_DIST = cast(Any, dist) + + +class _Waitable(Protocol): + def wait(self) -> Any: ... + + +def _launch_peer_exchange( + *, + recv_buffer: torch.Tensor, + send_buffer: torch.Tensor, + output_split_sizes: list[int], + input_split_sizes: list[int], + group: Any, + async_op: bool, +) -> _Waitable | None: + # CP exchange waves are globally scheduled: every rank in the CP group must + # enter the wave's collective in the same order, even when this rank's local + # split sizes are all zero. + return cast( + _Waitable | None, + _DIST.all_to_all_single( + recv_buffer, + send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=async_op, + ), + ) + + +@dataclass +class KvFetchWork: + packed_buffer: torch.Tensor + recv_splits: tuple[int, ...] + handle: _Waitable | None + send_buffer: torch.Tensor | None = None + stream: torch.cuda.Stream | None = None + label: str = "kv_fetch" + output_layout: str = "head_major" + _wait_complete: bool = False + + def is_completed(self) -> bool: + if self._wait_complete: + return True + handle_complete = True + if self.handle is not None: + is_completed = getattr(self.handle, "is_completed", None) + if callable(is_completed): + handle_complete = bool(is_completed()) + if self.stream is not None: + return handle_complete and bool(self.stream.query()) + return handle_complete + + def wait(self) -> None: + if self._wait_complete: + return + if self.handle is not None: + self.handle.wait() + if self.stream is not None: + current_stream = torch.cuda.current_stream(self.packed_buffer.device) + current_stream.wait_stream(self.stream) + self._wait_complete = True + + def wait_post_process(self) -> tuple[torch.Tensor, torch.Tensor]: + self.wait() + return _unpack_packed_tensor_per_peer( + self.packed_buffer, + self.recv_splits, + output_layout=self.output_layout, + ) + + +@dataclass +class DkvReduceWork: + packed_buffer: torch.Tensor | None + handle: _Waitable | None + send_buffer: torch.Tensor | None + stream: torch.cuda.Stream | None + plan: DkvReducePlan + dk_local: torch.Tensor + dv_local: torch.Tensor + range_meta_cache: dict[Any, Any] | None = None + label: str = "dkv_reduce" + input_layout: str = "token_major" + _wait_complete: bool = False + + def is_completed(self) -> bool: + if self._wait_complete: + return True + handle_complete = True + if self.handle is not None: + is_completed = getattr(self.handle, "is_completed", None) + if callable(is_completed): + handle_complete = bool(is_completed()) + if self.stream is not None: + return handle_complete and bool(self.stream.query()) + return handle_complete + + def wait(self) -> None: + if self._wait_complete: + return + if self.handle is not None: + self.handle.wait() + if self.stream is not None and self.packed_buffer is not None: + current_stream = torch.cuda.current_stream(self.packed_buffer.device) + current_stream.wait_stream(self.stream) + self._wait_complete = True + + def wait_post_process(self) -> tuple[torch.Tensor, torch.Tensor]: + self.wait() + if self.packed_buffer is not None and int(self.packed_buffer.shape[0]) > 0: + dk_remote, dv_remote = _unpack_packed_tensor_per_peer( + self.packed_buffer, + self.plan.recv_splits, + output_layout=( + "head_major" if self.input_layout == "head_major" else "token_major" + ), + ) + flattened_ranges = tuple( + range_ + for peer_ranges in self.plan.recv_ranges_by_peer + for range_ in peer_ranges + if range_.size() > 0 + ) + + def _apply_reduce() -> None: + dk_reduce = ( + dk_remote + if dk_remote.dtype == self.dk_local.dtype + else dk_remote.to(dtype=self.dk_local.dtype) + ) + dv_reduce = ( + dv_remote + if dv_remote.dtype == self.dv_local.dtype + else dv_remote.to(dtype=self.dv_local.dtype) + ) + reduce_fn = ( + range_reduce_sum_head_major_ + if self.input_layout == "head_major" + else range_reduce_sum_ + ) + reduce_fn( + dk_reduce, + output_tensor=self.dk_local, + ranges=flattened_ranges, + range_meta_cache=self.range_meta_cache, + ) + reduce_fn( + dv_reduce, + output_tensor=self.dv_local, + ranges=flattened_ranges, + range_meta_cache=self.range_meta_cache, + ) + return + + _apply_reduce() + return self.dk_local, self.dv_local + + +class A2AVCommunicator: + def __init__(self) -> None: + self._streams: dict[int, torch.cuda.Stream] = {} + + def _get_stream(self, tensor: torch.Tensor) -> torch.cuda.Stream | None: + if not tensor.is_cuda: + return None + device_index = tensor.device.index + if device_index is None: + device_index = torch.cuda.current_device() + stream = self._streams.get(device_index) + if stream is None: + stream = torch.cuda.Stream(device=tensor.device) + self._streams[device_index] = stream + return stream + + def launch_kv_fetch( + self, + *, + k_local: torch.Tensor, + v_local: torch.Tensor, + plan: KvFetchPlan, + group: Any, + async_op: bool, + range_meta_cache: dict[Any, Any] | None = None, + label: str = "kv_fetch", + input_layout: str = "token_major", + output_layout: str = "head_major", + ) -> KvFetchWork: + if group is None or _DIST.get_world_size(group) == 1: + return KvFetchWork( + packed_buffer=k_local.new_empty( + _packed_peer_tensor_shape( + tensor=k_local, + total_rows=0, + input_layout=input_layout, + ) + ), + recv_splits=plan.recv_splits, + handle=None, + label=label, + output_layout=output_layout, + ) + + total_send_rows = int(sum(plan.send_splits)) + total_recv_rows = int(sum(plan.recv_splits)) + recv_packed = k_local.new_empty( + _packed_peer_tensor_shape( + tensor=k_local, + total_rows=total_recv_rows, + input_layout=input_layout, + ) + ) + input_split_sizes = [split * 2 for split in plan.send_splits] + output_split_sizes = [split * 2 for split in plan.recv_splits] + stream = self._get_stream(k_local) if async_op else None + if stream is not None: + current_stream = torch.cuda.current_stream(k_local.device) + if total_send_rows <= 0: + send_buffer = k_local.new_empty( + _packed_peer_tensor_shape( + tensor=k_local, + total_rows=0, + input_layout=input_layout, + ) + ) + else: + send_buffer = _pack_gathered_tensors_per_peer( + left_tensor=k_local, + right_tensor=v_local, + ranges_by_peer=plan.send_ranges_by_peer, + range_meta_cache=range_meta_cache, + input_layout=input_layout, + ) + stream.wait_stream(current_stream) + send_buffer.record_stream(stream) + recv_packed.record_stream(stream) + with torch.cuda.stream(stream): + handle = _launch_peer_exchange( + recv_buffer=recv_packed, + send_buffer=send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=True, + ) + else: + if total_send_rows <= 0: + send_buffer = k_local.new_empty( + _packed_peer_tensor_shape( + tensor=k_local, + total_rows=0, + input_layout=input_layout, + ) + ) + handle = _launch_peer_exchange( + recv_buffer=recv_packed, + send_buffer=send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=async_op, + ) + else: + send_buffer = _pack_gathered_tensors_per_peer( + left_tensor=k_local, + right_tensor=v_local, + ranges_by_peer=plan.send_ranges_by_peer, + range_meta_cache=range_meta_cache, + input_layout=input_layout, + ) + handle = _launch_peer_exchange( + recv_buffer=recv_packed, + send_buffer=send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=async_op, + ) + return KvFetchWork( + packed_buffer=recv_packed, + recv_splits=plan.recv_splits, + handle=handle, + send_buffer=send_buffer, + stream=stream, + label=label, + output_layout=output_layout, + ) + + def launch_dkv_reduce( + self, + *, + dk_remote: torch.Tensor, + dv_remote: torch.Tensor, + plan: DkvReducePlan, + group: Any, + async_op: bool, + dk_local: torch.Tensor, + dv_local: torch.Tensor, + range_meta_cache: dict[Any, Any] | None = None, + label: str = "dkv_reduce", + input_layout: str = "token_major", + ) -> DkvReduceWork: + if group is None or _DIST.get_world_size(group) == 1: + return DkvReduceWork( + packed_buffer=None, + handle=None, + send_buffer=None, + stream=None, + plan=plan, + dk_local=dk_local, + dv_local=dv_local, + range_meta_cache=range_meta_cache, + label=label, + ) + + total_send_rows = int(sum(plan.send_splits)) + recv_total = int(sum(plan.recv_splits)) + recv_packed = ( + dk_remote.new_empty( + _packed_peer_tensor_shape( + tensor=dk_remote, + total_rows=recv_total, + input_layout=input_layout, + ) + ) + if recv_total > 0 + else dk_remote.new_empty( + _packed_peer_tensor_shape( + tensor=dk_remote, + total_rows=0, + input_layout=input_layout, + ) + ) + ) + input_split_sizes = [split * 2 for split in plan.send_splits] + output_split_sizes = [split * 2 for split in plan.recv_splits] + stream = self._get_stream(dk_remote) if async_op else None + if stream is not None: + current_stream = torch.cuda.current_stream(dk_remote.device) + if total_send_rows <= 0: + send_buffer = dk_remote.new_empty( + _packed_peer_tensor_shape( + tensor=dk_remote, + total_rows=0, + input_layout=input_layout, + ) + ) + else: + send_buffer = _pack_split_tensors_by_peer( + left_tensor=dk_remote, + right_tensor=dv_remote, + splits=plan.send_splits, + input_layout=input_layout, + ) + stream.wait_stream(current_stream) + send_buffer.record_stream(stream) + recv_packed.record_stream(stream) + with torch.cuda.stream(stream): + handle = _launch_peer_exchange( + recv_buffer=recv_packed, + send_buffer=send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=True, + ) + else: + if total_send_rows <= 0: + send_buffer = dk_remote.new_empty( + _packed_peer_tensor_shape( + tensor=dk_remote, + total_rows=0, + input_layout=input_layout, + ) + ) + handle = _launch_peer_exchange( + recv_buffer=recv_packed, + send_buffer=send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=async_op, + ) + else: + send_buffer = _pack_split_tensors_by_peer( + left_tensor=dk_remote, + right_tensor=dv_remote, + splits=plan.send_splits, + input_layout=input_layout, + ) + handle = _launch_peer_exchange( + recv_buffer=recv_packed, + send_buffer=send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=async_op, + ) + return DkvReduceWork( + packed_buffer=recv_packed if recv_total > 0 else None, + handle=handle, + send_buffer=send_buffer, + stream=stream, + plan=plan, + dk_local=dk_local, + dv_local=dv_local, + range_meta_cache=range_meta_cache, + label=label, + input_layout=input_layout, + ) + + +def range_gather_per_peer( + input_tensor: torch.Tensor, + ranges_by_peer: tuple[tuple[TokenRange, ...], ...], + range_meta_cache: dict[Any, Any] | None = None, +) -> torch.Tensor: + chunks = [ + range_gather( + input_tensor, + peer_ranges, + range_meta_cache=range_meta_cache, + ) + for peer_ranges in ranges_by_peer + ] + if not chunks: + return input_tensor.new_empty((0, *input_tensor.shape[1:])) + nonempty = [chunk for chunk in chunks if int(chunk.shape[0]) > 0] + if not nonempty: + return input_tensor.new_empty((0, *input_tensor.shape[1:])) + return torch.cat(chunks, dim=0).contiguous() + + +def _split_tensor_to_peer( + input_tensor: torch.Tensor, + splits: tuple[int, ...], +) -> torch.Tensor: + if int(sum(splits)) == 0: + return input_tensor.new_empty((0, *input_tensor.shape[1:])) + if int(input_tensor.shape[0]) == int(sum(splits)): + return input_tensor.contiguous() + if len([split for split in splits if split > 0]) > 1: + raise RuntimeError( + f"Expected at most one non-zero send split for dKV reduce, got {splits}" + ) + pieces: list[torch.Tensor] = [] + cursor = 0 + for split in splits: + if split == 0: + pieces.append(input_tensor.new_empty((0, *input_tensor.shape[1:]))) + continue + pieces.append(input_tensor[cursor : cursor + split]) + cursor += split + return torch.cat(pieces, dim=0).contiguous() + + +def _pack_gathered_tensors_per_peer( + *, + left_tensor: torch.Tensor, + right_tensor: torch.Tensor, + ranges_by_peer: tuple[tuple[TokenRange, ...], ...], + range_meta_cache: dict[Any, Any] | None = None, + input_layout: str = "token_major", +) -> torch.Tensor: + if input_layout == "head_major": + return _pack_gathered_tensors_per_peer_head_major( + left_tensor=left_tensor, + right_tensor=right_tensor, + ranges_by_peer=ranges_by_peer, + range_meta_cache=range_meta_cache, + ) + if input_layout != "token_major": + raise ValueError(f"Unsupported gathered-pack input layout: {input_layout}") + total_rows = sum( + range_.size() for peer_ranges in ranges_by_peer for range_ in peer_ranges + ) + if total_rows == 0: + return left_tensor.new_empty((0, *left_tensor.shape[1:])) + packed = left_tensor.new_empty((total_rows * 2, *left_tensor.shape[1:])) + cursor = 0 + for peer_ranges in ranges_by_peer: + split = sum(range_.size() for range_ in peer_ranges) + if split <= 0: + continue + range_gather( + left_tensor, + peer_ranges, + output=packed[cursor : cursor + split], + range_meta_cache=range_meta_cache, + ) + range_gather( + right_tensor, + peer_ranges, + output=packed[cursor + split : cursor + split * 2], + range_meta_cache=range_meta_cache, + ) + cursor += split * 2 + return packed + + +def _pack_gathered_tensors_per_peer_head_major( + *, + left_tensor: torch.Tensor, + right_tensor: torch.Tensor, + ranges_by_peer: tuple[tuple[TokenRange, ...], ...], + range_meta_cache: dict[Any, Any] | None = None, +) -> torch.Tensor: + total_rows = sum( + range_.size() for peer_ranges in ranges_by_peer for range_ in peer_ranges + ) + if total_rows == 0: + return left_tensor.new_empty((0, left_tensor.shape[0], left_tensor.shape[2])) + packed = left_tensor.new_empty( + (total_rows * 2, left_tensor.shape[0], left_tensor.shape[2]) + ) + cursor = 0 + for peer_ranges in ranges_by_peer: + split = sum(range_.size() for range_ in peer_ranges) + if split <= 0: + continue + packed[cursor : cursor + split].copy_( + range_gather_head_major( + left_tensor, + peer_ranges, + range_meta_cache=range_meta_cache, + ).permute(1, 0, 2) + ) + packed[cursor + split : cursor + split * 2].copy_( + range_gather_head_major( + right_tensor, + peer_ranges, + range_meta_cache=range_meta_cache, + ).permute(1, 0, 2) + ) + cursor += split * 2 + return packed + + +def _pack_split_tensors_by_peer( + *, + left_tensor: torch.Tensor, + right_tensor: torch.Tensor, + splits: tuple[int, ...], + input_layout: str = "token_major", +) -> torch.Tensor: + if input_layout == "head_major": + return _pack_split_tensors_by_peer_head_major( + left_tensor=left_tensor, + right_tensor=right_tensor, + splits=splits, + ) + if input_layout != "token_major": + raise ValueError(f"Unsupported split-pack input layout: {input_layout}") + total_rows = int(sum(splits)) + if total_rows == 0: + return left_tensor.new_empty((0, *left_tensor.shape[1:])) + packed = left_tensor.new_empty((total_rows * 2, *left_tensor.shape[1:])) + cursor = 0 + for split in splits: + if split <= 0: + continue + packed[cursor * 2 : cursor * 2 + split].copy_( + left_tensor[cursor : cursor + split] + ) + packed[cursor * 2 + split : cursor * 2 + split * 2].copy_( + right_tensor[cursor : cursor + split] + ) + cursor += split + if cursor != int(left_tensor.shape[0]) or cursor != int(right_tensor.shape[0]): + raise RuntimeError( + "Packed split consumed the wrong number of rows: " + f"consumed={cursor}, left={int(left_tensor.shape[0])}, right={int(right_tensor.shape[0])}" + ) + return packed + + +def _packed_peer_tensor_shape( + *, + tensor: torch.Tensor, + total_rows: int, + input_layout: str, +) -> tuple[int, ...]: + if input_layout == "head_major": + return (total_rows * 2, int(tensor.shape[0]), int(tensor.shape[2])) + if input_layout != "token_major": + raise ValueError(f"Unsupported split-pack input layout: {input_layout}") + return (total_rows * 2, *tuple(int(dim) for dim in tensor.shape[1:])) + + +def _pack_split_tensors_by_peer_head_major( + *, + left_tensor: torch.Tensor, + right_tensor: torch.Tensor, + splits: tuple[int, ...], +) -> torch.Tensor: + total_rows = int(sum(splits)) + if total_rows == 0: + return left_tensor.new_empty((0, left_tensor.shape[0], left_tensor.shape[2])) + packed = left_tensor.new_empty( + (total_rows * 2, left_tensor.shape[0], left_tensor.shape[2]) + ) + cursor = 0 + for split in splits: + if split <= 0: + continue + packed[cursor * 2 : cursor * 2 + split].copy_( + left_tensor[:, cursor : cursor + split].permute(1, 0, 2) + ) + packed[cursor * 2 + split : cursor * 2 + split * 2].copy_( + right_tensor[:, cursor : cursor + split].permute(1, 0, 2) + ) + cursor += split + if cursor != int(left_tensor.shape[1]) or cursor != int(right_tensor.shape[1]): + raise RuntimeError( + "Head-major split pack consumed the wrong number of rows: " + f"consumed={cursor}, left={int(left_tensor.shape[1])}, right={int(right_tensor.shape[1])}" + ) + return packed + + +def _unpack_packed_tensor_per_peer( + packed_tensor: torch.Tensor, + splits: tuple[int, ...], + *, + output_layout: str = "token_major", +) -> tuple[torch.Tensor, torch.Tensor]: + if output_layout == "head_major": + return _unpack_packed_tensor_per_peer_head_major( + packed_tensor, + splits, + ) + if output_layout != "token_major": + raise ValueError(f"Unsupported packed-tensor output layout: {output_layout}") + if int(packed_tensor.shape[0]) == 0: + empty = packed_tensor.new_empty((0, *packed_tensor.shape[1:])) + return empty, empty + total_rows = 0 + cursor = 0 + for split in splits: + if split <= 0: + continue + cursor += split * 2 + total_rows += split + if cursor != int(packed_tensor.shape[0]): + raise RuntimeError( + "Packed tensor unpack consumed the wrong number of rows: " + f"consumed={cursor}, input={int(packed_tensor.shape[0])}" + ) + left = packed_tensor.new_empty((total_rows, *packed_tensor.shape[1:])) + right = packed_tensor.new_empty((total_rows, *packed_tensor.shape[1:])) + in_cursor = 0 + out_cursor = 0 + for split in splits: + if split <= 0: + continue + left[out_cursor : out_cursor + split].copy_( + packed_tensor[in_cursor : in_cursor + split] + ) + right[out_cursor : out_cursor + split].copy_( + packed_tensor[in_cursor + split : in_cursor + split * 2] + ) + in_cursor += split * 2 + out_cursor += split + return left, right + + +def _unpack_packed_tensor_per_peer_head_major( + packed_tensor: torch.Tensor, + splits: tuple[int, ...], +) -> tuple[torch.Tensor, torch.Tensor]: + if int(packed_tensor.shape[0]) == 0: + empty = packed_tensor.new_empty( + (packed_tensor.shape[1], 0, packed_tensor.shape[2]) + ) + return empty, empty + total_rows = 0 + cursor = 0 + for split in splits: + if split <= 0: + continue + cursor += split * 2 + total_rows += split + if cursor != int(packed_tensor.shape[0]): + raise RuntimeError( + "Packed tensor unpack consumed the wrong number of rows: " + f"consumed={cursor}, input={int(packed_tensor.shape[0])}" + ) + left = packed_tensor.new_empty( + (packed_tensor.shape[1], total_rows, packed_tensor.shape[2]) + ) + right = packed_tensor.new_empty( + (packed_tensor.shape[1], total_rows, packed_tensor.shape[2]) + ) + in_cursor = 0 + out_cursor = 0 + for split in splits: + if split <= 0: + continue + left[:, out_cursor : out_cursor + split].copy_( + packed_tensor[in_cursor : in_cursor + split].permute(1, 0, 2) + ) + right[:, out_cursor : out_cursor + split].copy_( + packed_tensor[in_cursor + split : in_cursor + split * 2].permute(1, 0, 2) + ) + in_cursor += split * 2 + out_cursor += split + return left, right diff --git a/src/art/megatron/context_parallel/core_attention.py b/src/art/megatron/context_parallel/core_attention.py new file mode 100644 index 000000000..8944878b7 --- /dev/null +++ b/src/art/megatron/context_parallel/core_attention.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import math +from typing import Any + +from megatron.core.packed_seq_params import PackedSeqParams +from megatron.core.process_groups_config import ProcessGroupCollection +from megatron.core.transformer.enums import AttnMaskType +from megatron.core.transformer.transformer_config import TransformerConfig +from megatron.core.utils import divide +import torch +from torch import Tensor +from torch.nn.attention.flex_attention import BlockMask + +from art.megatron.flex_attn.attention import ( + FlexAttentionWrapper, + SharedPrefixAttentionState, +) + +from .executor import run_context_parallel +from .types import ArtContextParallelState + + +class ArtContextParallelCoreAttention(torch.nn.Module): + def __init__( + self, + config: TransformerConfig, + layer_number: int, + attn_mask_type: AttnMaskType, + attention_type: str, + attention_dropout: float | None = None, + softmax_scale: float | None = None, + cp_comm_type: str | None = None, + pg_collection: ProcessGroupCollection | None = None, + ): + super().__init__() + del ( + layer_number, + attn_mask_type, + attention_type, + attention_dropout, + cp_comm_type, + ) + self.config = config + self.dense_kernel = FlexAttentionWrapper() + + if pg_collection is None: + tp_world_size = self.config.tensor_model_parallel_size + else: + tp_world_size = pg_collection.tp.size() + + kv_channels = self.config.kv_channels + assert kv_channels is not None, "Megatron config must provide kv_channels." + projection_size = kv_channels * self.config.num_attention_heads + self.hidden_size_per_partition = divide(projection_size, tp_world_size) + num_query_groups = ( + self.config.num_query_groups or self.config.num_attention_heads + ) + self.num_attention_heads_per_partition = divide( + self.config.num_attention_heads, + tp_world_size, + ) + self.num_query_groups_per_partition = divide(num_query_groups, tp_world_size) + + if softmax_scale is None: + head_dim = divide(projection_size, self.config.num_attention_heads) + self.softmax_scale = 1.0 / math.sqrt(head_dim) + else: + self.softmax_scale = softmax_scale + + def forward( + self, + query: Tensor, + key: Tensor, + value: Tensor, + attention_mask: Tensor, + attn_mask_type: AttnMaskType | None = None, + attention_bias: Any = None, + packed_seq_params: PackedSeqParams | None = None, + ) -> Tensor: + del attention_mask, attn_mask_type + assert packed_seq_params is None, ( + "PackedSeqParams is not used in the ART context parallel attention path." + ) + + if isinstance(attention_bias, ArtContextParallelState): + assert query.ndim == 4 and key.ndim == 4 and value.ndim == 4, ( + "ART context parallel attention expects [S, B, H, D] inputs." + ) + assert query.size(1) == 1 and key.size(1) == 1 and value.size(1) == 1, ( + "ART context parallel attention only supports exactly one packed sequence at a time." + ) + out = run_context_parallel( + query=query, + key=key, + value=value, + state=attention_bias, + scale=self.softmax_scale, + enable_gqa=self.num_attention_heads_per_partition + != self.num_query_groups_per_partition, + compile_enabled=True, + ) + else: + if isinstance(attention_bias, SharedPrefixAttentionState): + block_mask = attention_bias.block_mask + else: + assert isinstance(attention_bias, BlockMask), ( + "Expected ArtContextParallelState, SharedPrefixAttentionState, or BlockMask in attention_bias." + ) + block_mask = attention_bias + q = query.permute(1, 2, 0, 3) + k = key.permute(1, 2, 0, 3) + v = value.permute(1, 2, 0, 3) + out_dense = self.dense_kernel( + q, + k, + v, + block_mask=block_mask, + scale=self.softmax_scale, + enable_gqa=self.num_attention_heads_per_partition + != self.num_query_groups_per_partition, + ) + out = out_dense.permute(2, 0, 1, 3).contiguous() + + out = out.reshape(out.size(0), out.size(1), self.hidden_size_per_partition) + return out diff --git a/src/art/megatron/context_parallel/executor.py b/src/art/megatron/context_parallel/executor.py new file mode 100644 index 000000000..6590a739b --- /dev/null +++ b/src/art/megatron/context_parallel/executor.py @@ -0,0 +1,2230 @@ +from __future__ import annotations + +from typing import Any, cast + +import torch +from torch._dynamo import config as dynamo_config +import torch.distributed as dist +from torch.nn.attention.flex_attention import AuxOutput, AuxRequest, BlockMask +import triton +import triton.language as tl + +from art.megatron.flex_attn.compiled import ( + SparseBlockSize, + flash_sparse_block_size_for_head_dim, + get_sparse_compiled_flex_attention, + normalize_flex_lse, + normalize_sparse_block_size, + select_sparse_execution_family, + sparse_compiled_flex_attention, +) + +from .block_mask import build_block_mask +from .comm import A2AVCommunicator +from .range_ops import ( + range_gather_head_major, + range_reduce_sum_, + range_reduce_sum_head_major_, +) +from .types import ( + ArtContextParallelState, + AttnSlice, + DkvReducePlan, + ExactMaskMetadata, + FlexMaskSpec, + StageExecutionSpec, + StagePlan, + TokenRange, +) + +_COMMUNICATOR = A2AVCommunicator() +_DIST = cast(Any, dist) +_DYNAMO_CONFIG = cast(Any, dynamo_config) + +_DYNAMO_CONFIG.recompile_limit = max(int(_DYNAMO_CONFIG.recompile_limit), 256) +_DYNAMO_CONFIG.cache_size_limit = max(int(_DYNAMO_CONFIG.cache_size_limit), 256) +_STAGE_QUERY_GATHER_STREAMS: dict[tuple[str, int | None], torch.cuda.Stream] = {} + + +def _stage_sparse_block_size( + q_stage: torch.Tensor, + v_stage: torch.Tensor, +) -> tuple[int, int]: + return flash_sparse_block_size_for_head_dim( + head_dim=int(q_stage.shape[-1]), + head_dim_v=int(v_stage.shape[-1]), + device=q_stage.device, + ) + + +def _pad_exact_indices(indices: torch.Tensor, target_len: int) -> torch.Tensor: + current_len = int(indices.numel()) + target_len = int(target_len) + if current_len == target_len: + return indices + if current_len > target_len: + raise RuntimeError( + f"Cannot shrink exact mask metadata from {current_len} to {target_len}" + ) + pad = torch.full( + (target_len - current_len,), + -1, + dtype=indices.dtype, + device=indices.device, + ) + return torch.cat((indices, pad), dim=0) + + +def _resize_exact_mask_metadata( + metadata: ExactMaskMetadata | None, + *, + q_len: int, + k_len: int, +) -> ExactMaskMetadata | None: + if metadata is None: + return None + q_indices = _pad_exact_indices(metadata.q_token_indices, int(q_len)) + k_indices = _pad_exact_indices(metadata.k_token_indices, int(k_len)) + if q_indices is metadata.q_token_indices and k_indices is metadata.k_token_indices: + return metadata + return ExactMaskMetadata( + q_token_indices=q_indices, + k_token_indices=k_indices, + cache_key=f"{metadata.cache_key}:q{int(q_len)}:k{int(k_len)}", + ) + + +def _safe_logaddexp(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + out = torch.logaddexp(a, b) + both_neg_inf = torch.isneginf(a) & torch.isneginf(b) + return torch.where(both_neg_inf, torch.full_like(out, float("-inf")), out) + + +def _safe_exp_diff(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + diff = a - b + both_neg_inf = torch.isneginf(a) & torch.isneginf(b) + diff = torch.where(both_neg_inf, torch.full_like(diff, float("-inf")), diff) + return torch.exp(diff) + + +def _accum_output_dtype(input_dtype: torch.dtype) -> torch.dtype: + if input_dtype in {torch.float16, torch.bfloat16}: + return torch.float32 + return input_dtype + + +def _seed_stage_accumulators( + *, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, + target_dtype: torch.dtype, + needs_owned_storage: bool, +) -> tuple[torch.Tensor, torch.Tensor]: + if stage_out.dtype != target_dtype: + accum_out = stage_out.to(dtype=target_dtype) + else: + accum_out = stage_out.clone() if needs_owned_storage else stage_out + if stage_lse.dtype != target_dtype: + accum_lse = stage_lse.to(dtype=target_dtype) + else: + accum_lse = stage_lse.clone() if needs_owned_storage else stage_lse + return accum_out, accum_lse + + +def _stage_merge_values( + prev_out: torch.Tensor, + prev_lse: torch.Tensor, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + merged_lse = _safe_logaddexp(prev_lse, stage_lse) + prev_weight = _safe_exp_diff(prev_lse, merged_lse).unsqueeze(-1) + stage_weight = _safe_exp_diff(stage_lse, merged_lse).unsqueeze(-1) + merged_out = prev_weight * prev_out + stage_weight * stage_out + return merged_out, merged_lse + + +def _stage_merge_values_inplace( + prev_out: torch.Tensor, + prev_lse: torch.Tensor, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + merged_lse = _safe_logaddexp(prev_lse, stage_lse) + prev_weight = _safe_exp_diff(prev_lse, merged_lse).unsqueeze(-1) + stage_weight = _safe_exp_diff(stage_lse, merged_lse).unsqueeze(-1) + prev_out.mul_(prev_weight) + prev_out.add_(stage_out * stage_weight) + prev_lse.copy_(merged_lse) + return prev_out, prev_lse + + +@triton.jit +def _stage_merge_backward_row_kernel( + prev_out_ptr, + prev_lse_ptr, + stage_out_ptr, + stage_lse_ptr, + grad_merged_out_ptr, + grad_merged_lse_ptr, + grad_prev_out_ptr, + grad_prev_lse_ptr, + grad_stage_out_ptr, + grad_stage_lse_ptr, + row_stride, + lse_stride, + d: tl.constexpr, + block_d: tl.constexpr, +): + row = tl.program_id(0) + cols = tl.arange(0, block_d) + mask = cols < d + out_offsets = row * row_stride + cols + lse_offset = row * lse_stride + + prev_out = tl.load(prev_out_ptr + out_offsets, mask=mask, other=0.0) + stage_out = tl.load(stage_out_ptr + out_offsets, mask=mask, other=0.0) + grad_merged_out = tl.load(grad_merged_out_ptr + out_offsets, mask=mask, other=0.0) + + neg_inf = float("-inf") + prev_lse = tl.load(prev_lse_ptr + lse_offset) + stage_lse = tl.load(stage_lse_ptr + lse_offset) + grad_merged_lse = tl.load(grad_merged_lse_ptr + lse_offset) + + both_neg_inf = (prev_lse == neg_inf) & (stage_lse == neg_inf) + max_lse = tl.maximum(prev_lse, stage_lse) + merged_lse = max_lse + tl.log( + tl.exp(prev_lse - max_lse) + tl.exp(stage_lse - max_lse) + ) + merged_lse = tl.where(both_neg_inf, neg_inf, merged_lse) + + prev_diff = tl.where( + (prev_lse == neg_inf) & (merged_lse == neg_inf), + neg_inf, + prev_lse - merged_lse, + ) + stage_diff = tl.where( + (stage_lse == neg_inf) & (merged_lse == neg_inf), + neg_inf, + stage_lse - merged_lse, + ) + prev_weight = tl.exp(prev_diff) + stage_weight = tl.exp(stage_diff) + + delta = tl.sum((grad_merged_out * (stage_out - prev_out)).to(tl.float32), axis=0) + lse_delta = delta * (prev_weight * stage_weight) + + tl.store( + grad_prev_out_ptr + out_offsets, + grad_merged_out * prev_weight, + mask=mask, + ) + tl.store( + grad_stage_out_ptr + out_offsets, + grad_merged_out * stage_weight, + mask=mask, + ) + tl.store(grad_prev_lse_ptr + lse_offset, grad_merged_lse * prev_weight - lse_delta) + tl.store( + grad_stage_lse_ptr + lse_offset, + grad_merged_lse * stage_weight + lse_delta, + ) + + +def _stage_merge_backward_values_triton( + *, + prev_out: torch.Tensor, + prev_lse: torch.Tensor, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, + grad_merged_out: torch.Tensor, + grad_merged_lse: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor] | None: + if not ( + prev_out.is_cuda + and prev_lse.is_cuda + and stage_out.is_cuda + and stage_lse.is_cuda + and grad_merged_out.is_cuda + and grad_merged_lse.is_cuda + ): + return None + if not ( + prev_out.is_contiguous() + and prev_lse.is_contiguous() + and stage_out.is_contiguous() + and stage_lse.is_contiguous() + and grad_merged_out.is_contiguous() + and grad_merged_lse.is_contiguous() + ): + return None + if prev_out.ndim != 3 or prev_lse.ndim != 2: + return None + if prev_out.shape != stage_out.shape or prev_out.shape != grad_merged_out.shape: + return None + if prev_lse.shape != stage_lse.shape or prev_lse.shape != grad_merged_lse.shape: + return None + if prev_out.shape[:2] != prev_lse.shape: + return None + d = int(prev_out.shape[-1]) + if d <= 0 or d > 256: + return None + block_d = 1 << max(0, int((d - 1).bit_length())) + + prev_out_rows = prev_out.reshape(-1, d) + stage_out_rows = stage_out.reshape(-1, d) + grad_merged_out_rows = grad_merged_out.reshape(-1, d) + prev_lse_rows = prev_lse.reshape(-1) + stage_lse_rows = stage_lse.reshape(-1) + grad_merged_lse_rows = grad_merged_lse.reshape(-1) + + grad_prev_out = torch.empty_like(prev_out_rows) + grad_stage_out = torch.empty_like(stage_out_rows) + grad_prev_lse = torch.empty_like(prev_lse_rows) + grad_stage_lse = torch.empty_like(stage_lse_rows) + _stage_merge_backward_row_kernel[(prev_out_rows.shape[0],)]( + prev_out_rows, + prev_lse_rows, + stage_out_rows, + stage_lse_rows, + grad_merged_out_rows, + grad_merged_lse_rows, + grad_prev_out, + grad_prev_lse, + grad_stage_out, + grad_stage_lse, + prev_out_rows.stride(0), + prev_lse_rows.stride(0), + d=d, # ty: ignore[invalid-argument-type] + block_d=block_d, # ty: ignore[invalid-argument-type] + num_warps=4, # ty: ignore[unknown-argument] + num_stages=2, # ty: ignore[unknown-argument] + ) + return ( + grad_prev_out.view_as(prev_out), + grad_prev_lse.view_as(prev_lse), + grad_stage_out.view_as(stage_out), + grad_stage_lse.view_as(stage_lse), + ) + + +def _allocate_stage_accumulators( + *, + q_flat: torch.Tensor, + out_dtype: torch.dtype, + lse_dtype: torch.dtype, +) -> tuple[torch.Tensor, torch.Tensor]: + return ( + torch.zeros(q_flat.shape, device=q_flat.device, dtype=out_dtype), + torch.full( + (q_flat.shape[0], q_flat.shape[1]), + float("-inf"), + device=q_flat.device, + dtype=lse_dtype, + ), + ) + + +def _maybe_promote_accumulators( + *, + accum_out: torch.Tensor, + accum_lse: torch.Tensor, + target_dtype: torch.dtype, +) -> tuple[torch.Tensor, torch.Tensor]: + if accum_out.dtype == target_dtype and accum_lse.dtype == target_dtype: + return accum_out, accum_lse + return accum_out.to(dtype=target_dtype), accum_lse.to(dtype=target_dtype) + + +def _stage_merge_backward_values( + *, + prev_out: torch.Tensor, + prev_lse: torch.Tensor, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, + grad_merged_out: torch.Tensor | None, + grad_merged_lse: torch.Tensor | None, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + if grad_merged_out is None: + grad_merged_out = torch.zeros_like(prev_out) + if grad_merged_lse is None: + grad_merged_lse = torch.zeros_like(prev_lse) + triton_result = _stage_merge_backward_values_triton( + prev_out=prev_out, + prev_lse=prev_lse, + stage_out=stage_out, + stage_lse=stage_lse, + grad_merged_out=grad_merged_out, + grad_merged_lse=grad_merged_lse, + ) + if triton_result is not None: + return triton_result + merged_lse = _safe_logaddexp(prev_lse, stage_lse) + prev_weight = _safe_exp_diff(prev_lse, merged_lse) + stage_weight = _safe_exp_diff(stage_lse, merged_lse) + lse_delta = (grad_merged_out * (stage_out - prev_out)).sum(dim=-1) * ( + prev_weight * stage_weight + ) + return ( + grad_merged_out * prev_weight.unsqueeze(-1), + grad_merged_lse * prev_weight - lse_delta, + grad_merged_out * stage_weight.unsqueeze(-1), + grad_merged_lse * stage_weight + lse_delta, + ) + + +class _StageMergeFn(torch.autograd.Function): + @staticmethod + def forward( + ctx, + prev_out: torch.Tensor, + prev_lse: torch.Tensor, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, + ) -> tuple[torch.Tensor, torch.Tensor]: + ctx.save_for_backward(prev_out, prev_lse, stage_out, stage_lse) + return _stage_merge_values(prev_out, prev_lse, stage_out, stage_lse) + + @staticmethod + def backward( + ctx, + *grad_outputs: Any, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + grad_merged_out, grad_merged_lse = cast( + tuple[torch.Tensor | None, torch.Tensor | None], + grad_outputs, + ) + prev_out, prev_lse, stage_out, stage_lse = ctx.saved_tensors + return _stage_merge_backward_values( + prev_out=prev_out, + prev_lse=prev_lse, + stage_out=stage_out, + stage_lse=stage_lse, + grad_merged_out=grad_merged_out, + grad_merged_lse=grad_merged_lse, + ) + + +class _StageScatterMergeFn(torch.autograd.Function): + @staticmethod + def forward( + ctx, + accum_out: torch.Tensor, + accum_lse: torch.Tensor, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, + q_index: torch.Tensor, + index_dim: int, + ) -> tuple[torch.Tensor, torch.Tensor]: + prev_out = torch.index_select(accum_out, index_dim, q_index) + prev_lse = torch.index_select(accum_lse, index_dim, q_index) + merged_out, merged_lse = _stage_merge_values( + prev_out, + prev_lse, + stage_out, + stage_lse, + ) + ctx.save_for_backward(prev_out, prev_lse, stage_out, stage_lse, q_index) + ctx.index_dim = int(index_dim) + ctx.accum_out_shape = tuple(accum_out.shape) + ctx.accum_lse_shape = tuple(accum_lse.shape) + ctx.accum_out_dtype = accum_out.dtype + ctx.accum_lse_dtype = accum_lse.dtype + ctx.accum_device = accum_out.device + ctx.mark_dirty(accum_out, accum_lse) + accum_out.index_copy_(ctx.index_dim, q_index, merged_out) + accum_lse.index_copy_(ctx.index_dim, q_index, merged_lse) + return accum_out, accum_lse + + @staticmethod + def backward( + ctx, + *grad_outputs: Any, + ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, None, None]: + grad_updated_out, grad_updated_lse = cast( + tuple[torch.Tensor | None, torch.Tensor | None], + grad_outputs, + ) + prev_out, prev_lse, stage_out, stage_lse, q_index = ctx.saved_tensors + if grad_updated_out is None: + grad_updated_out = torch.zeros( + ctx.accum_out_shape, + device=ctx.accum_device, + dtype=ctx.accum_out_dtype, + ) + if grad_updated_lse is None: + grad_updated_lse = torch.zeros( + ctx.accum_lse_shape, + device=ctx.accum_device, + dtype=ctx.accum_lse_dtype, + ) + grad_merged_out = torch.index_select(grad_updated_out, ctx.index_dim, q_index) + grad_merged_lse = torch.index_select(grad_updated_lse, ctx.index_dim, q_index) + grad_prev_out, grad_prev_lse, grad_stage_out, grad_stage_lse = ( + _stage_merge_backward_values( + prev_out=prev_out, + prev_lse=prev_lse, + stage_out=stage_out, + stage_lse=stage_lse, + grad_merged_out=grad_merged_out, + grad_merged_lse=grad_merged_lse, + ) + ) + grad_accum_out = grad_updated_out.clone() + grad_accum_out.index_copy_(ctx.index_dim, q_index, grad_prev_out) + grad_accum_lse = grad_updated_lse.clone() + grad_accum_lse.index_copy_(ctx.index_dim, q_index, grad_prev_lse) + return ( + grad_accum_out, + grad_accum_lse, + grad_stage_out, + grad_stage_lse, + None, + None, + ) + + +def flatten_valid_sequence( + tensor: torch.Tensor, + valid_lengths: tuple[int, ...], +) -> torch.Tensor: + if tensor.ndim != 4: + raise RuntimeError(f"Expected [S, B, H, D] tensor, got {tuple(tensor.shape)}") + if len(valid_lengths) != 1 or int(tensor.shape[1]) != 1: + raise RuntimeError( + "ART context parallel attention only supports exactly one packed sequence in the hot path, " + f"got valid_lengths={valid_lengths} and batch={int(tensor.shape[1])}." + ) + valid_len = int(valid_lengths[0]) + if valid_len <= 0: + return tensor.new_empty((0, tensor.shape[2], tensor.shape[3])) + return tensor[:valid_len, 0].contiguous() + + +def flatten_valid_sequence_head_major( + tensor: torch.Tensor, + valid_lengths: tuple[int, ...], +) -> torch.Tensor: + if tensor.ndim != 4: + raise RuntimeError(f"Expected [S, B, H, D] tensor, got {tuple(tensor.shape)}") + if len(valid_lengths) != 1 or int(tensor.shape[1]) != 1: + raise RuntimeError( + "ART context parallel attention only supports exactly one packed sequence in the hot path, " + f"got valid_lengths={valid_lengths} and batch={int(tensor.shape[1])}." + ) + valid_len = int(valid_lengths[0]) + if valid_len <= 0: + return tensor.new_empty((tensor.shape[2], 0, tensor.shape[3])) + return tensor[:valid_len, 0].permute(1, 0, 2) + + +def unflatten_valid_sequence( + flat: torch.Tensor, + *, + valid_lengths: tuple[int, ...], + seq_len: int, +) -> torch.Tensor: + if flat.ndim != 3: + raise RuntimeError(f"Expected [N, H, D] flat tensor, got {tuple(flat.shape)}") + if len(valid_lengths) != 1: + raise RuntimeError( + "ART context parallel attention only supports exactly one packed sequence in the hot path, " + f"got valid_lengths={valid_lengths}." + ) + valid_len = int(valid_lengths[0]) + if int(flat.shape[0]) != valid_len: + raise RuntimeError( + "unflatten_valid_sequence expected flat rows to match valid length: " + f"{int(flat.shape[0])} != {valid_len}" + ) + if valid_len == seq_len: + return flat.unsqueeze(1).contiguous() + output = flat.new_zeros((seq_len, 1, flat.shape[1], flat.shape[2])) + if valid_len > 0: + output[:valid_len, 0] = flat + return output + + +def unflatten_valid_sequence_head_major( + flat: torch.Tensor, + *, + valid_lengths: tuple[int, ...], + seq_len: int, +) -> torch.Tensor: + if flat.ndim != 3: + raise RuntimeError( + f"Expected [H, N, D] head-major flat tensor, got {tuple(flat.shape)}" + ) + if len(valid_lengths) != 1: + raise RuntimeError( + "ART context parallel attention only supports exactly one packed sequence in the hot path, " + f"got valid_lengths={valid_lengths}." + ) + valid_len = int(valid_lengths[0]) + if int(flat.shape[1]) != valid_len: + raise RuntimeError( + "unflatten_valid_sequence_head_major expected flat token dim to match valid length: " + f"{int(flat.shape[1])} != {valid_len}" + ) + token_major = flat.permute(1, 0, 2) + if valid_len == seq_len: + return token_major.unsqueeze(1) + return unflatten_valid_sequence( + token_major, + valid_lengths=valid_lengths, + seq_len=seq_len, + ) + + +class FlexAttentionKernel: + def __init__(self, *, compile_enabled: bool) -> None: + if not compile_enabled: + raise RuntimeError( + "ART context parallel attention requires compiled flex attention." + ) + + def run( + self, + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + is_local_stage: bool = True, + compile_key: str | None = None, + block_mask: BlockMask | None, + scale: float, + enable_gqa: bool, + ) -> tuple[torch.Tensor, torch.Tensor]: + if not ( + q.dtype.is_floating_point + and k.dtype.is_floating_point + and v.dtype.is_floating_point + ): + raise RuntimeError( + "ART context parallel attention requires floating-point inputs for compiled flex attention, " + f"got q={q.dtype}, k={k.dtype}, v={v.dtype}." + ) + if block_mask is None: + raise RuntimeError( + "ART context parallel attention requires a concrete block mask for compiled flex attention." + ) + if compile_key is None: + _q_len, _k_len, compile_key = select_sparse_execution_family( + is_local_stage=bool(is_local_stage), + q_len=int(q.shape[2]), + k_len=int(k.shape[2]), + block_size=block_mask.BLOCK_SIZE, + ) + compiled_flex_attention = ( + sparse_compiled_flex_attention + if str(compile_key) == "sparse" + else get_sparse_compiled_flex_attention( + family_key=str(compile_key), + ) + ) + out, aux = cast( + tuple[torch.Tensor, AuxOutput], + compiled_flex_attention( + q, + k, + v, + block_mask=block_mask, + scale=scale, + enable_gqa=enable_gqa, + return_aux=AuxRequest(lse=True), + ), + ) + lse = aux.lse + if lse is None: + raise RuntimeError("Compiled flex attention did not return lse.") + lse = normalize_flex_lse(lse) + return out, lse + + +def _build_stage_block_mask( + *, + stage_plan: StagePlan, + state: ArtContextParallelState, + device: torch.device, + execution_spec: StageExecutionSpec | None = None, + block_size: SparseBlockSize | None = None, +) -> BlockMask | None: + resolved_block_size = normalize_sparse_block_size( + state.config.block_size if block_size is None else block_size + ) + execution_spec = ( + _resolve_stage_execution_spec( + stage_plan=stage_plan, + state=state, + block_size=resolved_block_size, + ) + if execution_spec is None + else execution_spec + ) + cache_key = ( + int(stage_plan.stage_index), + int(execution_spec.q_len), + int(execution_spec.k_len), + resolved_block_size, + device.type, + device.index, + ) + cache = state.execution_cache.block_masks + cached = cache.get(cache_key) + if cached is not None or cache_key in cache: + return cached + mask_metadata = ( + stage_plan.mask_metadata + if execution_spec.mask_metadata is None + else execution_spec.mask_metadata + ) + if mask_metadata is None: + raise RuntimeError( + f"Stage {stage_plan.stage_index} is missing exact mask metadata" + ) + mask = build_block_mask( + FlexMaskSpec( + q_len=int(execution_spec.q_len), + k_len=int(execution_spec.k_len), + block_size=resolved_block_size, + slices=stage_plan.slices, + exact_mask=mask_metadata.model_dump(mode="python"), + ), + group_ids=state.group_ids, + parent_ids=state.parent_ids, + device=device, + ) + cache[cache_key] = mask + return mask + + +def prepare_context_parallel_execution_state( + *, + state: ArtContextParallelState, + device: torch.device, +) -> None: + for stage_plan in state.rank_plan.stage_plans: + if stage_plan.q_len <= 0 or stage_plan.k_len <= 0 or not stage_plan.slices: + continue + execution_spec = _resolve_stage_execution_spec( + stage_plan=stage_plan, + state=state, + ) + _build_stage_block_mask( + stage_plan=stage_plan, + state=state, + device=device, + execution_spec=execution_spec, + block_size=state.config.block_size, + ) + + +def _causal_slice_pair_count(slice_: AttnSlice) -> int: + q_start = int(slice_.q_range.start) + q_end = int(slice_.q_range.end) + k_start = int(slice_.k_range.start) + k_end = int(slice_.k_range.end) + if q_end <= q_start or k_end <= k_start: + return 0 + + k_len = k_end - k_start + partial_q_start = max(q_start, k_start) + partial_q_end = min(q_end - 1, k_end - 2) + partial = 0 + if partial_q_start <= partial_q_end: + count = partial_q_end - partial_q_start + 1 + partial = count * (partial_q_start + partial_q_end + 2 - 2 * k_start) // 2 + + full_q_start = max(q_start, k_end - 1) + full_q_end = q_end - 1 + full = 0 + if full_q_start <= full_q_end: + full = (full_q_end - full_q_start + 1) * k_len + return int(partial + full) + + +def _validate_stage_block_alignment( + *, + q_len: int, + k_len: int, + block_mask: BlockMask, +) -> None: + q_block, k_block = normalize_sparse_block_size(block_mask.BLOCK_SIZE) + if q_len <= 0 or k_len <= 0: + return + if (q_len % q_block) != 0 or (k_len % k_block) != 0: + raise RuntimeError( + "ART context parallel attention requires block-aligned stage shapes, " + f"got q_len={q_len} k_len={k_len} " + f"with block_size=({q_block}, {k_block})" + ) + + +def _logical_stage_q_len(stage_plan: StagePlan) -> int: + return int(sum(range_.size() for range_ in stage_plan.owner_local_q_ranges)) + + +def _logical_stage_k_len(stage_plan: StagePlan) -> int: + return int(sum(range_.size() for range_ in stage_plan.owner_local_k_ranges)) + + +def _pad_stage_token_tensor( + tensor: torch.Tensor, + *, + target_len: int, + head_major: bool = False, +) -> torch.Tensor: + current_len = int(tensor.shape[1] if head_major else tensor.shape[0]) + if current_len == target_len: + return tensor + if current_len > target_len: + raise RuntimeError( + f"Cannot shrink stage tensor from {current_len} to {target_len} rows" + ) + pad_shape = list(tensor.shape) + pad_shape[1 if head_major else 0] = target_len - current_len + pad = torch.zeros(pad_shape, dtype=tensor.dtype, device=tensor.device) + dim = 1 if head_major else 0 + return torch.cat((tensor, pad), dim=dim) + + +def _resolve_stage_execution_spec( + *, + stage_plan: StagePlan, + state: ArtContextParallelState, + block_size: SparseBlockSize | None = None, +) -> StageExecutionSpec: + resolved_block_size = normalize_sparse_block_size( + state.config.block_size if block_size is None else block_size + ) + cache_key = (int(stage_plan.stage_index), resolved_block_size) + execution_cache = getattr(state, "execution_cache", None) + if execution_cache is None: + target_q_len, target_k_len, compile_key = select_sparse_execution_family( + is_local_stage=bool(stage_plan.is_local_stage), + q_len=int(stage_plan.q_len), + k_len=int(stage_plan.k_len), + block_size=resolved_block_size, + ) + return StageExecutionSpec( + q_len=int(target_q_len), + k_len=int(target_k_len), + compile_key=str(compile_key), + mask_metadata=_resize_exact_mask_metadata( + stage_plan.mask_metadata, + q_len=int(target_q_len), + k_len=int(target_k_len), + ), + ) + cache = getattr(execution_cache, "stage_execution_specs", None) + if cache is None: + target_q_len, target_k_len, compile_key = select_sparse_execution_family( + is_local_stage=bool(stage_plan.is_local_stage), + q_len=int(stage_plan.q_len), + k_len=int(stage_plan.k_len), + block_size=resolved_block_size, + ) + return StageExecutionSpec( + q_len=int(target_q_len), + k_len=int(target_k_len), + compile_key=str(compile_key), + mask_metadata=_resize_exact_mask_metadata( + stage_plan.mask_metadata, + q_len=int(target_q_len), + k_len=int(target_k_len), + ), + ) + cached = cache.get(cache_key) + if cached is not None: + return cached + target_q_len, target_k_len, compile_key = select_sparse_execution_family( + is_local_stage=bool(stage_plan.is_local_stage), + q_len=int(stage_plan.q_len), + k_len=int(stage_plan.k_len), + block_size=resolved_block_size, + ) + resolved = StageExecutionSpec( + q_len=int(target_q_len), + k_len=int(target_k_len), + compile_key=str(compile_key), + mask_metadata=_resize_exact_mask_metadata( + stage_plan.mask_metadata, + q_len=int(target_q_len), + k_len=int(target_k_len), + ), + ) + cache[cache_key] = resolved + return resolved + + +def _run_stage_attention( + *, + q_stage: torch.Tensor, + k_stage: torch.Tensor, + v_stage: torch.Tensor, + stage_plan: StagePlan, + state: ArtContextParallelState, + kernel: FlexAttentionKernel, + scale: float, + enable_gqa: bool, +) -> tuple[torch.Tensor, torch.Tensor]: + sparse_block_size = _stage_sparse_block_size(q_stage, v_stage) + execution_spec = _resolve_stage_execution_spec( + stage_plan=stage_plan, + state=state, + block_size=sparse_block_size, + ) + block_mask = _build_stage_block_mask( + stage_plan=stage_plan, + state=state, + device=q_stage.device, + execution_spec=execution_spec, + block_size=sparse_block_size, + ) + if block_mask is None: + raise RuntimeError( + f"Stage {stage_plan.stage_index} unexpectedly produced an empty block mask" + ) + _validate_stage_block_alignment( + q_len=int(execution_spec.q_len), + k_len=int(execution_spec.k_len), + block_mask=block_mask, + ) + logical_q_len = _logical_stage_q_len(stage_plan) + input_head_major = q_stage.ndim == 3 and int(q_stage.shape[1]) == logical_q_len + q_stage = _pad_stage_token_tensor( + q_stage, + target_len=int(execution_spec.q_len), + head_major=input_head_major, + ) + k_stage = _pad_stage_token_tensor( + k_stage, + target_len=int(execution_spec.k_len), + head_major=input_head_major, + ) + v_stage = _pad_stage_token_tensor( + v_stage, + target_len=int(execution_spec.k_len), + head_major=input_head_major, + ) + if input_head_major: + q_flex = q_stage.unsqueeze(0) + k_flex = k_stage.unsqueeze(0) + v_flex = v_stage.unsqueeze(0) + else: + q_flex = q_stage.permute(1, 0, 2).unsqueeze(0).contiguous() + k_flex = k_stage.permute(1, 0, 2).unsqueeze(0).contiguous() + v_flex = v_stage.permute(1, 0, 2).unsqueeze(0).contiguous() + out, lse = kernel.run( + q_flex, + k_flex, + v_flex, + block_mask=block_mask, + scale=scale, + enable_gqa=enable_gqa, + ) + if input_head_major: + out_tokens = out.squeeze(0) + lse_tokens = lse.squeeze(0).to(dtype=torch.float32) + return ( + out_tokens[:, :logical_q_len] + if int(execution_spec.q_len) == logical_q_len + else out_tokens[:, :logical_q_len].contiguous(), + lse_tokens[:, :logical_q_len] + if int(execution_spec.q_len) == logical_q_len + else lse_tokens[:, :logical_q_len].contiguous(), + ) + out_tokens = out.squeeze(0).permute(1, 0, 2).contiguous() + lse_tokens = lse.squeeze(0).permute(1, 0).contiguous().to(dtype=torch.float32) + return ( + out_tokens[:logical_q_len].contiguous(), + lse_tokens[:logical_q_len].contiguous(), + ) + + +def _range_index_tensor( + ranges: tuple, + *, + device: torch.device, + range_index_cache: dict[Any, torch.Tensor] | None = None, +) -> torch.Tensor: + key = ( + tuple((range_.start, range_.end) for range_ in ranges if range_.size() > 0), + device.type, + device.index, + ) + if range_index_cache is not None: + cached = range_index_cache.get(key) + if cached is not None: + return cached + parts = [ + torch.arange(range_.start, range_.end, device=device, dtype=torch.int64) + for range_ in ranges + if range_.size() > 0 + ] + if not parts: + cached = torch.empty((0,), device=device, dtype=torch.int64) + else: + cached = torch.cat(parts, dim=0) + if range_index_cache is not None: + range_index_cache[key] = cached + return cached + + +def _ranges_cover_full_length( + ranges: tuple, + *, + length: int, +) -> bool: + cursor = 0 + for range_ in ranges: + if range_.size() <= 0: + continue + if range_.start != cursor: + return False + cursor = range_.end + return cursor == length + + +def _ordered_stage_plans(stage_plans: tuple[StagePlan, ...]) -> list[StagePlan]: + return sorted( + stage_plans, + key=lambda stage_plan: (not stage_plan.is_local_stage, stage_plan.stage_index), + ) + + +def _stage_q_is_full( + *, + stage_plan: StagePlan, + q_flat_len: int, +) -> bool: + return _ranges_cover_full_length( + stage_plan.owner_local_q_ranges, + length=q_flat_len, + ) + + +def _stage_requires_reduce(stage_plan: StagePlan) -> bool: + if stage_plan.dkv_reduce_plan is None: + return False + return bool( + sum(stage_plan.dkv_reduce_plan.send_splits) + or sum(stage_plan.dkv_reduce_plan.recv_splits) + ) + + +def _distributed_cp_comm_enabled(state: ArtContextParallelState) -> bool: + return state.cp_group is not None and _DIST.get_world_size(state.cp_group) > 1 + + +def _remote_comm_launch_enabled( + *, + state: ArtContextParallelState, + remote_stages: list[StagePlan], +) -> bool: + if not remote_stages: + return False + if not _distributed_cp_comm_enabled(state): + raise RuntimeError( + "ART context parallel remote stages require distributed async per-stage KV fetch." + ) + return True + + +def _ready_remote_stage_batch( + *, + pending_stages: list[StagePlan], + fetch_works_by_stage_index: dict[int, Any], +) -> list[StagePlan]: + ready_stages: list[StagePlan] = [] + for stage_plan in pending_stages: + fetch_work = fetch_works_by_stage_index.get(int(stage_plan.stage_index)) + if fetch_work is None or fetch_work.is_completed(): + ready_stages.append(stage_plan) + if ready_stages: + return ready_stages + if not pending_stages: + return [] + fetch_work = fetch_works_by_stage_index.get(int(pending_stages[0].stage_index)) + if fetch_work is None: + return [pending_stages[0]] + fetch_work.wait() + ready_stages = [] + for stage_plan in pending_stages: + fetch_work = fetch_works_by_stage_index.get(int(stage_plan.stage_index)) + if fetch_work is None or fetch_work.is_completed(): + ready_stages.append(stage_plan) + return ready_stages + + +def _drain_launched_remote_fetch_works( + *, + fetch_works_by_stage_index: dict[int, Any], +) -> None: + for fetch_work in fetch_works_by_stage_index.values(): + if fetch_work is not None: + fetch_work.wait() + + +class _StageQueryGatherWork: + def __init__( + self, + *, + gathered_q: torch.Tensor, + stream: torch.cuda.Stream | None, + ) -> None: + self.gathered_q = gathered_q + self.stream = stream + + def wait_post_process(self) -> torch.Tensor: + if self.stream is not None: + torch.cuda.current_stream(self.gathered_q.device).wait_stream(self.stream) + return self.gathered_q + + +def _get_stage_query_gather_stream(tensor: torch.Tensor) -> torch.cuda.Stream | None: + if not tensor.is_cuda: + return None + key = (tensor.device.type, tensor.device.index) + stream = _STAGE_QUERY_GATHER_STREAMS.get(key) + if stream is None: + stream = torch.cuda.Stream(device=tensor.device) + _STAGE_QUERY_GATHER_STREAMS[key] = stream + return stream + + +def _launch_stage_query_gather( + *, + q_flat: torch.Tensor, + state: ArtContextParallelState, + stage_plan: StagePlan, +) -> _StageQueryGatherWork | None: + if stage_plan.q_len == 0: + return None + if _ranges_cover_full_length( + stage_plan.owner_local_q_ranges, + length=int(q_flat.shape[1]), + ): + return None + stream = _get_stage_query_gather_stream(q_flat) + if stream is None: + return None + gathered_q = q_flat.new_empty( + (q_flat.shape[0], _logical_stage_q_len(stage_plan), q_flat.shape[2]) + ) + current_stream = torch.cuda.current_stream(q_flat.device) + stream.wait_stream(current_stream) + q_flat.record_stream(stream) + gathered_q.record_stream(stream) + with torch.cuda.stream(stream): + range_gather_head_major( + q_flat, + stage_plan.owner_local_q_ranges, + output=gathered_q, + range_meta_cache=state.execution_cache.range_meta, + ) + return _StageQueryGatherWork(gathered_q=gathered_q, stream=stream) + + +def _stage_remote_kv_tensors( + *, + stage_plan: StagePlan, + fetch_work: Any, +) -> tuple[torch.Tensor, torch.Tensor, bool]: + if fetch_work is None: + raise RuntimeError( + f"Remote stage {stage_plan.stage_index} is missing async KV fetch work" + ) + output_layout = str(getattr(fetch_work, "output_layout", "head_major")) + if output_layout != "head_major": + raise RuntimeError( + "Remote stage KV fetch must land in head-major layout for flex attention, " + f"got output_layout={output_layout!r} for stage={stage_plan.stage_index}" + ) + k_stage, v_stage = fetch_work.wait_post_process() + k_rows = int(k_stage.shape[-2]) + v_rows = int(v_stage.shape[-2]) + expected_rows = _logical_stage_k_len(stage_plan) + if k_rows != expected_rows or v_rows != expected_rows: + raise RuntimeError( + "Remote stage fetch returned the wrong number of rows: " + f"stage={stage_plan.stage_index} expected={expected_rows} " + f"got_k={k_rows} got_v={v_rows}" + ) + return k_stage, v_stage, True + + +def _stage_query_tensor( + *, + q_flat: torch.Tensor, + state: ArtContextParallelState, + stage_plan: StagePlan, +) -> torch.Tensor: + if stage_plan.q_len == 0: + return q_flat.new_empty((q_flat.shape[0], 0, q_flat.shape[2])) + if _ranges_cover_full_length( + stage_plan.owner_local_q_ranges, + length=int(q_flat.shape[1]), + ): + return q_flat + return range_gather_head_major( + q_flat, + stage_plan.owner_local_q_ranges, + range_meta_cache=state.execution_cache.range_meta, + ) + + +def _stage_local_kv_tensors( + *, + k_flat: torch.Tensor, + v_flat: torch.Tensor, + state: ArtContextParallelState, + stage_plan: StagePlan, +) -> tuple[torch.Tensor, torch.Tensor]: + if stage_plan.k_len == 0: + empty = k_flat.new_empty((k_flat.shape[0], 0, k_flat.shape[2])) + return empty, empty + kv_is_full = _ranges_cover_full_length( + stage_plan.owner_local_k_ranges, + length=int(k_flat.shape[1]), + ) + if kv_is_full: + return k_flat, v_flat + return ( + range_gather_head_major( + k_flat, + stage_plan.owner_local_k_ranges, + range_meta_cache=state.execution_cache.range_meta, + ), + range_gather_head_major( + v_flat, + stage_plan.owner_local_k_ranges, + range_meta_cache=state.execution_cache.range_meta, + ), + ) + + +def _merge_stage_output( + *, + accum_out: torch.Tensor, + accum_lse: torch.Tensor, + stage_out: torch.Tensor, + stage_lse: torch.Tensor, + state: ArtContextParallelState, + stage_plan: StagePlan, + q_is_full: bool | None = None, + produced_output: bool, +) -> tuple[torch.Tensor, torch.Tensor]: + if q_is_full is None: + q_is_full = _ranges_cover_full_length( + stage_plan.owner_local_q_ranges, + length=int(accum_out.shape[1]), + ) + if not produced_output: + if q_is_full: + return stage_out, stage_lse + cursor = 0 + for range_ in stage_plan.owner_local_q_ranges: + size = range_.size() + if size <= 0: + continue + next_cursor = cursor + size + accum_out[:, range_.start : range_.end].copy_( + stage_out[:, cursor:next_cursor] + ) + accum_lse[:, range_.start : range_.end].copy_( + stage_lse[:, cursor:next_cursor] + ) + cursor = next_cursor + return accum_out, accum_lse + if q_is_full: + if ( + accum_out.requires_grad + or accum_lse.requires_grad + or stage_out.requires_grad + or stage_lse.requires_grad + ): + return _StageMergeFn.apply(accum_out, accum_lse, stage_out, stage_lse) + return _stage_merge_values_inplace( + accum_out, + accum_lse, + stage_out, + stage_lse, + ) + if ( + accum_out.requires_grad + or accum_lse.requires_grad + or stage_out.requires_grad + or stage_lse.requires_grad + ): + q_index = _range_index_tensor( + stage_plan.owner_local_q_ranges, + device=accum_out.device, + range_index_cache=state.execution_cache.range_indices, + ) + return _StageScatterMergeFn.apply( + accum_out, + accum_lse, + stage_out, + stage_lse, + q_index, + 1, + ) + cursor = 0 + for range_ in stage_plan.owner_local_q_ranges: + size = range_.size() + if size <= 0: + continue + next_cursor = cursor + size + _stage_merge_values_inplace( + accum_out[:, range_.start : range_.end], + accum_lse[:, range_.start : range_.end], + stage_out[:, cursor:next_cursor], + stage_lse[:, cursor:next_cursor], + ) + cursor = next_cursor + return accum_out, accum_lse + + +def _capture_stage_merge_tape( + *, + accum_out: torch.Tensor | None, + accum_lse: torch.Tensor | None, + q_flat_len: int, + device: torch.device, + state: ArtContextParallelState, + stage_plan: StagePlan, + produced_output: bool, +) -> dict[str, Any]: + q_is_full = _ranges_cover_full_length( + stage_plan.owner_local_q_ranges, + length=q_flat_len, + ) + q_index = None + if not q_is_full: + q_index = _range_index_tensor( + stage_plan.owner_local_q_ranges, + device=device, + range_index_cache=state.execution_cache.range_indices, + ) + if not produced_output: + return { + "merge_is_copy": True, + "merge_q_is_full": q_is_full, + "merge_q_index": q_index, + } + if accum_out is None or accum_lse is None: + raise RuntimeError("Missing merge accumulators for produced stage output") + if q_is_full: + prev_out = accum_out.detach().clone() + prev_lse = accum_lse.detach().clone() + else: + prev_out = torch.index_select(accum_out, 1, cast(torch.Tensor, q_index)) + prev_lse = torch.index_select(accum_lse, 1, cast(torch.Tensor, q_index)) + return { + "merge_is_copy": False, + "merge_q_is_full": q_is_full, + "merge_q_index": q_index, + "merge_prev_out": prev_out, + "merge_prev_lse": prev_lse, + } + + +def _release_replay_record_merge_tape(record: dict[str, Any]) -> None: + for key in ( + "merge_is_copy", + "merge_q_is_full", + "merge_q_index", + "merge_prev_out", + "merge_prev_lse", + ): + record.pop(key, None) + + +def _release_replay_record_tensors(record: dict[str, Any]) -> None: + _release_replay_record_merge_tape(record) + for key in ( + "q_input", + "k_input", + "v_input", + "stage_out", + "stage_lse", + ): + record.pop(key, None) + + +def _merge_stage_output_grads_from_tape( + *, + replay_records: list[dict[str, Any]], + grad_output_flat: torch.Tensor, +) -> tuple[list[torch.Tensor], list[torch.Tensor]]: + if not replay_records: + return [], [] + accum_dtype = _accum_output_dtype(grad_output_flat.dtype) + grad_accum_out = grad_output_flat.to( + dtype=accum_dtype, + memory_format=torch.contiguous_format, + ) + grad_accum_lse = torch.zeros( + (grad_output_flat.shape[0], grad_output_flat.shape[1]), + device=grad_output_flat.device, + dtype=accum_dtype, + ) + stage_out_grads: list[torch.Tensor] = [] + stage_lse_grads: list[torch.Tensor] = [] + for record in replay_records: + stage_out_grads.append( + torch.zeros_like(cast(torch.Tensor, record["stage_out"])) + ) + stage_lse_grads.append( + torch.zeros_like(cast(torch.Tensor, record["stage_lse"])) + ) + for record_index in range(len(replay_records) - 1, -1, -1): + record = replay_records[record_index] + q_index = cast(torch.Tensor | None, record.get("merge_q_index")) + if bool(record.get("merge_q_is_full", False)): + grad_merged_out = grad_accum_out + grad_merged_lse = grad_accum_lse + else: + if q_index is None: + raise RuntimeError("Missing stage q index for partial merge tape") + grad_merged_out = torch.index_select(grad_accum_out, 1, q_index) + grad_merged_lse = torch.index_select(grad_accum_lse, 1, q_index) + stage_out = cast(torch.Tensor, record["stage_out"]) + stage_lse = cast(torch.Tensor, record["stage_lse"]) + if bool(record.get("merge_is_copy", False)): + stage_out_grads[record_index] = grad_merged_out.to(dtype=stage_out.dtype) + stage_lse_grads[record_index] = grad_merged_lse.to(dtype=stage_lse.dtype) + _release_replay_record_merge_tape(record) + continue + prev_out = cast(torch.Tensor, record["merge_prev_out"]) + prev_lse = cast(torch.Tensor, record["merge_prev_lse"]) + grad_prev_out, grad_prev_lse, grad_stage_out, grad_stage_lse = ( + _stage_merge_backward_values( + prev_out=prev_out, + prev_lse=prev_lse, + stage_out=stage_out.detach().to(accum_dtype), + stage_lse=stage_lse.detach().to(accum_dtype), + grad_merged_out=grad_merged_out, + grad_merged_lse=grad_merged_lse, + ) + ) + stage_out_grads[record_index] = grad_stage_out.to(dtype=stage_out.dtype) + stage_lse_grads[record_index] = grad_stage_lse.to(dtype=stage_lse.dtype) + if bool(record.get("merge_q_is_full", False)): + grad_accum_out = grad_prev_out + grad_accum_lse = grad_prev_lse + continue + if q_index is None: + raise RuntimeError("Missing stage q index for partial merge tape") + grad_accum_out.index_copy_(1, q_index, grad_prev_out) + grad_accum_lse.index_copy_(1, q_index, grad_prev_lse) + _release_replay_record_merge_tape(record) + return stage_out_grads, stage_lse_grads + + +def _forward_stage_records( + *, + q_flat: torch.Tensor, + k_flat: torch.Tensor, + v_flat: torch.Tensor, + state: ArtContextParallelState, + kernel: FlexAttentionKernel, + scale: float, + enable_gqa: bool, + record_for_backward: bool, +) -> tuple[torch.Tensor, list[dict[str, Any]]]: + q_source = q_flat.detach() if record_for_backward else q_flat + k_source = k_flat.detach() if record_for_backward else k_flat + v_source = v_flat.detach() if record_for_backward else v_flat + + accum_dtype = _accum_output_dtype(q_flat.dtype) + accum_out: torch.Tensor | None = None + accum_lse: torch.Tensor | None = None + + ordered_stages = _ordered_stage_plans(state.rank_plan.stage_plans) + local_stage = next( + (stage for stage in ordered_stages if stage.is_local_stage), None + ) + remote_stages = [stage for stage in ordered_stages if not stage.is_local_stage] + wave_pipeline_enabled = _remote_comm_launch_enabled( + state=state, + remote_stages=remote_stages, + ) + replay_records: list[dict[str, Any]] = [] + produced_output = False + + remote_fetch_works_by_stage_index: dict[int, Any] = {} + remote_query_works_by_stage_index: dict[int, _StageQueryGatherWork] = {} + if wave_pipeline_enabled: + for stage_plan in remote_stages: + remote_fetch_works_by_stage_index[int(stage_plan.stage_index)] = ( + _COMMUNICATOR.launch_kv_fetch( + k_local=k_source, + v_local=v_source, + plan=cast(Any, stage_plan.kv_fetch_plan), + group=state.cp_group, + async_op=True, + range_meta_cache=state.execution_cache.range_meta, + label=( + f"kv_fetch.wave{stage_plan.wave_index}." + f"stage{stage_plan.stage_index}.src{stage_plan.source_rank}" + ), + input_layout="head_major", + output_layout="head_major", + ) + ) + query_work = _launch_stage_query_gather( + q_flat=q_source, + state=state, + stage_plan=stage_plan, + ) + if query_work is not None: + remote_query_works_by_stage_index[int(stage_plan.stage_index)] = ( + query_work + ) + pending_remote_stages = [ + stage_plan + for stage_plan in remote_stages + if stage_plan.q_len > 0 and stage_plan.k_len > 0 and stage_plan.slices + ] + + if ( + local_stage is not None + and local_stage.q_len > 0 + and local_stage.k_len > 0 + and local_stage.slices + ): + local_q_is_full = _stage_q_is_full( + stage_plan=local_stage, + q_flat_len=int(q_flat.shape[1]), + ) + q_stage = _stage_query_tensor( + q_flat=q_source, + state=state, + stage_plan=local_stage, + ) + k_stage, v_stage = _stage_local_kv_tensors( + k_flat=k_source, + v_flat=v_source, + state=state, + stage_plan=local_stage, + ) + if record_for_backward: + q_leaf = q_stage.detach().requires_grad_(bool(q_flat.requires_grad)) + k_leaf = k_stage.detach().requires_grad_(bool(k_flat.requires_grad)) + v_leaf = v_stage.detach().requires_grad_(bool(v_flat.requires_grad)) + else: + q_leaf = k_leaf = v_leaf = None + if record_for_backward: + stage_out, stage_lse = _run_stage_attention( + q_stage=cast(torch.Tensor, q_leaf), + k_stage=cast(torch.Tensor, k_leaf), + v_stage=cast(torch.Tensor, v_leaf), + stage_plan=local_stage, + state=state, + kernel=kernel, + scale=scale, + enable_gqa=enable_gqa, + ) + replay_records.append( + { + "stage_plan": local_stage, + "q_input": q_leaf, + "k_input": k_leaf, + "v_input": v_leaf, + "stage_out": stage_out, + "stage_lse": stage_lse, + } + ) + else: + stage_out, stage_lse = _run_stage_attention( + q_stage=q_stage, + k_stage=k_stage, + v_stage=v_stage, + stage_plan=local_stage, + state=state, + kernel=kernel, + scale=scale, + enable_gqa=enable_gqa, + ) + stage_out_value = stage_out.detach() if record_for_backward else stage_out + stage_lse_value = stage_lse.detach() if record_for_backward else stage_lse + if record_for_backward: + replay_records[-1].update( + _capture_stage_merge_tape( + accum_out=accum_out, + accum_lse=accum_lse, + q_flat_len=int(q_flat.shape[1]), + device=q_flat.device, + state=state, + stage_plan=local_stage, + produced_output=produced_output, + ) + ) + if not produced_output and local_q_is_full: + accum_out, accum_lse = _seed_stage_accumulators( + stage_out=stage_out_value, + stage_lse=stage_lse_value, + target_dtype=accum_dtype, + needs_owned_storage=bool(record_for_backward and pending_remote_stages), + ) + produced_output = True + else: + if not produced_output: + accum_out, accum_lse = _allocate_stage_accumulators( + q_flat=q_flat, + out_dtype=stage_out_value.dtype, + lse_dtype=stage_lse_value.dtype, + ) + else: + if accum_out is None or accum_lse is None: + raise RuntimeError("Missing accumulators before merge") + accum_out, accum_lse = _maybe_promote_accumulators( + accum_out=accum_out, + accum_lse=accum_lse, + target_dtype=accum_dtype, + ) + if accum_out is None or accum_lse is None: + raise RuntimeError("Missing accumulators for merge") + accum_out, accum_lse = _merge_stage_output( + accum_out=accum_out, + accum_lse=accum_lse, + stage_out=stage_out_value, + stage_lse=stage_lse_value, + state=state, + stage_plan=local_stage, + q_is_full=local_q_is_full, + produced_output=produced_output, + ) + produced_output = True + + while pending_remote_stages: + ready_stages = _ready_remote_stage_batch( + pending_stages=pending_remote_stages, + fetch_works_by_stage_index=remote_fetch_works_by_stage_index, + ) + if not ready_stages: + raise RuntimeError( + "Remote stage scheduler failed to produce a ready stage batch" + ) + ready_stage_indices = { + int(stage_plan.stage_index) for stage_plan in ready_stages + } + pending_remote_stages = [ + stage_plan + for stage_plan in pending_remote_stages + if int(stage_plan.stage_index) not in ready_stage_indices + ] + for ready_index, stage_plan in enumerate(ready_stages): + stage_q_is_full = _stage_q_is_full( + stage_plan=stage_plan, + q_flat_len=int(q_flat.shape[1]), + ) + stage_index = int(stage_plan.stage_index) + query_work = remote_query_works_by_stage_index.get(stage_index) + if query_work is None: + q_stage = _stage_query_tensor( + q_flat=q_source, + state=state, + stage_plan=stage_plan, + ) + else: + q_stage = query_work.wait_post_process() + remote_query_works_by_stage_index.pop(stage_index, None) + fetch_work = remote_fetch_works_by_stage_index.get(stage_index) + k_stage, v_stage, _kv_head_major = _stage_remote_kv_tensors( + stage_plan=stage_plan, + fetch_work=fetch_work, + ) + remote_fetch_works_by_stage_index.pop(stage_index, None) + if record_for_backward: + q_leaf = q_stage.detach().requires_grad_(bool(q_flat.requires_grad)) + k_leaf = k_stage.detach().requires_grad_(bool(k_flat.requires_grad)) + v_leaf = v_stage.detach().requires_grad_(bool(v_flat.requires_grad)) + else: + q_leaf = k_leaf = v_leaf = None + del query_work, fetch_work + if record_for_backward: + stage_out, stage_lse = _run_stage_attention( + q_stage=cast(torch.Tensor, q_leaf), + k_stage=cast(torch.Tensor, k_leaf), + v_stage=cast(torch.Tensor, v_leaf), + stage_plan=stage_plan, + state=state, + kernel=kernel, + scale=scale, + enable_gqa=enable_gqa, + ) + replay_records.append( + { + "stage_plan": stage_plan, + "q_input": q_leaf, + "k_input": k_leaf, + "v_input": v_leaf, + "stage_out": stage_out, + "stage_lse": stage_lse, + } + ) + else: + stage_out, stage_lse = _run_stage_attention( + q_stage=q_stage, + k_stage=k_stage, + v_stage=v_stage, + stage_plan=stage_plan, + state=state, + kernel=kernel, + scale=scale, + enable_gqa=enable_gqa, + ) + stage_out_value = stage_out.detach() if record_for_backward else stage_out + stage_lse_value = stage_lse.detach() if record_for_backward else stage_lse + del q_stage, k_stage, v_stage + if produced_output: + if accum_out is None or accum_lse is None: + raise RuntimeError("Missing accumulators before remote merge") + accum_out, accum_lse = _maybe_promote_accumulators( + accum_out=accum_out, + accum_lse=accum_lse, + target_dtype=accum_dtype, + ) + if record_for_backward: + replay_records[-1].update( + _capture_stage_merge_tape( + accum_out=accum_out, + accum_lse=accum_lse, + q_flat_len=int(q_flat.shape[1]), + device=q_flat.device, + state=state, + stage_plan=stage_plan, + produced_output=produced_output, + ) + ) + if not produced_output and stage_q_is_full: + accum_out, accum_lse = _seed_stage_accumulators( + stage_out=stage_out_value, + stage_lse=stage_lse_value, + target_dtype=accum_dtype, + needs_owned_storage=bool( + record_for_backward + and ( + pending_remote_stages or ready_index + 1 < len(ready_stages) + ) + ), + ) + produced_output = True + continue + if not produced_output: + accum_out, accum_lse = _allocate_stage_accumulators( + q_flat=q_flat, + out_dtype=stage_out_value.dtype, + lse_dtype=stage_lse_value.dtype, + ) + if accum_out is None or accum_lse is None: + raise RuntimeError("Missing accumulators for remote merge") + accum_out, accum_lse = _merge_stage_output( + accum_out=accum_out, + accum_lse=accum_lse, + stage_out=stage_out_value, + stage_lse=stage_lse_value, + state=state, + stage_plan=stage_plan, + q_is_full=stage_q_is_full, + produced_output=produced_output, + ) + produced_output = True + + _drain_launched_remote_fetch_works( + fetch_works_by_stage_index=remote_fetch_works_by_stage_index + ) + + if not produced_output: + if int(q_flat.shape[1]) == 0: + return q_flat.new_empty( + (q_flat.shape[0], 0, q_flat.shape[2]) + ), replay_records + raise RuntimeError("Sparse attention produced no stage outputs") + if accum_out is None: + raise RuntimeError("Sparse attention produced no accumulated output") + return accum_out, replay_records + + +def _flatten_qkv( + *, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + state: ArtContextParallelState, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + return ( + flatten_valid_sequence_head_major(query, state.rank_plan.local_valid_lengths), + flatten_valid_sequence_head_major(key, state.rank_plan.local_valid_lengths), + flatten_valid_sequence_head_major(value, state.rank_plan.local_valid_lengths), + ) + + +def _run_context_parallel_forward( + *, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + state: ArtContextParallelState, + scale: float, + enable_gqa: bool, + compile_enabled: bool, +) -> torch.Tensor: + kernel = FlexAttentionKernel(compile_enabled=compile_enabled) + q_flat, k_flat, v_flat = _flatten_qkv( + query=query, + key=key, + value=value, + state=state, + ) + accum_out, _ = _forward_stage_records( + q_flat=q_flat, + k_flat=k_flat, + v_flat=v_flat, + state=state, + kernel=kernel, + scale=scale, + enable_gqa=enable_gqa, + record_for_backward=False, + ) + return unflatten_valid_sequence_head_major( + accum_out.to(dtype=query.dtype), + valid_lengths=state.rank_plan.local_valid_lengths, + seq_len=query.shape[0], + ) + + +def _run_context_parallel_forward_recorded( + *, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + state: ArtContextParallelState, + scale: float, + enable_gqa: bool, + compile_enabled: bool, +) -> tuple[torch.Tensor, torch.Tensor, list[dict[str, Any]]]: + kernel = FlexAttentionKernel(compile_enabled=compile_enabled) + q_flat, k_flat, v_flat = _flatten_qkv( + query=query, + key=key, + value=value, + state=state, + ) + accum_out, replay_records = _forward_stage_records( + q_flat=q_flat, + k_flat=k_flat, + v_flat=v_flat, + state=state, + kernel=kernel, + scale=scale, + enable_gqa=enable_gqa, + record_for_backward=True, + ) + return ( + unflatten_valid_sequence_head_major( + accum_out.to(dtype=query.dtype), + valid_lengths=state.rank_plan.local_valid_lengths, + seq_len=query.shape[0], + ), + accum_out, + replay_records, + ) + + +def _scatter_stage_grad( + *, + target: torch.Tensor, + grad: torch.Tensor | None, + ranges: tuple[TokenRange, ...], + state: ArtContextParallelState | None = None, + head_major: bool = False, +) -> None: + if grad is None or grad.numel() == 0: + return + grad = grad.contiguous() + if grad.dtype != target.dtype: + grad = grad.to(dtype=target.dtype) + full_length = _ranges_cover_full_length( + ranges, + length=int(target.shape[1] if head_major else target.shape[0]), + ) + if full_length: + target.add_(grad) + return + if head_major: + range_reduce_sum_head_major_( + grad, + output_tensor=target, + ranges=ranges, + range_meta_cache=( + None if state is None else state.execution_cache.range_meta + ), + ) + return + range_reduce_sum_( + grad, + output_tensor=target, + ranges=ranges, + range_meta_cache=(None if state is None else state.execution_cache.range_meta), + ) + + +def _sanitize_nested_stage_input_grad( + grad: torch.Tensor | None, +) -> torch.Tensor | None: + if grad is None: + return None + # Nested autograd.grad can hand back view-backed stage input grads tied to + # raw compiled flex backward storage. Clone away from that base lineage and + # synchronize before first downstream use. + cloned = grad.detach().clone() + if cloned.is_cuda: + torch.cuda.current_stream(device=cloned.device).synchronize() + return cloned + + +def _zero_stage_grads_like( + tensor: torch.Tensor, +) -> torch.Tensor: + return torch.zeros_like(tensor) + + +def _run_context_parallel_backward( + *, + grad_output: torch.Tensor, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + state: ArtContextParallelState, + scale: float, + enable_gqa: bool, + compile_enabled: bool, + replay_records: list[dict[str, Any]] | None = None, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + kernel = FlexAttentionKernel(compile_enabled=compile_enabled) + comm_async_enabled = _distributed_cp_comm_enabled(state) + q_flat, k_flat, v_flat = _flatten_qkv( + query=query, + key=key, + value=value, + state=state, + ) + grad_output_flat = flatten_valid_sequence_head_major( + grad_output, + state.rank_plan.local_valid_lengths, + ) + if replay_records is None: + _, replay_records = _forward_stage_records( + q_flat=q_flat, + k_flat=k_flat, + v_flat=v_flat, + state=state, + kernel=kernel, + scale=scale, + enable_gqa=enable_gqa, + record_for_backward=True, + ) + stage_out_grads, stage_lse_grads = _merge_stage_output_grads_from_tape( + replay_records=replay_records, + grad_output_flat=grad_output_flat, + ) + if stage_out_grads and stage_out_grads[0].is_cuda: + # Nested FLASH flex backward consumes these external grad_outputs on an + # internal stream; complete the merge-backward producers before the handoff. + torch.cuda.current_stream(stage_out_grads[0].device).synchronize() + grad_by_stage_index: dict[int, tuple[torch.Tensor, torch.Tensor]] = {} + for record, stage_out_grad, stage_lse_grad in zip( + replay_records, + stage_out_grads, + stage_lse_grads, + strict=True, + ): + stage_plan = cast(StagePlan, record["stage_plan"]) + grad_by_stage_index[int(stage_plan.stage_index)] = ( + _zero_stage_grads_like(record["stage_out"]) + if stage_out_grad is None + else stage_out_grad, + _zero_stage_grads_like(record["stage_lse"]) + if stage_lse_grad is None + else stage_lse_grad, + ) + del stage_out_grads, stage_lse_grads + + grad_accum_dtype = q_flat.dtype + dq_flat = torch.zeros(q_flat.shape, device=q_flat.device, dtype=grad_accum_dtype) + dk_flat = torch.zeros(k_flat.shape, device=k_flat.device, dtype=grad_accum_dtype) + dv_flat = torch.zeros(v_flat.shape, device=v_flat.device, dtype=grad_accum_dtype) + reduce_works: list[Any] = [] + needs_remote_reduce = any( + not stage_plan.is_local_stage for stage_plan in state.rank_plan.stage_plans + ) + if needs_remote_reduce and not comm_async_enabled: + raise RuntimeError( + "ART context parallel backward remote reductions require distributed async per-stage " + "dKV reduce." + ) + if not any( + (not stage_plan.is_local_stage) and _stage_requires_reduce(stage_plan) + for stage_plan in state.rank_plan.stage_plans + ) and ( + sum(state.rank_plan.remote_dkv_reduce_plan.send_splits) > 0 + or sum(state.rank_plan.remote_dkv_reduce_plan.recv_splits) > 0 + ): + empty = k_flat.new_empty((k_flat.shape[0], 0, k_flat.shape[2])) + reduce_works.append( + _COMMUNICATOR.launch_dkv_reduce( + dk_remote=empty, + dv_remote=empty, + plan=state.rank_plan.remote_dkv_reduce_plan, + group=state.cp_group, + async_op=comm_async_enabled, + dk_local=dk_flat, + dv_local=dv_flat, + range_meta_cache=state.execution_cache.range_meta, + label="dkv_reduce.global", + input_layout="head_major", + ) + ) + records_by_stage_index = { + int(cast(StagePlan, record["stage_plan"]).stage_index): record + for record in replay_records + } + + for stage_index in state.rank_plan.backward_stage_indices: + stage_plan = state.rank_plan.stage_plans[int(stage_index)] + stage_record = records_by_stage_index.get(int(stage_plan.stage_index)) + if stage_record is None: + if stage_plan.is_local_stage: + continue + empty = k_flat.new_empty((k_flat.shape[0], 0, k_flat.shape[2])) + reduce_works.append( + _COMMUNICATOR.launch_dkv_reduce( + dk_remote=empty, + dv_remote=empty, + plan=cast(DkvReducePlan, stage_plan.dkv_reduce_plan), + group=state.cp_group, + async_op=comm_async_enabled, + dk_local=dk_flat, + dv_local=dv_flat, + range_meta_cache=state.execution_cache.range_meta, + label=( + f"dkv_reduce.stage{stage_plan.stage_index}." + f"src{stage_plan.source_rank}" + ), + input_layout="head_major", + ) + ) + continue + + stage_out_grad, stage_lse_grad = grad_by_stage_index[ + int(stage_plan.stage_index) + ] + inputs: list[torch.Tensor] = [] + input_names: list[str] = [] + for name in ("q_input", "k_input", "v_input"): + tensor = cast(torch.Tensor, stage_record[name]) + if tensor.requires_grad: + inputs.append(tensor) + input_names.append(name) + if not inputs: + grad_by_stage_index.pop(int(stage_plan.stage_index), None) + _release_replay_record_tensors(stage_record) + stage_record.clear() + continue + stage_outputs: list[torch.Tensor] = [] + stage_output_grads: list[torch.Tensor] = [] + stage_out_tensor = cast(torch.Tensor, stage_record["stage_out"]) + stage_lse_tensor = cast(torch.Tensor, stage_record["stage_lse"]) + if stage_out_tensor.requires_grad: + stage_outputs.append(stage_out_tensor) + stage_output_grads.append(stage_out_grad) + if stage_lse_tensor.requires_grad: + stage_outputs.append(stage_lse_tensor) + stage_output_grads.append(stage_lse_grad) + if not stage_outputs: + grad_by_stage_index.pop(int(stage_plan.stage_index), None) + _release_replay_record_tensors(stage_record) + stage_record.clear() + continue + input_grads = torch.autograd.grad( + outputs=tuple(stage_outputs), + inputs=inputs, + grad_outputs=tuple(stage_output_grads), + allow_unused=True, + ) + grad_map = { + name: grad for name, grad in zip(input_names, input_grads, strict=True) + } + for grad_name in ("q_input", "k_input", "v_input"): + grad_map[grad_name] = _sanitize_nested_stage_input_grad( + cast(torch.Tensor | None, grad_map.get(grad_name)), + ) + _scatter_stage_grad( + target=dq_flat, + grad=cast(torch.Tensor | None, grad_map.get("q_input")), + ranges=stage_plan.owner_local_q_ranges, + state=state, + head_major=True, + ) + if stage_plan.is_local_stage: + _scatter_stage_grad( + target=dk_flat, + grad=cast(torch.Tensor | None, grad_map.get("k_input")), + ranges=stage_plan.owner_local_k_ranges, + state=state, + head_major=True, + ) + _scatter_stage_grad( + target=dv_flat, + grad=cast(torch.Tensor | None, grad_map.get("v_input")), + ranges=stage_plan.owner_local_k_ranges, + state=state, + head_major=True, + ) + grad_by_stage_index.pop(int(stage_plan.stage_index), None) + _release_replay_record_tensors(stage_record) + stage_record.clear() + continue + if not stage_plan.is_local_stage: + dk_remote = cast(torch.Tensor | None, grad_map.get("k_input")) + dv_remote = cast(torch.Tensor | None, grad_map.get("v_input")) + if dk_remote is None: + dk_remote = k_flat.new_empty((k_flat.shape[0], 0, k_flat.shape[2])) + if dv_remote is None: + dv_remote = v_flat.new_empty((v_flat.shape[0], 0, v_flat.shape[2])) + reduce_works.append( + _COMMUNICATOR.launch_dkv_reduce( + dk_remote=dk_remote.contiguous(), + dv_remote=dv_remote.contiguous(), + plan=cast(DkvReducePlan, stage_plan.dkv_reduce_plan), + group=state.cp_group, + async_op=comm_async_enabled, + dk_local=dk_flat, + dv_local=dv_flat, + range_meta_cache=state.execution_cache.range_meta, + label=( + f"dkv_reduce.stage{stage_plan.stage_index}." + f"src{stage_plan.source_rank}" + ), + input_layout="head_major", + ) + ) + grad_by_stage_index.pop(int(stage_plan.stage_index), None) + _release_replay_record_tensors(stage_record) + stage_record.clear() + + for work in reduce_works: + work.wait_post_process() + records_by_stage_index.clear() + replay_records.clear() + + return ( + unflatten_valid_sequence_head_major( + dq_flat.to(dtype=query.dtype), + valid_lengths=state.rank_plan.local_valid_lengths, + seq_len=query.shape[0], + ), + unflatten_valid_sequence_head_major( + dk_flat.to(dtype=key.dtype), + valid_lengths=state.rank_plan.local_valid_lengths, + seq_len=key.shape[0], + ), + unflatten_valid_sequence_head_major( + dv_flat.to(dtype=value.dtype), + valid_lengths=state.rank_plan.local_valid_lengths, + seq_len=value.shape[0], + ), + ) + + +class ArtContextParallelFn(torch.autograd.Function): + @staticmethod + def forward( + ctx, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + state: ArtContextParallelState, + scale: float, + enable_gqa: bool, + compile_enabled: bool, + ) -> torch.Tensor: + ctx.state = state + ctx.scale = float(scale) + ctx.enable_gqa = bool(enable_gqa) + ctx.compile_enabled = bool(compile_enabled) + ctx.save_for_backward(query, key, value) + with torch.enable_grad(): + query_record = query.detach().requires_grad_(bool(query.requires_grad)) + key_record = key.detach().requires_grad_(bool(key.requires_grad)) + value_record = value.detach().requires_grad_(bool(value.requires_grad)) + output, _replay_accum_out, replay_records = ( + _run_context_parallel_forward_recorded( + query=query_record, + key=key_record, + value=value_record, + state=state, + scale=float(scale), + enable_gqa=bool(enable_gqa), + compile_enabled=bool(compile_enabled), + ) + ) + ctx.replay_records = replay_records + return output.detach() + + @staticmethod + def backward(ctx, *grad_outputs: Any): + (grad_output,) = cast(tuple[torch.Tensor], grad_outputs) + query, key, value = ctx.saved_tensors + try: + dq, dk, dv = _run_context_parallel_backward( + grad_output=grad_output, + query=query, + key=key, + value=value, + state=ctx.state, + scale=ctx.scale, + enable_gqa=ctx.enable_gqa, + compile_enabled=ctx.compile_enabled, + replay_records=cast(list[dict[str, Any]], ctx.replay_records), + ) + finally: + ctx.replay_records = None + return dq, dk, dv, None, None, None, None + + +def run_context_parallel( + *, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + state: ArtContextParallelState, + scale: float, + enable_gqa: bool, + compile_enabled: bool, +) -> torch.Tensor: + if torch.is_grad_enabled() and ( + query.requires_grad or key.requires_grad or value.requires_grad + ): + return ArtContextParallelFn.apply( + query, + key, + value, + state, + float(scale), + bool(enable_gqa), + bool(compile_enabled), + ) + return _run_context_parallel_forward( + query=query, + key=key, + value=value, + state=state, + scale=scale, + enable_gqa=enable_gqa, + compile_enabled=compile_enabled, + ) diff --git a/src/art/megatron/context_parallel/loss_inputs.py b/src/art/megatron/context_parallel/loss_inputs.py new file mode 100644 index 000000000..cbad1a792 --- /dev/null +++ b/src/art/megatron/context_parallel/loss_inputs.py @@ -0,0 +1,101 @@ +from typing import Any, Literal + +import torch + +from art.loss import AlignedLossInputs + + +class ContextParallelLossInputs(AlignedLossInputs): + loss_all_reduce_group: Any | None = None + entropies_are_aligned: bool = True + + def group_mean(self, values: torch.Tensor, by: torch.Tensor) -> torch.Tensor: + if self.loss_all_reduce_group is None: + return super().group_mean(values, by) + return _distributed_group_mean( + values, + by=by, + group=self.loss_all_reduce_group, + ) + + def masked_mean(self, values: torch.Tensor, mask: torch.Tensor) -> torch.Tensor: + if self.loss_all_reduce_group is None: + return super().masked_mean(values, mask) + numerator = values.sum() + denominator = mask.sum() + torch.distributed.all_reduce( # ty: ignore[possibly-missing-attribute] + numerator, + group=self.loss_all_reduce_group, + ) + torch.distributed.all_reduce( # ty: ignore[possibly-missing-attribute] + denominator, + group=self.loss_all_reduce_group, + ) + return numerator / (denominator + 1e-18) + + def denominator( + self, + mask: torch.Tensor, + reduction: Literal["mean", "sum"], + ): + if self.loss_all_reduce_group is None or reduction == "sum": + return super().denominator(mask, reduction) + denominator = mask.sum() + torch.distributed.all_reduce( # ty: ignore[possibly-missing-attribute] + denominator, + group=self.loss_all_reduce_group, + ) + return denominator + 1e-18 + + +def _distributed_group_mean( + values: torch.Tensor, + *, + by: torch.Tensor, + group: Any, +) -> torch.Tensor: + flat_values = values.reshape(-1) + flat_by = by.reshape(-1).to(dtype=torch.float32) + unique_local = torch.unique(flat_by, sorted=True) + world_size = torch.distributed.get_world_size(group) # ty: ignore[possibly-missing-attribute] + local_count = torch.tensor( + [unique_local.numel()], + device=values.device, + dtype=torch.long, + ) + gathered_counts = [torch.empty_like(local_count) for _ in range(world_size)] + torch.distributed.all_gather( # ty: ignore[possibly-missing-attribute] + gathered_counts, + local_count, + group=group, + ) + max_count = int(torch.stack(gathered_counts).max().item()) + padded_ids = torch.zeros(max_count, device=values.device, dtype=torch.float32) + padded_ids[: unique_local.numel()] = unique_local + gathered_ids = [torch.empty_like(padded_ids) for _ in range(world_size)] + torch.distributed.all_gather( # ty: ignore[possibly-missing-attribute] + gathered_ids, + padded_ids, + group=group, + ) + global_ids = torch.unique( + torch.cat( + [ + gathered[: int(count.item())] + for gathered, count in zip(gathered_ids, gathered_counts, strict=True) + ] + ), + sorted=True, + ) + group_indices = torch.searchsorted(global_ids, flat_by) + sums = torch.zeros_like(global_ids) + counts = torch.zeros_like(global_ids) + sums.scatter_add_(0, group_indices, flat_values.to(dtype=sums.dtype)) + counts.scatter_add_( + 0, + group_indices, + torch.ones_like(flat_values, dtype=sums.dtype), + ) + torch.distributed.all_reduce(sums, group=group) # ty: ignore[possibly-missing-attribute] + torch.distributed.all_reduce(counts, group=group) # ty: ignore[possibly-missing-attribute] + return (sums / (counts + 1e-18)).gather(0, group_indices).reshape_as(values) diff --git a/src/art/megatron/context_parallel/range_ops.py b/src/art/megatron/context_parallel/range_ops.py new file mode 100644 index 000000000..a645e9f80 --- /dev/null +++ b/src/art/megatron/context_parallel/range_ops.py @@ -0,0 +1,683 @@ +from __future__ import annotations + +from collections.abc import Sequence + +import torch +import triton +import triton.language as tl + +from .types import TokenRange + + +def _single_range(ranges: Sequence[TokenRange]) -> TokenRange | None: + compact = [range_ for range_ in ranges if range_.size() > 0] + if len(compact) != 1: + return None + return compact[0] + + +@triton.jit +def _range_gather_per_row_kernel( + input_ptr, + output_ptr, + ranges_ptr, + cu_range_sizes_ptr, + row_map_ptr, + input_stride, + output_stride, + n_cols: tl.constexpr, + n_col_blocks: tl.constexpr, + elem_per_block: tl.constexpr, +): + out_row = tl.program_id(0) + block_idx = tl.program_id(1) + + range_idx = tl.load(row_map_ptr + out_row) + range_base = tl.load(cu_range_sizes_ptr + range_idx) + range_row = out_row - range_base + input_row = tl.load(ranges_ptr + range_idx * 2) + range_row + + cols = block_idx * elem_per_block + tl.arange(0, elem_per_block) + mask = cols < n_cols + + input_offsets = input_row * input_stride + cols + output_offsets = out_row * output_stride + cols + + values = tl.load(input_ptr + input_offsets, mask=mask) + tl.store(output_ptr + output_offsets, values, mask=mask) + + +@triton.jit +def _range_reduce_sum_per_row_kernel( + input_ptr, + output_ptr, + ranges_ptr, + cu_range_sizes_ptr, + row_map_ptr, + input_stride, + output_stride, + n_cols: tl.constexpr, + n_col_blocks: tl.constexpr, + elem_per_block: tl.constexpr, +): + in_row = tl.program_id(0) + block_idx = tl.program_id(1) + + range_idx = tl.load(row_map_ptr + in_row) + range_base = tl.load(cu_range_sizes_ptr + range_idx) + range_row = in_row - range_base + output_row = tl.load(ranges_ptr + range_idx * 2) + range_row + + cols = block_idx * elem_per_block + tl.arange(0, elem_per_block) + mask = cols < n_cols + + input_offsets = in_row * input_stride + cols + output_offsets = output_row * output_stride + cols + + update = tl.load(input_ptr + input_offsets, mask=mask) + tl.atomic_add(output_ptr + output_offsets, update, mask=mask) + + +@triton.jit +def _range_gather_head_major_kernel( + input_ptr, + output_ptr, + ranges_ptr, + cu_range_sizes_ptr, + row_map_ptr, + input_head_stride, + input_token_stride, + output_head_stride, + output_token_stride, + inner_size: tl.constexpr, + n_cols: tl.constexpr, + n_col_blocks: tl.constexpr, + elem_per_block: tl.constexpr, +): + out_row = tl.program_id(0) + block_idx = tl.program_id(1) + + range_idx = tl.load(row_map_ptr + out_row) + range_base = tl.load(cu_range_sizes_ptr + range_idx) + range_row = out_row - range_base + input_row = tl.load(ranges_ptr + range_idx * 2) + range_row + + cols = block_idx * elem_per_block + tl.arange(0, elem_per_block) + mask = cols < n_cols + head_idx = cols // inner_size + inner_idx = cols % inner_size + + input_offsets = ( + head_idx * input_head_stride + input_row * input_token_stride + inner_idx + ) + output_offsets = ( + head_idx * output_head_stride + out_row * output_token_stride + inner_idx + ) + values = tl.load(input_ptr + input_offsets, mask=mask) + tl.store(output_ptr + output_offsets, values, mask=mask) + + +@triton.jit +def _range_reduce_sum_head_major_kernel( + input_ptr, + output_ptr, + ranges_ptr, + cu_range_sizes_ptr, + row_map_ptr, + input_head_stride, + input_token_stride, + output_head_stride, + output_token_stride, + inner_size: tl.constexpr, + n_cols: tl.constexpr, + n_col_blocks: tl.constexpr, + elem_per_block: tl.constexpr, +): + in_row = tl.program_id(0) + block_idx = tl.program_id(1) + + range_idx = tl.load(row_map_ptr + in_row) + range_base = tl.load(cu_range_sizes_ptr + range_idx) + range_row = in_row - range_base + output_row = tl.load(ranges_ptr + range_idx * 2) + range_row + + cols = block_idx * elem_per_block + tl.arange(0, elem_per_block) + mask = cols < n_cols + head_idx = cols // inner_size + inner_idx = cols % inner_size + + input_offsets = ( + head_idx * input_head_stride + in_row * input_token_stride + inner_idx + ) + output_offsets = ( + head_idx * output_head_stride + output_row * output_token_stride + inner_idx + ) + update = tl.load(input_ptr + input_offsets, mask=mask) + tl.atomic_add(output_ptr + output_offsets, update, mask=mask) + + +def _range_key(ranges: Sequence[TokenRange]) -> tuple[tuple[int, int], ...]: + return tuple((range_.start, range_.end) for range_ in ranges if range_.size() > 0) + + +def _range_meta( + ranges: Sequence[TokenRange], + *, + device: torch.device, + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None = None, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]: + key = (_range_key(ranges), device.type, device.index) + if range_meta_cache is not None: + cached = range_meta_cache.get(key) + if cached is not None: + return cached + + compact = [range_ for range_ in ranges if range_.size() > 0] + if not compact: + empty_i64 = torch.empty((0,), device=device, dtype=torch.int64) + empty_ranges = torch.empty((0, 2), device=device, dtype=torch.int64) + cached = (empty_ranges, empty_i64, empty_i64, 0) + if range_meta_cache is not None: + range_meta_cache[key] = cached + return cached + + ranges_tensor = torch.tensor( + [(range_.start, range_.end) for range_ in compact], + device=device, + dtype=torch.int64, + ) + range_sizes = torch.tensor( + [0, *[range_.size() for range_ in compact]], + device=device, + dtype=torch.int64, + ) + cu_range_sizes = torch.cumsum(range_sizes, dim=0) + total_size = int(cu_range_sizes[-1].item()) + row_map = torch.repeat_interleave( + torch.arange(len(compact), device=device, dtype=torch.int64), + range_sizes[1:], + output_size=total_size, + ) + cached = (ranges_tensor, cu_range_sizes, row_map, total_size) + if range_meta_cache is not None: + range_meta_cache[key] = cached + return cached + + +def _python_range_gather( + input_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + *, + output: torch.Tensor | None = None, +) -> torch.Tensor: + parts = [ + input_tensor[range_.start : range_.end] + for range_ in ranges + if range_.size() > 0 + ] + if output is None: + if not parts: + return input_tensor.new_empty((0, *input_tensor.shape[1:])) + if len(parts) == 1: + return parts[0].contiguous() + return torch.cat(parts, dim=0).contiguous() + if not parts: + return output + cursor = 0 + for part in parts: + next_cursor = cursor + int(part.shape[0]) + output[cursor:next_cursor].copy_(part) + cursor = next_cursor + return output + + +def _python_range_gather_head_major( + input_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + *, + output: torch.Tensor | None = None, +) -> torch.Tensor: + parts = [ + input_tensor[:, range_.start : range_.end] + for range_ in ranges + if range_.size() > 0 + ] + if output is None: + if not parts: + return input_tensor.new_empty( + (input_tensor.shape[0], 0, *input_tensor.shape[2:]) + ) + if len(parts) == 1: + return parts[0].contiguous() + return torch.cat(parts, dim=1).contiguous() + if not parts: + return output + cursor = 0 + for part in parts: + next_cursor = cursor + int(part.shape[1]) + output[:, cursor:next_cursor].copy_(part) + cursor = next_cursor + return output + + +def _range_gather_impl( + input_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + *, + output: torch.Tensor | None = None, + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None = None, +) -> torch.Tensor: + if input_tensor.ndim < 1: + raise RuntimeError( + f"Expected tensor with dim>=1, got {tuple(input_tensor.shape)}" + ) + if not ranges: + if output is not None: + return output + return input_tensor.new_empty((0, *input_tensor.shape[1:])) + if not input_tensor.is_cuda: + return _python_range_gather(input_tensor, ranges, output=output) + + ranges_tensor, cu_range_sizes, row_map, total_size = _range_meta( + ranges, + device=input_tensor.device, + range_meta_cache=range_meta_cache, + ) + if output is None: + output = input_tensor.new_empty((total_size, *input_tensor.shape[1:])) + else: + if int(output.shape[0]) != total_size: + raise RuntimeError( + f"range_gather output has wrong first dim: expected {total_size}, got {int(output.shape[0])}" + ) + output = output.contiguous() + if total_size == 0 or input_tensor.numel() == 0: + return output + + n_cols = input_tensor.numel() // max(int(input_tensor.shape[0]), 1) + elem_per_block = max(1, 2048 // input_tensor.element_size()) + n_col_blocks = triton.cdiv(n_cols, elem_per_block) + _range_gather_per_row_kernel[(total_size, n_col_blocks)]( + input_tensor, + output, + ranges_tensor, + cu_range_sizes, + row_map, + input_tensor.stride(0), + output.stride(0), + n_cols=n_cols, # ty: ignore[invalid-argument-type] + n_col_blocks=n_col_blocks, + elem_per_block=elem_per_block, # ty: ignore[invalid-argument-type] + num_warps=4, # ty: ignore[unknown-argument] + ) + return output + + +def _range_gather_head_major_impl( + input_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + *, + output: torch.Tensor | None = None, + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None = None, +) -> torch.Tensor: + if input_tensor.ndim < 2: + raise RuntimeError( + f"Expected tensor with dim>=2, got {tuple(input_tensor.shape)}" + ) + if not ranges: + if output is not None: + return output + return input_tensor.new_empty( + (input_tensor.shape[0], 0, *input_tensor.shape[2:]) + ) + if not input_tensor.is_cuda: + return _python_range_gather_head_major(input_tensor, ranges, output=output) + + input_tensor = input_tensor.contiguous() + ranges_tensor, cu_range_sizes, row_map, total_size = _range_meta( + ranges, + device=input_tensor.device, + range_meta_cache=range_meta_cache, + ) + if output is None: + output = input_tensor.new_empty( + (input_tensor.shape[0], total_size, *input_tensor.shape[2:]) + ) + else: + if int(output.shape[1]) != total_size: + raise RuntimeError( + "range_gather_head_major output has wrong token dim: " + f"expected {total_size}, got {int(output.shape[1])}" + ) + output = output.contiguous() + if total_size == 0 or input_tensor.numel() == 0: + return output + + inner_size = input_tensor.numel() // max( + int(input_tensor.shape[0] * input_tensor.shape[1]), 1 + ) + n_cols = int(input_tensor.shape[0]) * inner_size + elem_per_block = max(1, 2048 // input_tensor.element_size()) + n_col_blocks = triton.cdiv(n_cols, elem_per_block) + _range_gather_head_major_kernel[(total_size, n_col_blocks)]( + input_tensor, + output, + ranges_tensor, + cu_range_sizes, + row_map, + input_tensor.stride(0), + input_tensor.stride(1), + output.stride(0), + output.stride(1), + inner_size=inner_size, # ty: ignore[invalid-argument-type] + n_cols=n_cols, # ty: ignore[invalid-argument-type] + n_col_blocks=n_col_blocks, + elem_per_block=elem_per_block, # ty: ignore[invalid-argument-type] + num_warps=4, # ty: ignore[unknown-argument] + ) + return output + + +class _RangeGatherFn(torch.autograd.Function): + @staticmethod + def forward( + ctx, + input_tensor: torch.Tensor, + ranges: tuple[TokenRange, ...], + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None, + ) -> torch.Tensor: + ctx.ranges = ranges + ctx.input_shape = tuple(input_tensor.shape) + ctx.range_meta_cache = range_meta_cache + return _range_gather_impl( + input_tensor, + ranges, + range_meta_cache=range_meta_cache, + ) + + @staticmethod + def backward( + ctx, *grad_outputs: torch.Tensor | None + ) -> tuple[torch.Tensor, None, None]: + grad_output = grad_outputs[0] + if grad_output is None: + raise RuntimeError("_RangeGatherFn.backward expected one grad output") + grad_output = grad_output.contiguous() + grad_input = grad_output.new_zeros(ctx.input_shape) + range_reduce_sum_( + grad_output, + output_tensor=grad_input, + ranges=ctx.ranges, + range_meta_cache=ctx.range_meta_cache, + ) + return grad_input, None, None + + +class _RangeGatherHeadMajorFn(torch.autograd.Function): + @staticmethod + def forward( + ctx, + input_tensor: torch.Tensor, + ranges: tuple[TokenRange, ...], + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None, + ) -> torch.Tensor: + ctx.ranges = ranges + ctx.input_shape = tuple(input_tensor.shape) + ctx.range_meta_cache = range_meta_cache + return _range_gather_head_major_impl( + input_tensor, + ranges, + range_meta_cache=range_meta_cache, + ) + + @staticmethod + def backward( + ctx, *grad_outputs: torch.Tensor | None + ) -> tuple[torch.Tensor, None, None]: + grad_output = grad_outputs[0] + if grad_output is None: + raise RuntimeError( + "_RangeGatherHeadMajorFn.backward expected one grad output" + ) + grad_output = grad_output.contiguous() + grad_input = grad_output.new_zeros(ctx.input_shape) + range_reduce_sum_head_major_( + grad_output, + output_tensor=grad_input, + ranges=ctx.ranges, + range_meta_cache=ctx.range_meta_cache, + ) + return grad_input, None, None + + +def range_gather( + input_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + *, + output: torch.Tensor | None = None, + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None = None, +) -> torch.Tensor: + normalized_ranges = tuple(range_ for range_ in ranges if range_.size() > 0) + single_range = _single_range(normalized_ranges) + if single_range is not None: + gathered = input_tensor[single_range.start : single_range.end] + if output is None: + return gathered.contiguous() + output.copy_(gathered) + return output + if output is not None: + return _range_gather_impl( + input_tensor, + normalized_ranges, + output=output, + range_meta_cache=range_meta_cache, + ) + if input_tensor.requires_grad: + return _RangeGatherFn.apply(input_tensor, normalized_ranges, range_meta_cache) + return _range_gather_impl( + input_tensor, + normalized_ranges, + range_meta_cache=range_meta_cache, + ) + + +def range_gather_head_major( + input_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + *, + output: torch.Tensor | None = None, + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None = None, +) -> torch.Tensor: + normalized_ranges = tuple(range_ for range_ in ranges if range_.size() > 0) + single_range = _single_range(normalized_ranges) + if single_range is not None: + gathered = input_tensor[:, single_range.start : single_range.end] + if output is None: + return gathered.contiguous() + output.copy_(gathered) + return output + if output is not None: + return _range_gather_head_major_impl( + input_tensor, + normalized_ranges, + output=output, + range_meta_cache=range_meta_cache, + ) + if input_tensor.requires_grad: + return _RangeGatherHeadMajorFn.apply( + input_tensor, + normalized_ranges, + range_meta_cache, + ) + return _range_gather_head_major_impl( + input_tensor, + normalized_ranges, + range_meta_cache=range_meta_cache, + ) + + +def range_reduce_sum_( + input_tensor: torch.Tensor, + *, + output_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None = None, +) -> torch.Tensor: + expected_rows = sum(range_.size() for range_ in ranges) + if int(input_tensor.shape[0]) != expected_rows: + raise RuntimeError( + "range_reduce_sum_ consumed the wrong number of rows: " + f"consumed={int(input_tensor.shape[0])}, expected={expected_rows}" + ) + if expected_rows == 0: + return output_tensor + single_range = _single_range(ranges) + if single_range is not None: + updated = output_tensor[single_range.start : single_range.end] + input_tensor + output_tensor[single_range.start : single_range.end].copy_(updated) + return output_tensor + if not input_tensor.is_cuda or not output_tensor.is_cuda: + cursor = 0 + for range_ in ranges: + size = range_.size() + if size <= 0: + continue + output_tensor[range_.start : range_.end].add_( + input_tensor[cursor : cursor + size] + ) + cursor += size + return output_tensor + + input_tensor = input_tensor.contiguous() + output_tensor = output_tensor.contiguous() + ranges_tensor, cu_range_sizes, row_map, total_size = _range_meta( + ranges, + device=input_tensor.device, + range_meta_cache=range_meta_cache, + ) + if total_size != expected_rows: + raise RuntimeError( + f"range_reduce_sum_ range metadata mismatch: expected {expected_rows}, got {total_size}" + ) + n_cols = input_tensor.numel() // max(int(input_tensor.shape[0]), 1) + elem_per_block = max(1, 2048 // input_tensor.element_size()) + n_col_blocks = triton.cdiv(n_cols, elem_per_block) + _range_reduce_sum_per_row_kernel[(total_size, n_col_blocks)]( + input_tensor, + output_tensor, + ranges_tensor, + cu_range_sizes, + row_map, + input_tensor.stride(0), + output_tensor.stride(0), + n_cols=n_cols, # ty: ignore[invalid-argument-type] + n_col_blocks=n_col_blocks, + elem_per_block=elem_per_block, # ty: ignore[invalid-argument-type] + num_warps=4, # ty: ignore[unknown-argument] + ) + return output_tensor + + +def range_reduce_sum_head_major_( + input_tensor: torch.Tensor, + *, + output_tensor: torch.Tensor, + ranges: Sequence[TokenRange], + range_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], str, int | None], + tuple[torch.Tensor, torch.Tensor, torch.Tensor, int], + ] + | None = None, +) -> torch.Tensor: + expected_rows = sum(range_.size() for range_ in ranges) + if int(input_tensor.shape[1]) != expected_rows: + raise RuntimeError( + "range_reduce_sum_head_major_ consumed the wrong number of rows: " + f"consumed={int(input_tensor.shape[1])}, expected={expected_rows}" + ) + if expected_rows == 0: + return output_tensor + single_range = _single_range(ranges) + if single_range is not None: + updated = output_tensor[:, single_range.start : single_range.end] + input_tensor + output_tensor[:, single_range.start : single_range.end].copy_(updated) + return output_tensor + if not input_tensor.is_cuda or not output_tensor.is_cuda: + cursor = 0 + for range_ in ranges: + size = range_.size() + if size <= 0: + continue + output_tensor[:, range_.start : range_.end].add_( + input_tensor[:, cursor : cursor + size] + ) + cursor += size + return output_tensor + + input_tensor = input_tensor.contiguous() + output_tensor = output_tensor.contiguous() + ranges_tensor, cu_range_sizes, row_map, total_size = _range_meta( + ranges, + device=input_tensor.device, + range_meta_cache=range_meta_cache, + ) + if total_size != expected_rows: + raise RuntimeError( + "range_reduce_sum_head_major_ range metadata mismatch: " + f"expected {expected_rows}, got {total_size}" + ) + inner_size = input_tensor.numel() // max( + int(input_tensor.shape[0] * input_tensor.shape[1]), 1 + ) + n_cols = int(input_tensor.shape[0]) * inner_size + elem_per_block = max(1, 2048 // input_tensor.element_size()) + n_col_blocks = triton.cdiv(n_cols, elem_per_block) + _range_reduce_sum_head_major_kernel[(total_size, n_col_blocks)]( + input_tensor, + output_tensor, + ranges_tensor, + cu_range_sizes, + row_map, + input_tensor.stride(0), + input_tensor.stride(1), + output_tensor.stride(0), + output_tensor.stride(1), + inner_size=inner_size, # ty: ignore[invalid-argument-type] + n_cols=n_cols, # ty: ignore[invalid-argument-type] + n_col_blocks=n_col_blocks, + elem_per_block=elem_per_block, # ty: ignore[invalid-argument-type] + num_warps=4, # ty: ignore[unknown-argument] + ) + return output_tensor diff --git a/src/art/megatron/context_parallel/runtime.py b/src/art/megatron/context_parallel/runtime.py new file mode 100644 index 000000000..c6eb9fddd --- /dev/null +++ b/src/art/megatron/context_parallel/runtime.py @@ -0,0 +1,2989 @@ +from __future__ import annotations + +from bisect import bisect_left, bisect_right +import hashlib +import json +from typing import Any, cast +import warnings + +from pydantic import BaseModel, ConfigDict +import torch + +from art.loss import shift_tensor +from art.preprocessing.pack import PackedTensors + +from .builder import build_shared_prefix_attention_spec +from .layout_index import TokenLayoutIndex +from .types import ( + ArtContextParallelState, + AttnMaskKind, + AttnSlice, + ContextParallelConfig, + ContextParallelRuntimeKey, + ContextParallelRuntimePlan, + DispatchedPackedTensors, + DkvReducePlan, + ExactMaskMetadata, + KvFetchPlan, + PackedBatchAttentionSpec, + PackedRowAttentionSpec, + ParallelTopology, + PlannerProvenance, + PreparedMegatronBatch, + RankRuntimePlan, + StagePlan, + TokenRange, +) + +_PLANNER_RUNTIME_BACKEND = "art_context_parallel" +_PLANNER_BEST_EFFORT_WARNING_KEYS: set[ + tuple[str, str, int, str, str, tuple[int, ...]] +] = set() +_CHUNK_MASK_STATS_TORCH_THRESHOLD = 1024 +_CP4_SEARCH_PROBE_CANDIDATE_LIMIT = 2 +_CP4_SEARCH_PROBE_IMPROVEMENT_MS = 1.0 +_PLAN_CACHE_MAX_ENTRIES = 128 + +StagePiece = tuple[TokenRange, TokenRange, AttnMaskKind, int | None] +StageSliceKey = tuple[int, int, int, int, int, str, int] + + +class _PlanningBundle(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + spec: PackedBatchAttentionSpec + runtime_key: ContextParallelRuntimeKey + runtime_plan: ContextParallelRuntimePlan + gdn_execution_spec: Any | None = None + + +_PLANNING_BUNDLE_CACHE: dict[str, _PlanningBundle] = {} +_RUNTIME_PLAN_CACHE: dict[tuple[str, int], ContextParallelRuntimePlan] = {} +_GDN_RANK_PLAN_CACHE: dict[tuple[str, str, int | None, int], Any] = {} + + +def _json_cache_key(payload: Any) -> str: + return json.dumps(payload, sort_keys=True, separators=(",", ":")) + + +def _cache_put(cache: dict[Any, Any], key: Any, value: Any) -> None: + if key not in cache and len(cache) >= _PLAN_CACHE_MAX_ENTRIES: + cache.pop(next(iter(cache))) + cache[key] = value + + +def _metadata_tensor_digest(tensor: torch.Tensor) -> str: + """Digest planning metadata without touching CUDA in the normal path. + + CP lookahead depends on this function receiving CPU metadata. CUDA input is + still accepted for compatibility, but the device-to-host copy below will + synchronize and break host-ahead overlap with the previous microbatch's GPU + work. + """ + cpu_tensor = tensor.detach().to(device="cpu").contiguous() + digest = hashlib.sha1() + digest.update(str(tuple(cpu_tensor.shape)).encode("utf-8")) + digest.update(str(cpu_tensor.dtype).encode("utf-8")) + digest.update(cpu_tensor.numpy().tobytes()) + return digest.hexdigest() + + +def _planning_metadata_cpu(tensor: torch.Tensor) -> torch.Tensor: + """Return metadata on CPU for the no-sync CP planning boundary. + + Production lookahead callers should pass CPU tensors, making this a cheap + detach/contiguous operation. CUDA input is a compatibility fallback and + necessarily synchronizes. + """ + return tensor.detach().to(device="cpu").contiguous() + + +def _planning_bundle_cache_key( + *, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + topology: ParallelTopology, + config: ContextParallelConfig, + original_seq_len: int, + build_gdn_execution_spec: bool, +) -> str: + return _json_cache_key( + { + "group_ids": _metadata_tensor_digest(group_ids), + "parent_ids": _metadata_tensor_digest(parent_ids), + "topology": topology.model_dump(mode="json"), + "config": config.model_dump(mode="json"), + "original_seq_len": int(original_seq_len), + "build_gdn_execution_spec": bool(build_gdn_execution_spec), + } + ) + + +def _rank_plan_cache_key( + *, + planning_key: str, + device: torch.device, + cp_rank: int, +) -> tuple[str, str, int | None, int]: + return (planning_key, device.type, device.index, int(cp_rank)) + + +def _config_for_runtime_cp( + *, + topology: ParallelTopology, + config: ContextParallelConfig, +) -> ContextParallelConfig: + cp_size = max(int(topology.cp), 1) + updates: dict[str, Any] = {} + applied_override = False + for override in config.planner_cp_overrides: + if int(override.cp_size) != cp_size: + continue + override_updates = override.model_dump(mode="python", exclude_none=True) + override_updates.pop("cp_size", None) + updates.update(override_updates) + applied_override = True + if not applied_override: + return config + updates.setdefault("planner_tuned_cp_sizes", (cp_size,)) + return config.model_copy(update=updates) + + +def _normalized_planner_metadata_value(value: str | None) -> str: + if value is None: + return "" + normalized = "".join( + character.lower() if character.isalnum() else " " + for character in str(value).strip() + ) + return " ".join(part for part in normalized.split() if part) + + +def _planner_metadata_matches( + expected: str | None, + actual: str | None, + *, + fuzzy: bool, +) -> bool: + normalized_expected = _normalized_planner_metadata_value(expected) + normalized_actual = _normalized_planner_metadata_value(actual) + if not normalized_expected or not normalized_actual: + return False + if normalized_expected == normalized_actual: + return True + return bool( + fuzzy + and ( + normalized_expected in normalized_actual + or normalized_actual in normalized_expected + ) + ) + + +def _planner_runtime_hardware() -> str | None: + if not torch.cuda.is_available(): + return None + try: + return str(torch.cuda.get_device_name(torch.cuda.current_device())) + except Exception: + return str(torch.cuda.get_device_name(0)) + + +def _planner_best_effort_warning_message(provenance: PlannerProvenance) -> str: + mismatch_reasons: list[str] = [] + if not provenance.backend_match: + mismatch_reasons.append( + f"backend runtime={provenance.runtime_backend!r} tuned={provenance.tuned_backend!r}" + ) + if not provenance.hardware_match: + mismatch_reasons.append( + f"hardware runtime={provenance.runtime_hardware!r} tuned={provenance.tuned_hardware!r}" + ) + if not provenance.cp_size_match: + mismatch_reasons.append( + f"cp_size runtime={int(provenance.runtime_cp_size)} tuned={list(provenance.tuned_cp_sizes)}" + ) + mismatch_text = ( + "; ".join(mismatch_reasons) if mismatch_reasons else "metadata missing" + ) + return ( + "ART context parallel planner coefficients are running in best-effort mode; " + f"{mismatch_text}. The runtime will continue with the configured coefficients." + ) + + +def _planner_provenance( + *, + topology: ParallelTopology, + config: ContextParallelConfig, + warn: bool = True, +) -> PlannerProvenance: + runtime_hardware = _planner_runtime_hardware() + tuned_cp_sizes = tuple( + sorted( + { + int(cp_size) + for cp_size in config.planner_tuned_cp_sizes + if int(cp_size) > 0 + } + ) + ) + provenance = PlannerProvenance( + runtime_backend=_PLANNER_RUNTIME_BACKEND, + runtime_hardware=runtime_hardware, + runtime_cp_size=max(int(topology.cp), 1), + tuned_backend=config.planner_tuned_backend, + tuned_hardware=config.planner_tuned_hardware, + tuned_cp_sizes=tuned_cp_sizes, + backend_match=_planner_metadata_matches( + config.planner_tuned_backend, + _PLANNER_RUNTIME_BACKEND, + fuzzy=False, + ), + hardware_match=_planner_metadata_matches( + config.planner_tuned_hardware, + runtime_hardware, + fuzzy=True, + ), + cp_size_match=bool(tuned_cp_sizes) + and max(int(topology.cp), 1) in tuned_cp_sizes, + using_best_effort=False, + ) + if ( + provenance.backend_match + and provenance.hardware_match + and provenance.cp_size_match + ): + return provenance + + warning_message = _planner_best_effort_warning_message(provenance) + warning_key = ( + _normalized_planner_metadata_value(provenance.runtime_backend), + _normalized_planner_metadata_value(provenance.runtime_hardware), + int(provenance.runtime_cp_size), + _normalized_planner_metadata_value(provenance.tuned_backend), + _normalized_planner_metadata_value(provenance.tuned_hardware), + provenance.tuned_cp_sizes, + ) + warning_emitted = False + if warn and warning_key not in _PLANNER_BEST_EFFORT_WARNING_KEYS: + _PLANNER_BEST_EFFORT_WARNING_KEYS.add(warning_key) + warnings.warn(warning_message, RuntimeWarning, stacklevel=3) + warning_emitted = True + return provenance.model_copy( + update={ + "using_best_effort": True, + "warning_message": warning_message, + "warning_emitted": warning_emitted, + } + ) + + +def _normalized_chunk_size( + *, + valid_tokens: int, + block_size: int, + requested_chunk_size: int, + cp_size: int | None = None, + config: ContextParallelConfig | None = None, +) -> int: + chunk_size = max(int(block_size), int(requested_chunk_size)) + if chunk_size % int(block_size) != 0: + chunk_size = ((chunk_size + int(block_size) - 1) // int(block_size)) * int( + block_size + ) + chunk_size = max(1, min(chunk_size, max(valid_tokens, 1))) + if cp_size is None or config is None: + return chunk_size + + chunk_budget_base = max(int(config.planner_chunk_budget_base), 0) + chunk_budget_per_cp_rank = max(int(config.planner_chunk_budget_per_cp_rank), 0) + if chunk_budget_base <= 0 and chunk_budget_per_cp_rank <= 0: + return chunk_size + + chunk_budget = max( + int(cp_size), + chunk_budget_base + chunk_budget_per_cp_rank * max(int(cp_size), 1), + ) + if chunk_budget <= 0: + return chunk_size + + requested_chunk_count = max( + 1, + (max(int(valid_tokens), 1) + int(chunk_size) - 1) // int(chunk_size), + ) + if requested_chunk_count <= chunk_budget: + return chunk_size + + chunk_size = max( + int(chunk_size), + (max(int(valid_tokens), 1) + int(chunk_budget) - 1) // int(chunk_budget), + ) + if chunk_size % int(block_size) != 0: + chunk_size = ((chunk_size + int(block_size) - 1) // int(block_size)) * int( + block_size + ) + return max(1, min(chunk_size, max(valid_tokens, 1))) + + +def _search_config_for_chunk_count( + *, + config: ContextParallelConfig, + chunk_count: int, +) -> ContextParallelConfig: + if int(chunk_count) >= 128: + updates = { + "planner_max_search_steps": min(int(config.planner_max_search_steps), 2), + "planner_candidate_chunk_limit": min( + int(config.planner_candidate_chunk_limit), 4 + ), + "planner_max_remote_waves": min(int(config.planner_max_remote_waves), 2), + } + elif int(chunk_count) >= 64: + updates = { + "planner_max_search_steps": min(int(config.planner_max_search_steps), 4), + "planner_candidate_chunk_limit": min( + int(config.planner_candidate_chunk_limit), 6 + ), + "planner_max_remote_waves": min(int(config.planner_max_remote_waves), 3), + } + else: + return config + if all(int(getattr(config, key)) == int(value) for key, value in updates.items()): + return config + return config.model_copy(update=updates) + + +def _best_improving_move( + *, + current_owners: tuple[int, ...], + current_eval: dict[str, Any], + wave_assignment: tuple[int, ...], + cp_size: int, + q_weights: list[float], + candidate_limit: int, + evaluate_candidate: Any, +) -> tuple[tuple[int, ...], dict[str, Any]] | None: + slow_rank = int( + max( + range(cp_size), + key=lambda rank: cast(tuple[float, ...], current_eval["rank_scores"])[rank], + ) + ) + candidate_chunks = _candidate_chunk_indices( + owners=current_owners, + target_rank=slow_rank, + q_weights=q_weights, + limit=int(candidate_limit), + ) + if not candidate_chunks: + return None + + best_move: tuple[tuple[int, ...], dict[str, Any]] | None = None + for chunk_index in candidate_chunks: + for dst_rank in range(cp_size): + if dst_rank == slow_rank: + continue + candidate = list(current_owners) + candidate[chunk_index] = dst_rank + candidate_owners = tuple(candidate) + if not _assignment_uses_all_ranks( + candidate_owners, + cp_size=cp_size, + ): + continue + candidate_eval = evaluate_candidate( + owners=candidate_owners, + wave_assignment=wave_assignment, + ) + if float(candidate_eval["score"]) + 1e-9 >= float(current_eval["score"]): + continue + if best_move is None or float(candidate_eval["score"]) + 1e-9 < float( + best_move[1]["score"] + ): + best_move = (candidate_owners, candidate_eval) + return best_move + + +def _build_chunk_ranges( + *, + valid_tokens: int, + chunk_size: int, +) -> tuple[TokenRange, ...]: + ranges: list[TokenRange] = [] + for start in range(0, valid_tokens, chunk_size): + ranges.append( + TokenRange(start=start, end=min(start + chunk_size, valid_tokens)) + ) + return tuple(ranges) + + +def _indexed_intersections( + base_range: TokenRange, + candidate_ranges: tuple[TokenRange, ...], + *, + candidate_starts: tuple[int, ...] | None = None, + candidate_ends: tuple[int, ...] | None = None, +) -> list[tuple[int, TokenRange]]: + if not candidate_ranges: + return [] + base_start = int(base_range.start) + base_end = int(base_range.end) + if candidate_starts is None: + candidate_starts = tuple(int(candidate.start) for candidate in candidate_ranges) + if candidate_ends is None: + candidate_ends = tuple(int(candidate.end) for candidate in candidate_ranges) + first_index = bisect_right(candidate_ends, base_start) + last_index = bisect_left(candidate_starts, base_end, lo=first_index) + intersections: list[tuple[int, TokenRange]] = [] + for index in range(first_index, last_index): + candidate = candidate_ranges[index] + start = max(base_start, int(candidate.start)) + end = min(base_end, int(candidate.end)) + if end > start: + intersections.append((index, TokenRange(start=start, end=end))) + return intersections + + +def _slice_pair_count( + *, + mask_kind: AttnMaskKind, + q_range: TokenRange, + k_range: TokenRange, +) -> int: + if mask_kind is AttnMaskKind.FULL: + return int(q_range.size()) * int(k_range.size()) + return _causal_piece_pair_count( + q_range=q_range, + k_range=k_range, + ) + + +def _causal_piece_pair_count( + *, + q_range: TokenRange, + k_range: TokenRange, +) -> int: + return _causal_piece_pair_count_from_bounds( + q_start=int(q_range.start), + q_end=int(q_range.end), + k_start=int(k_range.start), + k_end=int(k_range.end), + ) + + +def _causal_piece_pair_count_from_bounds( + *, + q_start: int, + q_end: int, + k_start: int, + k_end: int, +) -> int: + if q_end <= q_start or k_end <= k_start: + return 0 + + k_len = k_end - k_start + partial_q_start = max(q_start, k_start) + partial_q_end = min(q_end - 1, k_end - 2) + partial = 0 + if partial_q_start <= partial_q_end: + count = partial_q_end - partial_q_start + 1 + partial = count * (partial_q_start + partial_q_end + 2 - 2 * k_start) // 2 + + full_q_start = max(q_start, k_end - 1) + full_q_end = q_end - 1 + full = 0 + if full_q_start <= full_q_end: + full = (full_q_end - full_q_start + 1) * k_len + return int(partial + full) + + +def _chunk_piece_decomposition( + *, + start: int, + end: int, + chunk_size: int, +) -> tuple[ + int, tuple[int, ...], tuple[int, ...], tuple[int, ...], tuple[int, ...], int +]: + first = start // chunk_size + last = (end - 1) // chunk_size + piece_starts: list[int] = [] + piece_ends: list[int] = [] + piece_lengths: list[int] = [] + piece_prefix_lengths: list[int] = [] + running_len = 0 + for chunk_index in range(first, last + 1): + piece_start = start if chunk_index == first else chunk_index * chunk_size + piece_end = end if chunk_index == last else (chunk_index + 1) * chunk_size + piece_len = piece_end - piece_start + if piece_len <= 0: + continue + running_len += piece_len + piece_starts.append(piece_start) + piece_ends.append(piece_end) + piece_lengths.append(piece_len) + piece_prefix_lengths.append(running_len) + return ( + first, + tuple(piece_starts), + tuple(piece_ends), + tuple(piece_lengths), + tuple(piece_prefix_lengths), + running_len, + ) + + +def _can_use_shared_prefix_chunk_pair_program( + row_spec: PackedRowAttentionSpec, +) -> bool: + slices = row_spec.slices + index = 0 + while index < len(slices): + prompt_slice = slices[index] + if ( + prompt_slice.family_index is None + or prompt_slice.mask_kind is not AttnMaskKind.CAUSAL + or prompt_slice.q_range != prompt_slice.k_range + ): + return False + prompt_family_index = prompt_slice.family_index + if prompt_family_index is None: + raise RuntimeError("shared-prefix prompt slices must carry family_index") + family_index = int(prompt_family_index) + prompt_start = int(prompt_slice.q_range.start) + prompt_end = int(prompt_slice.q_range.end) + index += 1 + while index < len(slices): + family_value = slices[index].family_index + if family_value is None or int(family_value) != family_index: + break + if index + 1 >= len(slices): + return False + full_slice = slices[index] + causal_slice = slices[index + 1] + if ( + full_slice.family_index != prompt_slice.family_index + or causal_slice.family_index != prompt_slice.family_index + or full_slice.mask_kind is not AttnMaskKind.FULL + or causal_slice.mask_kind is not AttnMaskKind.CAUSAL + or full_slice.q_range != causal_slice.q_range + or causal_slice.q_range != causal_slice.k_range + or int(full_slice.k_range.start) != prompt_start + or int(full_slice.k_range.end) != prompt_end + ): + return False + index += 2 + return True + + +def _build_chunk_pair_program_generic( + row_spec: PackedRowAttentionSpec, + *, + chunk_count: int, + chunk_size: int, +) -> tuple[torch.Tensor, list[float]]: + pair_rows = [[0 for _ in range(chunk_count)] for _ in range(chunk_count)] + q_weights = [0.0 for _ in range(chunk_count)] + + for slice_ in row_spec.slices: + q_start = int(slice_.q_range.start) + q_end = int(slice_.q_range.end) + k_start = int(slice_.k_range.start) + k_end = int(slice_.k_range.end) + if q_end <= q_start or k_end <= k_start: + continue + + q_first = q_start // chunk_size + q_last = (q_end - 1) // chunk_size + k_first = k_start // chunk_size + k_last = (k_end - 1) // chunk_size + + k_piece_lengths: list[int] = [] + k_piece_prefix_lengths: list[int] = [] + running_k_len = 0 + for k_chunk_index in range(k_first, k_last + 1): + k_piece_start = ( + k_start if k_chunk_index == k_first else k_chunk_index * chunk_size + ) + k_piece_end = ( + k_end if k_chunk_index == k_last else (k_chunk_index + 1) * chunk_size + ) + k_piece_len = k_piece_end - k_piece_start + if k_piece_len <= 0: + continue + running_k_len += k_piece_len + k_piece_lengths.append(k_piece_len) + k_piece_prefix_lengths.append(running_k_len) + if not k_piece_lengths: + continue + + if slice_.mask_kind is AttnMaskKind.FULL: + total_k_len = running_k_len + for q_chunk_index in range(q_first, q_last + 1): + q_piece_start = ( + q_start if q_chunk_index == q_first else q_chunk_index * chunk_size + ) + q_piece_end = ( + q_end + if q_chunk_index == q_last + else (q_chunk_index + 1) * chunk_size + ) + q_piece_len = q_piece_end - q_piece_start + if q_piece_len <= 0: + continue + row = pair_rows[q_chunk_index] + for k_offset, k_piece_len in enumerate(k_piece_lengths): + row[k_first + k_offset] += q_piece_len * k_piece_len + q_weights[q_chunk_index] += float(q_piece_len * total_k_len) + continue + + for q_chunk_index in range(q_first, q_last + 1): + q_piece_start = ( + q_start if q_chunk_index == q_first else q_chunk_index * chunk_size + ) + q_piece_end = ( + q_end if q_chunk_index == q_last else (q_chunk_index + 1) * chunk_size + ) + q_piece_len = q_piece_end - q_piece_start + if q_piece_len <= 0: + continue + + row = pair_rows[q_chunk_index] + q_total = 0 + + full_k_last = min(k_last, q_chunk_index - 1) + if full_k_last >= k_first: + full_k_limit = full_k_last - k_first + for k_offset in range(full_k_limit + 1): + row[k_first + k_offset] += q_piece_len * k_piece_lengths[k_offset] + q_total += q_piece_len * k_piece_prefix_lengths[full_k_limit] + + if k_first <= q_chunk_index <= k_last: + k_piece_start = q_chunk_index * chunk_size + if q_chunk_index == k_first: + k_piece_start = max(k_piece_start, k_start) + k_piece_end = (q_chunk_index + 1) * chunk_size + if q_chunk_index == k_last: + k_piece_end = min(k_piece_end, k_end) + pair_count = _causal_piece_pair_count_from_bounds( + q_start=q_piece_start, + q_end=q_piece_end, + k_start=k_piece_start, + k_end=k_piece_end, + ) + if pair_count > 0: + row[q_chunk_index] += pair_count + q_total += pair_count + + if q_total > 0: + q_weights[q_chunk_index] += float(q_total) + return torch.tensor(pair_rows, dtype=torch.int64), q_weights + + +def _build_chunk_pair_program( + row_spec: PackedRowAttentionSpec, + *, + chunk_ranges: tuple[TokenRange, ...], +) -> tuple[torch.Tensor, list[float]]: + chunk_count = len(chunk_ranges) + if chunk_count == 0: + return torch.zeros((0, 0), dtype=torch.int64), [] + chunk_size = int(chunk_ranges[0].size()) + if not _can_use_shared_prefix_chunk_pair_program(row_spec): + return _build_chunk_pair_program_generic( + row_spec, + chunk_count=chunk_count, + chunk_size=chunk_size, + ) + + pair_rows = [[0 for _ in range(chunk_count)] for _ in range(chunk_count)] + q_weights = [0.0 for _ in range(chunk_count)] + slices = row_spec.slices + index = 0 + while index < len(slices): + prompt_slice = slices[index] + ( + prompt_first, + prompt_starts, + prompt_ends, + prompt_lengths, + prompt_prefix, + prompt_total, + ) = _chunk_piece_decomposition( + start=int(prompt_slice.q_range.start), + end=int(prompt_slice.q_range.end), + chunk_size=chunk_size, + ) + for offset, q_chunk_index in enumerate( + range(prompt_first, prompt_first + len(prompt_lengths)) + ): + q_piece_len = prompt_lengths[offset] + row = pair_rows[q_chunk_index] + q_total = 0 + if offset > 0: + for k_offset in range(offset): + row[prompt_first + k_offset] += ( + q_piece_len * prompt_lengths[k_offset] + ) + q_total += q_piece_len * prompt_prefix[offset - 1] + pair_count = _causal_piece_pair_count_from_bounds( + q_start=prompt_starts[offset], + q_end=prompt_ends[offset], + k_start=prompt_starts[offset], + k_end=prompt_ends[offset], + ) + if pair_count > 0: + row[q_chunk_index] += pair_count + q_total += pair_count + if q_total > 0: + q_weights[q_chunk_index] += float(q_total) + + prompt_family_index = prompt_slice.family_index + if prompt_family_index is None: + raise RuntimeError("shared-prefix prompt slices must carry family_index") + family_index = int(prompt_family_index) + index += 1 + completion_chunk_indices: list[int] = [] + completion_chunk_totals: list[int] = [] + while index < len(slices): + family_value = slices[index].family_index + if family_value is None or int(family_value) != family_index: + break + full_slice = slices[index] + ( + completion_first, + completion_starts, + completion_ends, + completion_lengths, + completion_prefix, + _, + ) = _chunk_piece_decomposition( + start=int(full_slice.q_range.start), + end=int(full_slice.q_range.end), + chunk_size=chunk_size, + ) + for offset, q_chunk_index in enumerate( + range(completion_first, completion_first + len(completion_lengths)) + ): + q_piece_len = completion_lengths[offset] + if ( + completion_chunk_indices + and completion_chunk_indices[-1] == q_chunk_index + ): + completion_chunk_totals[-1] += q_piece_len + else: + completion_chunk_indices.append(q_chunk_index) + completion_chunk_totals.append(q_piece_len) + + for offset, q_chunk_index in enumerate( + range(completion_first, completion_first + len(completion_lengths)) + ): + q_piece_len = completion_lengths[offset] + row = pair_rows[q_chunk_index] + q_total = 0 + if offset > 0: + for k_offset in range(offset): + row[completion_first + k_offset] += ( + q_piece_len * completion_lengths[k_offset] + ) + q_total += q_piece_len * completion_prefix[offset - 1] + pair_count = _causal_piece_pair_count_from_bounds( + q_start=completion_starts[offset], + q_end=completion_ends[offset], + k_start=completion_starts[offset], + k_end=completion_ends[offset], + ) + if pair_count > 0: + row[q_chunk_index] += pair_count + q_total += pair_count + if q_total > 0: + q_weights[q_chunk_index] += float(q_total) + index += 2 + + for q_chunk_index, total_q_len in zip( + completion_chunk_indices, + completion_chunk_totals, + strict=True, + ): + row = pair_rows[q_chunk_index] + for k_offset, k_piece_len in enumerate(prompt_lengths): + row[prompt_first + k_offset] += total_q_len * k_piece_len + q_weights[q_chunk_index] += float(total_q_len * prompt_total) + return torch.tensor(pair_rows, dtype=torch.int64), q_weights + + +def _collect_rank_stage_pieces( + row_spec: PackedRowAttentionSpec, + *, + chunk_ranges: tuple[TokenRange, ...], + owners: tuple[int, ...], + wave_assignment: tuple[int, ...], + target_rank: int, + cp_size: int, +) -> tuple[ + list[StagePiece], + list[list[StagePiece]], + list[list[list[TokenRange]]], + list[list[list[TokenRange]]], +]: + wave_count = max(wave_assignment, default=0) + 1 if wave_assignment else 0 + local_stage_pieces: list[StagePiece] = [] + remote_stage_pieces: list[list[StagePiece]] = [[] for _ in range(wave_count)] + recv_request_ranges: list[list[list[TokenRange]]] = [ + [[] for _ in range(cp_size)] for _ in range(wave_count) + ] + send_request_ranges: list[list[list[TokenRange]]] = [ + [[] for _ in range(cp_size)] for _ in range(wave_count) + ] + chunk_starts = tuple(int(range_.start) for range_ in chunk_ranges) + chunk_ends = tuple(int(range_.end) for range_ in chunk_ranges) + + for slice_ in row_spec.slices: + q_parts = _indexed_intersections( + slice_.q_range, + chunk_ranges, + candidate_starts=chunk_starts, + candidate_ends=chunk_ends, + ) + if not q_parts: + continue + k_parts = _indexed_intersections( + slice_.k_range, + chunk_ranges, + candidate_starts=chunk_starts, + candidate_ends=chunk_ends, + ) + if not k_parts: + continue + + target_q_parts = [ + (q_chunk_index, q_piece) + for q_chunk_index, q_piece in q_parts + if int(owners[q_chunk_index]) == int(target_rank) + ] + target_k_parts = [ + (k_chunk_index, k_piece) + for k_chunk_index, k_piece in k_parts + if int(owners[k_chunk_index]) == int(target_rank) + ] + + if target_q_parts: + for q_chunk_index, q_piece in target_q_parts: + del q_chunk_index + for k_chunk_index, k_piece in k_parts: + piece_mask_kind = _resolve_stage_mask_kind( + mask_kind=slice_.mask_kind, + q_piece=q_piece, + k_piece=k_piece, + ) + if piece_mask_kind is None: + continue + source_rank = int(owners[k_chunk_index]) + piece = ( + q_piece, + k_piece, + piece_mask_kind, + slice_.family_index, + ) + if source_rank == int(target_rank): + local_stage_pieces.append(piece) + continue + wave_index = int(wave_assignment[k_chunk_index]) + remote_stage_pieces[wave_index].append(piece) + recv_request_ranges[wave_index][source_rank].append(k_piece) + + if target_k_parts: + for q_chunk_index, q_piece in q_parts: + host_rank = int(owners[q_chunk_index]) + if host_rank == int(target_rank): + continue + for k_chunk_index, k_piece in target_k_parts: + piece_mask_kind = _resolve_stage_mask_kind( + mask_kind=slice_.mask_kind, + q_piece=q_piece, + k_piece=k_piece, + ) + if piece_mask_kind is None: + continue + wave_index = int(wave_assignment[k_chunk_index]) + send_request_ranges[wave_index][host_rank].append(k_piece) + + return ( + local_stage_pieces, + remote_stage_pieces, + recv_request_ranges, + send_request_ranges, + ) + + +def _contiguous_chunk_assignment( + *, + q_weights: list[float], + cp_size: int, +) -> tuple[int, ...]: + chunk_count = len(q_weights) + if chunk_count == 0: + return tuple() + if cp_size <= 1: + return tuple(0 for _ in range(chunk_count)) + prefix = [0.0] + for weight in q_weights: + prefix.append(prefix[-1] + weight) + total = prefix[-1] + boundaries = [0] + for split_index in range(1, cp_size): + remaining_ranks = cp_size - split_index + min_boundary = boundaries[-1] + 1 + max_boundary = chunk_count - remaining_ranks + if min_boundary > max_boundary: + boundaries.append(boundaries[-1]) + continue + target = ( + total * split_index / cp_size + if total > 0.0 + else float(chunk_count) * split_index / cp_size + ) + best_boundary = min_boundary + best_error = float("inf") + for boundary in range(min_boundary, max_boundary + 1): + current = prefix[boundary] if total > 0.0 else float(boundary) + error = abs(current - target) + if error < best_error: + best_error = error + best_boundary = boundary + boundaries.append(best_boundary) + boundaries.append(chunk_count) + + owners = [0 for _ in range(chunk_count)] + for rank, (start, end) in enumerate(zip(boundaries[:-1], boundaries[1:])): + for chunk_index in range(start, end): + owners[chunk_index] = rank + return tuple(owners) + + +def _bucket_chunk_assignment( + *, + q_weights: list[float], + cp_size: int, +) -> tuple[int, ...]: + chunk_count = len(q_weights) + if chunk_count == 0: + return tuple() + if cp_size <= 1: + return tuple(0 for _ in range(chunk_count)) + rank_loads = [0.0 for _ in range(cp_size)] + rank_chunk_counts = [0 for _ in range(cp_size)] + owners = [-1 for _ in range(chunk_count)] + for chunk_index in sorted( + range(chunk_count), + key=lambda index: (-q_weights[index], index), + ): + rank = min( + range(cp_size), + key=lambda candidate: ( + rank_loads[candidate], + rank_chunk_counts[candidate], + candidate, + ), + ) + owners[chunk_index] = rank + rank_loads[rank] += q_weights[chunk_index] + rank_chunk_counts[rank] += 1 + return tuple(int(owner) for owner in owners) + + +def _striped_chunk_assignment( + *, + chunk_count: int, + cp_size: int, + group_size: int, +) -> tuple[int, ...]: + if chunk_count == 0: + return tuple() + if cp_size <= 1: + return tuple(0 for _ in range(chunk_count)) + group_size = max(1, int(group_size)) + return tuple( + ((chunk_index // group_size) % cp_size) for chunk_index in range(chunk_count) + ) + + +def _assignment_uses_all_ranks( + owners: tuple[int, ...], + *, + cp_size: int, +) -> bool: + if len(owners) < cp_size: + return True + return len({int(owner) for owner in owners}) == cp_size + + +def _candidate_chunk_indices( + *, + owners: tuple[int, ...], + target_rank: int, + q_weights: list[float], + limit: int, +) -> tuple[int, ...]: + rank_chunks = [ + chunk_index + for chunk_index, owner in enumerate(owners) + if int(owner) == int(target_rank) + ] + if not rank_chunks: + return tuple() + if limit <= 0 or len(rank_chunks) <= limit: + return tuple(rank_chunks) + + boundary_chunks = [ + chunk_index + for chunk_index in rank_chunks + if chunk_index == 0 + or chunk_index + 1 == len(owners) + or int(owners[chunk_index - 1]) != int(target_rank) + or int(owners[chunk_index + 1]) != int(target_rank) + ] + weighted_chunks = sorted( + rank_chunks, + key=lambda index: (-q_weights[index], index), + )[:limit] + ordered_candidates = [*boundary_chunks, *weighted_chunks] + deduped: list[int] = [] + seen: set[int] = set() + for chunk_index in ordered_candidates: + if chunk_index in seen: + continue + deduped.append(chunk_index) + seen.add(chunk_index) + if len(deduped) >= limit: + break + return tuple(deduped) + + +def _wave_assignment( + *, + chunk_count: int, + wave_count: int, +) -> tuple[int, ...]: + if chunk_count <= 0: + return tuple() + if wave_count <= 1: + return tuple(0 for _ in range(chunk_count)) + return tuple( + (chunk_index * wave_count) // chunk_count for chunk_index in range(chunk_count) + ) + + +def _chunk_ranges_for_owner( + *, + chunk_ranges: tuple[TokenRange, ...], + owners: tuple[int, ...], + owner_rank: int, +) -> tuple[TokenRange, ...]: + return _merge_ranges( + [ + chunk_ranges[chunk_index] + for chunk_index, rank in enumerate(owners) + if int(rank) == int(owner_rank) + ] + ) + + +def _ranges_size(ranges: tuple[TokenRange, ...]) -> int: + return int(sum(range_.size() for range_ in ranges)) + + +def _chunk_mask_stats( + *, + chunk_lengths: tuple[int, ...], + chunk_mask: torch.Tensor, + chunk_lengths_tensor: torch.Tensor | None = None, +) -> tuple[int, int]: + if ( + chunk_lengths_tensor is not None + and len(chunk_lengths) >= _CHUNK_MASK_STATS_TORCH_THRESHOLD + ): + if int(chunk_mask.numel()) == 0 or not bool(chunk_mask.any().item()): + return 0, 0 + token_count = int(chunk_lengths_tensor[chunk_mask].sum().item()) + run_starts = chunk_mask.clone() + run_starts[1:] = torch.logical_and( + run_starts[1:], torch.logical_not(chunk_mask[:-1]) + ) + range_count = int(run_starts.sum().item()) + return token_count, range_count + token_count = 0 + range_count = 0 + in_run = False + for is_set, length in zip(chunk_mask.tolist(), chunk_lengths, strict=True): + if bool(is_set): + token_count += int(length) + if not in_run: + range_count += 1 + in_run = True + continue + in_run = False + return token_count, range_count + + +def _merge_chunk_ranges_from_mask( + *, + chunk_ranges: tuple[TokenRange, ...], + chunk_mask: torch.Tensor, +) -> tuple[TokenRange, ...]: + chunk_indices = torch.nonzero(chunk_mask, as_tuple=False).flatten() + if int(chunk_indices.numel()) == 0: + return tuple() + ordered_chunk_indices = chunk_indices.tolist() + first_range = chunk_ranges[int(ordered_chunk_indices[0])] + current_start = int(first_range.start) + current_end = int(first_range.end) + merged: list[TokenRange] = [] + for chunk_index in ordered_chunk_indices[1:]: + range_ = chunk_ranges[int(chunk_index)] + if int(range_.start) <= current_end: + current_end = max(current_end, int(range_.end)) + continue + merged.append(TokenRange(start=current_start, end=current_end)) + current_start = int(range_.start) + current_end = int(range_.end) + merged.append(TokenRange(start=current_start, end=current_end)) + return tuple(merged) + + +def _stage_cost_ms( + *, + pair_count: int, + q_tokens: int, + k_tokens: int, + q_range_count: int, + k_range_count: int, + config: ContextParallelConfig, + backward: bool, + local: bool, +) -> float: + pair_ms = ( + config.planner_local_backward_pair_ms + if backward and local + else config.planner_remote_backward_pair_ms + if backward + else config.planner_local_pair_ms + if local + else config.planner_remote_pair_ms + ) + remote_underfill_ms = 0.0 + if not local and (pair_count > 0 or q_tokens > 0 or k_tokens > 0): + token_shortfall = max( + int(config.planner_remote_stage_token_floor) - min(q_tokens, k_tokens), + 0, + ) + pair_shortfall = max( + int(config.planner_remote_stage_pair_floor) - int(pair_count), + 0, + ) + token_scale = ( + float(token_shortfall) / float(config.planner_remote_stage_token_floor) + if int(config.planner_remote_stage_token_floor) > 0 + else 0.0 + ) + pair_scale = ( + float(pair_shortfall) / float(config.planner_remote_stage_pair_floor) + if int(config.planner_remote_stage_pair_floor) > 0 + else 0.0 + ) + remote_underfill_ms = float(config.planner_remote_stage_underfill_ms) * max( + token_scale, + pair_scale, + ) + return ( + float(config.planner_stage_overhead_ms) + + float(pair_count) * float(pair_ms) + + float(q_tokens) * float(config.planner_merge_q_token_ms) + + float(q_range_count + k_range_count) + * float(config.planner_interval_overhead_ms) + + remote_underfill_ms + ) + + +def _comm_cost_ms( + *, + tokens: int, + range_count: int, + config: ContextParallelConfig, + backward: bool, +) -> float: + per_token = ( + float(config.planner_reduce_token_ms) + if backward + else float(config.planner_fetch_token_ms) + ) + if tokens <= 0 and range_count <= 0: + return 0.0 + return ( + float(config.planner_comm_stage_overhead_ms) + + float(tokens) * per_token + + float(range_count) * float(config.planner_interval_overhead_ms) + ) + + +def _simulate_forward_time_ms( + *, + local_stage_ms: float, + remote_stage_ms: tuple[float, ...], + remote_fetch_ms: tuple[float, ...], +) -> float: + if not remote_stage_ms: + return local_stage_ms + + fetch_ready = float(remote_fetch_ms[0]) + current_time = float(local_stage_ms) + for wave_index, stage_ms in enumerate(remote_stage_ms): + compute_start = max(current_time, fetch_ready) + if wave_index + 1 < len(remote_stage_ms): + fetch_ready = compute_start + float(remote_fetch_ms[wave_index + 1]) + current_time = compute_start + float(stage_ms) + return current_time + + +def _simulate_backward_time_ms( + *, + local_stage_ms: float, + remote_stage_ms: tuple[float, ...], + remote_reduce_ms: tuple[float, ...], +) -> float: + if not remote_stage_ms: + return local_stage_ms + + current_time = 0.0 + reduce_ready_times: list[float] = [] + for stage_ms, reduce_ms in zip(remote_stage_ms, remote_reduce_ms): + current_time += float(stage_ms) + reduce_ready_times.append(current_time + float(reduce_ms)) + current_time += float(local_stage_ms) + return max(current_time, max(reduce_ready_times, default=0.0)) + + +def _evaluate_plan( + *, + chunk_ranges: tuple[TokenRange, ...], + pair_matrix: list[list[int]] | torch.Tensor, + owners: tuple[int, ...], + wave_assignment: tuple[int, ...], + cp_size: int, + config: ContextParallelConfig, + pair_positive: torch.Tensor | None = None, + chunk_lengths: tuple[int, ...] | None = None, + chunk_lengths_tensor: torch.Tensor | None = None, +) -> dict[str, Any]: + rank_scores: list[float] = [] + rank_forward_ms: list[float] = [] + rank_backward_ms: list[float] = [] + chunk_count = len(chunk_ranges) + wave_count = max(wave_assignment, default=0) + 1 if wave_assignment else 0 + pair_counts = ( + pair_matrix + if isinstance(pair_matrix, torch.Tensor) and pair_matrix.dtype == torch.int64 + else torch.as_tensor(pair_matrix, dtype=torch.int64) + ) + if pair_positive is None: + pair_positive = pair_counts > 0 + if chunk_lengths is None: + chunk_lengths = tuple(int(range_.size()) for range_ in chunk_ranges) + if ( + chunk_lengths_tensor is None + and len(chunk_lengths) >= _CHUNK_MASK_STATS_TORCH_THRESHOLD + ): + chunk_lengths_tensor = torch.tensor(chunk_lengths, dtype=torch.int64) + owners_tensor = torch.tensor(owners, dtype=torch.int64) + wave_tensor = torch.tensor( + wave_assignment, + dtype=torch.int64, + ) + owner_masks = [owners_tensor == rank for rank in range(cp_size)] + owner_indices = [ + torch.nonzero(owner_mask, as_tuple=False).flatten() + for owner_mask in owner_masks + ] + empty_pair_counts = pair_counts.new_zeros((0, chunk_count)) + empty_pair_positive = pair_positive.new_zeros((0, chunk_count)) + pair_counts_by_rank_rows = [ + empty_pair_counts + if int(owner_index.numel()) == 0 + else pair_counts.index_select(0, owner_index) + for owner_index in owner_indices + ] + pair_positive_by_rank_rows = [ + empty_pair_positive + if int(owner_index.numel()) == 0 + else pair_positive.index_select(0, owner_index) + for owner_index in owner_indices + ] + pair_positive_by_rank_cols = [ + torch.zeros(chunk_count, dtype=torch.bool) + if int(rank_rows.numel()) == 0 + else rank_rows.any(dim=0) + for rank_rows in pair_positive_by_rank_rows + ] + wave_masks = [wave_tensor == wave_index for wave_index in range(wave_count)] + + for rank in range(cp_size): + owned_q_mask = owner_masks[rank] + owned_q_indices = owner_indices[rank] + owned_pair_counts = pair_counts_by_rank_rows[rank] + owned_pair_positive = pair_positive_by_rank_rows[rank] + owned_positive_cols = pair_positive_by_rank_cols[rank] + + local_pairs = ( + 0 + if int(owned_q_indices.numel()) == 0 + else int(owned_pair_counts.index_select(1, owned_q_indices).sum().item()) + ) + local_q_mask = torch.zeros(chunk_count, dtype=torch.bool) + if int(owned_q_indices.numel()) > 0: + touched_local_q = owned_pair_positive.index_select(1, owned_q_indices).any( + dim=1 + ) + if bool(touched_local_q.any().item()): + local_q_mask[owned_q_indices[touched_local_q]] = True + local_k_mask = owned_q_mask & owned_positive_cols + local_q_tokens, local_q_range_count = _chunk_mask_stats( + chunk_lengths=chunk_lengths, + chunk_mask=local_q_mask, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + local_k_tokens, local_k_range_count = _chunk_mask_stats( + chunk_lengths=chunk_lengths, + chunk_mask=local_k_mask, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + local_stage_ms = _stage_cost_ms( + pair_count=local_pairs, + q_tokens=local_q_tokens, + k_tokens=local_k_tokens, + q_range_count=local_q_range_count, + k_range_count=local_k_range_count, + config=config, + backward=False, + local=True, + ) + local_backward_ms = _stage_cost_ms( + pair_count=local_pairs, + q_tokens=local_q_tokens, + k_tokens=local_k_tokens, + q_range_count=local_q_range_count, + k_range_count=local_k_range_count, + config=config, + backward=True, + local=True, + ) + + remote_stage_ms: list[float] = [] + remote_fetch_ms: list[float] = [] + remote_backward_ms: list[float] = [] + remote_reduce_ms: list[float] = [] + for wave_index in range(wave_count): + request_tokens_by_source = [0 for _ in range(cp_size)] + request_range_counts_by_source = [0 for _ in range(cp_size)] + request_pairs = 0 + touched_q_mask = torch.zeros(chunk_count, dtype=torch.bool) + for source_rank in range(cp_size): + if source_rank == rank: + continue + touched_source_mask = ( + owner_masks[source_rank] + & wave_masks[wave_index] + & owned_positive_cols + ) + ( + request_tokens_by_source[source_rank], + request_range_counts_by_source[source_rank], + ) = _chunk_mask_stats( + chunk_lengths=chunk_lengths, + chunk_mask=touched_source_mask, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + if request_tokens_by_source[source_rank] <= 0: + continue + touched_source_indices = torch.nonzero( + touched_source_mask, + as_tuple=False, + ).flatten() + request_pairs += int( + owned_pair_counts.index_select(1, touched_source_indices) + .sum() + .item() + ) + touched_remote_q = owned_pair_positive.index_select( + 1, + touched_source_indices, + ).any(dim=1) + if bool(touched_remote_q.any().item()): + touched_q_mask[owned_q_indices[touched_remote_q]] = True + recv_tokens = sum(request_tokens_by_source) + recv_range_count = sum(request_range_counts_by_source) + if request_pairs <= 0 and recv_tokens <= 0 and recv_range_count <= 0: + continue + + send_tokens_by_peer = [0 for _ in range(cp_size)] + send_range_counts_by_peer = [0 for _ in range(cp_size)] + aggregate_send_mask = torch.zeros(chunk_count, dtype=torch.bool) + owned_wave_mask = owned_q_mask & wave_masks[wave_index] + if bool(owned_wave_mask.any().item()): + for peer_rank in range(cp_size): + if peer_rank == rank: + continue + send_mask = owned_wave_mask & pair_positive_by_rank_cols[peer_rank] + ( + send_tokens_by_peer[peer_rank], + send_range_counts_by_peer[peer_rank], + ) = _chunk_mask_stats( + chunk_lengths=chunk_lengths, + chunk_mask=send_mask, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + if send_tokens_by_peer[peer_rank] > 0: + aggregate_send_mask |= send_mask + ( + send_tokens_by_peer[rank], + send_range_counts_by_peer[rank], + ) = _chunk_mask_stats( + chunk_lengths=chunk_lengths, + chunk_mask=aggregate_send_mask, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + + send_tokens = sum(send_tokens_by_peer) + q_tokens, q_range_count = _chunk_mask_stats( + chunk_lengths=chunk_lengths, + chunk_mask=touched_q_mask, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + remote_stage_ms.append( + _stage_cost_ms( + pair_count=request_pairs, + q_tokens=q_tokens, + k_tokens=recv_tokens, + q_range_count=q_range_count, + k_range_count=recv_range_count, + config=config, + backward=False, + local=False, + ) + ) + remote_backward_ms.append( + _stage_cost_ms( + pair_count=request_pairs, + q_tokens=q_tokens, + k_tokens=recv_tokens, + q_range_count=q_range_count, + k_range_count=recv_range_count, + config=config, + backward=True, + local=False, + ) + ) + remote_fetch_ms.append( + _comm_cost_ms( + tokens=max(send_tokens, recv_tokens), + range_count=max(sum(send_range_counts_by_peer), recv_range_count), + config=config, + backward=False, + ) + ) + remote_reduce_ms.append( + _comm_cost_ms( + tokens=max(send_tokens, recv_tokens), + range_count=max(sum(send_range_counts_by_peer), recv_range_count), + config=config, + backward=True, + ) + ) + + forward_ms = _simulate_forward_time_ms( + local_stage_ms=local_stage_ms if local_pairs > 0 else 0.0, + remote_stage_ms=tuple(remote_stage_ms), + remote_fetch_ms=tuple(remote_fetch_ms), + ) + backward_ms = _simulate_backward_time_ms( + local_stage_ms=local_backward_ms if local_pairs > 0 else 0.0, + remote_stage_ms=tuple(remote_backward_ms), + remote_reduce_ms=tuple(remote_reduce_ms), + ) + rank_forward_ms.append(float(forward_ms)) + rank_backward_ms.append(float(backward_ms)) + rank_scores.append(float(forward_ms + backward_ms)) + return { + "score": max(rank_scores, default=0.0), + "rank_scores": tuple(rank_scores), + "rank_forward_ms": tuple(rank_forward_ms), + "rank_backward_ms": tuple(rank_backward_ms), + } + + +def _evaluate_plan_for_search( + *, + chunk_ranges: tuple[TokenRange, ...], + pair_matrix: list[list[int]] | torch.Tensor, + owners: tuple[int, ...], + wave_assignment: tuple[int, ...], + cp_size: int, + config: ContextParallelConfig, + pair_positive: torch.Tensor | None = None, + chunk_lengths: tuple[int, ...] | None = None, + chunk_lengths_tensor: torch.Tensor | None = None, +) -> dict[str, Any]: + return _evaluate_plan( + chunk_ranges=chunk_ranges, + pair_matrix=pair_matrix, + owners=owners, + wave_assignment=wave_assignment, + cp_size=cp_size, + config=config, + pair_positive=pair_positive, + chunk_lengths=chunk_lengths, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + + +def _search_chunk_assignment( + *, + chunk_ranges: tuple[TokenRange, ...], + pair_matrix: list[list[int]] | torch.Tensor, + q_weights: list[float], + cp_size: int, + config: ContextParallelConfig, +) -> tuple[tuple[int, ...], tuple[int, ...], dict[str, Any]]: + cp_size = int(cp_size) + config = _search_config_for_chunk_count( + config=config, + chunk_count=len(chunk_ranges), + ) + wave_count_candidates = range( + 1, + min(int(config.planner_max_remote_waves), len(chunk_ranges)) + 1, + ) + best_owners: tuple[int, ...] = tuple() + best_waves: tuple[int, ...] = tuple() + best_eval: dict[str, Any] | None = None + eval_cache: dict[tuple[tuple[int, ...], tuple[int, ...]], dict[str, Any]] = {} + pair_counts = torch.as_tensor(pair_matrix, dtype=torch.int64) + pair_positive = pair_counts > 0 + chunk_lengths = tuple(int(range_.size()) for range_ in chunk_ranges) + chunk_lengths_tensor = ( + torch.tensor(chunk_lengths, dtype=torch.int64) + if len(chunk_lengths) >= _CHUNK_MASK_STATS_TORCH_THRESHOLD + else None + ) + + def _evaluate_candidate( + *, + owners: tuple[int, ...], + wave_assignment: tuple[int, ...], + ) -> dict[str, Any]: + cache_key = (owners, wave_assignment) + cached = eval_cache.get(cache_key) + if cached is not None: + return cached + cached = _evaluate_plan_for_search( + chunk_ranges=chunk_ranges, + pair_matrix=pair_counts, + owners=owners, + wave_assignment=wave_assignment, + cp_size=cp_size, + config=config, + pair_positive=pair_positive, + chunk_lengths=chunk_lengths, + chunk_lengths_tensor=chunk_lengths_tensor, + ) + eval_cache[cache_key] = cached + return cached + + def _best_wave_assignment_for_owners( + owners: tuple[int, ...], + ) -> tuple[tuple[int, ...], dict[str, Any]]: + best_wave_assignment = tuple() + best_eval_local: dict[str, Any] | None = None + for wave_count in wave_count_candidates: + wave_assignment = _wave_assignment( + chunk_count=len(chunk_ranges), + wave_count=wave_count, + ) + candidate_eval = _evaluate_candidate( + owners=owners, + wave_assignment=wave_assignment, + ) + if best_eval_local is None or float(candidate_eval["score"]) + 1e-9 < float( + best_eval_local["score"] + ): + best_wave_assignment = wave_assignment + best_eval_local = candidate_eval + if best_eval_local is None: + raise RuntimeError("Failed to evaluate any wave assignment candidate.") + return best_wave_assignment, best_eval_local + + strategy = str(config.planner_assignment_strategy).strip().lower() + striped_owners = _striped_chunk_assignment( + chunk_count=len(chunk_ranges), + cp_size=cp_size, + group_size=int(config.planner_stripe_group_size), + ) + fixed_owners_by_strategy = { + "contiguous": _contiguous_chunk_assignment( + q_weights=q_weights, cp_size=cp_size + ), + "bucket": _bucket_chunk_assignment(q_weights=q_weights, cp_size=cp_size), + "striped": striped_owners, + } + if strategy in fixed_owners_by_strategy: + owners = fixed_owners_by_strategy[strategy] + best_waves, best_eval = _best_wave_assignment_for_owners(owners) + return owners, best_waves, best_eval + if strategy not in {"search", "search_with_striped_seed"}: + raise ValueError( + "Unsupported planner_assignment_strategy=" + f"{config.planner_assignment_strategy!r}." + ) + + contiguous_owners = _contiguous_chunk_assignment( + q_weights=q_weights, + cp_size=cp_size, + ) + for wave_count in wave_count_candidates: + wave_assignment = _wave_assignment( + chunk_count=len(chunk_ranges), + wave_count=wave_count, + ) + initial_candidates = [ + initial_owners + for initial_owners in (contiguous_owners,) + if initial_owners + if _assignment_uses_all_ranks(initial_owners, cp_size=cp_size) + ] + if not initial_candidates: + continue + current_owners = min( + initial_candidates, + key=lambda owners: float( + _evaluate_candidate(owners=owners, wave_assignment=wave_assignment)[ + "score" + ] + ), + ) + current_eval = _evaluate_candidate( + owners=current_owners, + wave_assignment=wave_assignment, + ) + + if cp_size >= 8: + search_steps_remaining = 0 + else: + search_steps_remaining = int(config.planner_max_search_steps) + if cp_size == 4 and search_steps_remaining > 0: + probe_move = _best_improving_move( + current_owners=current_owners, + current_eval=current_eval, + wave_assignment=wave_assignment, + cp_size=cp_size, + q_weights=q_weights, + candidate_limit=min( + int(config.planner_candidate_chunk_limit), + _CP4_SEARCH_PROBE_CANDIDATE_LIMIT, + ), + evaluate_candidate=_evaluate_candidate, + ) + if ( + probe_move is not None + and float(current_eval["score"]) - float(probe_move[1]["score"]) + >= _CP4_SEARCH_PROBE_IMPROVEMENT_MS + ): + current_owners, current_eval = probe_move + search_steps_remaining -= 1 + else: + search_steps_remaining = 0 + + for _ in range(search_steps_remaining): + best_move = _best_improving_move( + current_owners=current_owners, + current_eval=current_eval, + wave_assignment=wave_assignment, + cp_size=cp_size, + q_weights=q_weights, + candidate_limit=int(config.planner_candidate_chunk_limit), + evaluate_candidate=_evaluate_candidate, + ) + if best_move is None: + break + current_owners, current_eval = best_move + + if best_eval is None or float(current_eval["score"]) + 1e-9 < float( + best_eval["score"] + ): + best_owners = current_owners + best_waves = wave_assignment + best_eval = current_eval + + if best_eval is None: + best_owners = _contiguous_chunk_assignment(q_weights=q_weights, cp_size=cp_size) + best_waves = _wave_assignment(chunk_count=len(chunk_ranges), wave_count=1) + best_eval = _evaluate_candidate( + owners=best_owners, + wave_assignment=best_waves, + ) + return best_owners, best_waves, best_eval + + +def _concatenate_peer_ranges( + ranges_by_peer: list[tuple[TokenRange, ...]] | tuple[tuple[TokenRange, ...], ...], +) -> tuple[tuple[TokenRange, ...], ...]: + return tuple(tuple(ranges) for ranges in ranges_by_peer) + + +def _flatten_ranges_by_peer( + ranges_by_peer: tuple[tuple[TokenRange, ...], ...], +) -> tuple[TokenRange, ...]: + return tuple(range_ for peer_ranges in ranges_by_peer for range_ in peer_ranges) + + +def _stage_local_buffer_ranges( + global_ranges: tuple[TokenRange, ...], +) -> tuple[TokenRange, ...]: + cursor = 0 + local_ranges: list[TokenRange] = [] + for range_ in global_ranges: + size = int(range_.size()) + if size <= 0: + continue + local_ranges.append(TokenRange(start=cursor, end=cursor + size)) + cursor += size + return tuple(local_ranges) + + +def _build_stage_from_pieces( + *, + stage_index: int, + source_rank: int, + source_ranks: tuple[int, ...], + is_local_stage: bool, + wave_index: int | None, + pieces: list[StagePiece], + host_local_ranges: tuple[TokenRange, ...], + global_k_ranges: tuple[TokenRange, ...], + local_k_ranges: tuple[TokenRange, ...], + kv_fetch_plan: KvFetchPlan | None, + dkv_reduce_plan: DkvReducePlan | None, + remote_buffer_range: TokenRange | None, + block_size: int, +) -> StagePlan: + global_q_ranges = _merge_ranges([piece[0] for piece in pieces]) + logical_q_len = _ranges_size(global_q_ranges) + logical_k_len = _ranges_size(global_k_ranges) + q_len = ( + 0 + if logical_q_len <= 0 + else ((logical_q_len + int(block_size) - 1) // int(block_size)) + * int(block_size) + ) + k_len = ( + 0 + if logical_k_len <= 0 + else ((logical_k_len + int(block_size) - 1) // int(block_size)) + * int(block_size) + ) + owner_local_q_ranges: tuple[TokenRange, ...] = tuple() + localized_slices: list[AttnSlice] = [] + mask_metadata: ExactMaskMetadata | None = None + q_remap_cache: dict[tuple[int, int], TokenRange] = {} + k_remap_cache: dict[tuple[int, int], TokenRange] = {} + source_index_cache: dict[tuple[int, int], torch.Tensor] = {} + assigned_q_keys: set[tuple[int, int]] = set() + assigned_k_keys: set[tuple[int, int]] = set() + + if global_q_ranges: + owner_local_q_ranges = tuple( + _remap_subrange(range_, host_local_ranges) for range_ in global_q_ranges + ) + q_token_indices = torch.full((q_len,), -1, dtype=torch.int64) + k_token_indices = torch.full((k_len,), -1, dtype=torch.int64) + last_slice_key: StageSliceKey | None = None + for q_piece, k_piece, piece_mask_kind, family_index in sorted( + pieces, + key=lambda piece: ( + int(piece[0].start), + int(piece[0].end), + int(piece[1].start), + int(piece[1].end), + piece[2].value, + -1 if piece[3] is None else int(piece[3]), + ), + ): + q_key = _range_key(q_piece) + k_key = _range_key(k_piece) + localized_q = q_remap_cache.get(q_key) + if localized_q is None: + localized_q = _remap_subrange(q_piece, global_q_ranges) + q_remap_cache[q_key] = localized_q + localized_k = k_remap_cache.get(k_key) + if localized_k is None: + localized_k = _remap_subrange(k_piece, global_k_ranges) + k_remap_cache[k_key] = localized_k + q_source_indices = source_index_cache.get(q_key) + if q_source_indices is None: + q_source_indices = torch.arange( + q_piece.start, q_piece.end, dtype=torch.int64 + ) + source_index_cache[q_key] = q_source_indices + k_source_indices = source_index_cache.get(k_key) + if k_source_indices is None: + k_source_indices = torch.arange( + k_piece.start, k_piece.end, dtype=torch.int64 + ) + source_index_cache[k_key] = k_source_indices + slice_key = ( + 0, + int(localized_q.start), + int(localized_q.end), + int(localized_k.start), + int(localized_k.end), + piece_mask_kind.value, + -1 if family_index is None else int(family_index), + ) + if slice_key != last_slice_key: + localized_slices.append( + AttnSlice( + q_range=localized_q, + k_range=localized_k, + mask_kind=piece_mask_kind, + row_index=0, + family_index=family_index, + ) + ) + last_slice_key = slice_key + if q_key not in assigned_q_keys: + _set_stage_token_indices( + target_indices=q_token_indices, + stage_range=localized_q, + source_range=q_piece, + source_indices=q_source_indices, + ) + assigned_q_keys.add(q_key) + if k_key not in assigned_k_keys: + _set_stage_token_indices( + target_indices=k_token_indices, + stage_range=localized_k, + source_range=k_piece, + source_indices=k_source_indices, + ) + assigned_k_keys.add(k_key) + if localized_slices: + mask_metadata = ExactMaskMetadata( + q_token_indices=q_token_indices, + k_token_indices=k_token_indices, + cache_key=_exact_mask_metadata_cache_key( + q_token_indices=q_token_indices, + k_token_indices=k_token_indices, + ), + ) + return StagePlan( + stage_index=stage_index, + source_rank=source_rank, + source_ranks=source_ranks, + is_local_stage=is_local_stage, + wave_index=wave_index, + slices=tuple(localized_slices), + global_q_ranges=global_q_ranges, + global_k_ranges=global_k_ranges, + owner_local_q_ranges=owner_local_q_ranges, + owner_local_k_ranges=local_k_ranges, + mask_metadata=mask_metadata, + remote_buffer_range=remote_buffer_range, + q_len=q_len, + k_len=k_len, + kv_fetch_plan=kv_fetch_plan, + dkv_reduce_plan=dkv_reduce_plan, + ) + + +def _build_rank_runtime_plan( + *, + row_spec: PackedRowAttentionSpec, + chunk_ranges: tuple[TokenRange, ...], + owners: tuple[int, ...], + wave_assignment: tuple[int, ...], + token_layout_index: TokenLayoutIndex, + cp_size: int, + original_seq_len: int, + target_rank: int, + block_size: int, +) -> RankRuntimePlan: + host_local_ranges = _chunk_ranges_for_owner( + chunk_ranges=chunk_ranges, + owners=owners, + owner_rank=target_rank, + ) + local_row_ranges = ( + tuple(host_local_ranges) + if host_local_ranges + else cast(tuple[TokenRange | None, ...], (None,)) + ) + local_token_count = _ranges_size(host_local_ranges) + ( + local_stage_pieces, + remote_stage_pieces, + recv_request_ranges, + send_request_ranges, + ) = _collect_rank_stage_pieces( + row_spec, + chunk_ranges=chunk_ranges, + owners=owners, + wave_assignment=wave_assignment, + target_rank=target_rank, + cp_size=cp_size, + ) + + stage_plans: list[StagePlan] = [] + local_global_k_ranges = _merge_ranges([piece[1] for piece in local_stage_pieces]) + local_stage = _build_stage_from_pieces( + stage_index=0, + source_rank=target_rank, + source_ranks=(target_rank,), + is_local_stage=True, + wave_index=None, + pieces=local_stage_pieces, + host_local_ranges=host_local_ranges, + global_k_ranges=local_global_k_ranges, + local_k_ranges=tuple( + _remap_subrange(range_, host_local_ranges) + for range_ in local_global_k_ranges + ), + kv_fetch_plan=KvFetchPlan( + send_splits=tuple(0 for _ in range(cp_size)), + recv_splits=tuple(0 for _ in range(cp_size)), + send_ranges_by_peer=tuple(tuple() for _ in range(cp_size)), + ), + dkv_reduce_plan=DkvReducePlan( + send_splits=tuple(0 for _ in range(cp_size)), + recv_splits=tuple(0 for _ in range(cp_size)), + recv_ranges_by_peer=tuple(tuple() for _ in range(cp_size)), + ), + remote_buffer_range=None, + block_size=block_size, + ) + stage_plans.append(local_stage) + + wave_count = max(wave_assignment, default=0) + 1 if wave_assignment else 0 + remote_cursor = 0 + aggregate_send_ranges_by_peer: list[list[TokenRange]] = [[] for _ in range(cp_size)] + aggregate_recv_splits = [0 for _ in range(cp_size)] + backward_stage_indices: list[int] = [] + + for wave_index in range(wave_count): + request_ranges_by_source = tuple( + _merge_ranges(recv_request_ranges[wave_index][source_rank]) + if source_rank != target_rank + else tuple() + for source_rank in range(cp_size) + ) + send_global_ranges_by_peer = tuple( + _merge_ranges(send_request_ranges[wave_index][peer_rank]) + if peer_rank != target_rank + else tuple() + for peer_rank in range(cp_size) + ) + send_ranges_by_peer = tuple( + tuple(_remap_subrange(range_, host_local_ranges) for range_ in peer_ranges) + if peer_rank != target_rank + else tuple() + for peer_rank, peer_ranges in enumerate(send_global_ranges_by_peer) + ) + recv_splits = tuple( + _ranges_size(request_ranges_by_source[source_rank]) + if source_rank != target_rank + else 0 + for source_rank in range(cp_size) + ) + send_splits = tuple( + _ranges_size(peer_ranges) for peer_ranges in send_ranges_by_peer + ) + for peer_rank, peer_ranges in enumerate(send_ranges_by_peer): + if peer_rank == target_rank: + continue + aggregate_send_ranges_by_peer[peer_rank].extend(peer_ranges) + aggregate_recv_splits[peer_rank] += int(recv_splits[peer_rank]) + global_k_ranges = _flatten_ranges_by_peer(request_ranges_by_source) + local_k_ranges = _stage_local_buffer_ranges(global_k_ranges) + stage_k_len = _ranges_size(global_k_ranges) + remote_buffer_range = None + if stage_k_len > 0: + remote_buffer_range = TokenRange( + start=remote_cursor, + end=remote_cursor + stage_k_len, + ) + remote_cursor += stage_k_len + source_ranks = tuple( + source_rank + for source_rank in range(cp_size) + if source_rank != target_rank and recv_splits[source_rank] > 0 + ) + stage_plan = _build_stage_from_pieces( + stage_index=wave_index + 1, + source_rank=-1 if len(source_ranks) != 1 else source_ranks[0], + source_ranks=source_ranks, + is_local_stage=False, + wave_index=wave_index, + pieces=remote_stage_pieces[wave_index], + host_local_ranges=host_local_ranges, + global_k_ranges=global_k_ranges, + local_k_ranges=local_k_ranges, + kv_fetch_plan=KvFetchPlan( + send_splits=send_splits, + recv_splits=recv_splits, + send_ranges_by_peer=send_ranges_by_peer, + ), + dkv_reduce_plan=DkvReducePlan( + send_splits=recv_splits, + recv_splits=send_splits, + recv_ranges_by_peer=send_ranges_by_peer, + ), + remote_buffer_range=remote_buffer_range, + block_size=block_size, + ) + stage_plans.append(stage_plan) + backward_stage_indices.append(int(stage_plan.stage_index)) + + aggregate_send_ranges = tuple( + tuple(peer_ranges) for peer_ranges in aggregate_send_ranges_by_peer + ) + aggregate_send_splits = tuple( + _ranges_size(peer_ranges) for peer_ranges in aggregate_send_ranges + ) + return RankRuntimePlan( + rank=target_rank, + original_seq_len=original_seq_len, + token_layout_index=token_layout_index, + local_valid_lengths=(local_token_count,), + local_row_ranges=local_row_ranges, + local_token_count=local_token_count, + stage_plans=tuple(stage_plans), + backward_stage_indices=tuple(backward_stage_indices + [0]), + remote_kv_fetch_plan=KvFetchPlan( + send_splits=aggregate_send_splits, + recv_splits=tuple(aggregate_recv_splits), + send_ranges_by_peer=aggregate_send_ranges, + ), + remote_dkv_reduce_plan=DkvReducePlan( + send_splits=tuple(aggregate_recv_splits), + recv_splits=aggregate_send_splits, + recv_ranges_by_peer=aggregate_send_ranges, + ), + ) + + +def make_runtime_key( + spec: PackedBatchAttentionSpec, + *, + topology: ParallelTopology, + config: ContextParallelConfig, +) -> ContextParallelRuntimeKey: + if len(spec.rows) != 1: + raise RuntimeError( + "ART context parallel runtime keys expect exactly one packed sequence, " + f"got {len(spec.rows)} rows." + ) + row_signatures = tuple(_row_signature(row) for row in spec.rows) + return ContextParallelRuntimeKey( + topology=topology, + config=config, + row_signatures=row_signatures, + ) + + +def build_context_parallel_token_layout_index( + *, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + topology: ParallelTopology, + config: ContextParallelConfig, + original_seq_len: int, +) -> TokenLayoutIndex: + """Return the token ownership chosen by the real CP attention planner.""" + + spec = build_shared_prefix_attention_spec( + group_ids=group_ids, parent_ids=parent_ids + ) + if int(topology.cp) <= 1: + valid_tokens = int(spec.rows[0].valid_tokens) if spec.rows else 0 + return TokenLayoutIndex( + ownership_ranges_by_rank=(((0, valid_tokens, 0),) if valid_tokens else (),), + token_counts_by_rank=(valid_tokens,), + ) + runtime_config = _config_for_runtime_cp(topology=topology, config=config) + _row_spec, chunk_ranges, owners, _wave_assignment = _runtime_plan_assignment( + spec, + topology=topology, + config=runtime_config, + ) + del original_seq_len + return _build_runtime_token_layout_index( + chunk_ranges=chunk_ranges, + owners=owners, + cp_size=max(int(topology.cp), 1), + ) + + +def prepare_cp_micro( + *, + micro: PackedTensors, + topology: ParallelTopology, + config: ContextParallelConfig, + cp_group: Any, + cp_rank: int, + build_gdn_execution_spec: bool = False, + trace_token_uids: bool = False, + prepare_execution_state: bool = True, + target_device: torch.device | None = None, + ref_logprobs: torch.Tensor | None = None, +) -> PreparedMegatronBatch: + """Prepare one CP microbatch with a CPU-only planning phase. + + The intended overlap contract is: build the shared-prefix/runtime plan from + CPU metadata, then materialize local tensors and BlockMasks on + `target_device`. Passing CUDA `group_ids` or `parent_ids` still works for + older direct callers, but it reintroduces D2H syncs and invalidates the + host-ahead/device-behind lookahead assumption. + """ + state, rank_plan, spec, pad_multiple = prepare_megatron_context_parallel_state( + micro=micro, + topology=topology, + config=config, + cp_group=cp_group, + cp_rank=cp_rank, + build_gdn_execution_spec=build_gdn_execution_spec, + target_device=target_device, + ) + tensors = dispatch_megatron_context_parallel_training_tensors( + micro=micro, + rank_plan=rank_plan, + spec=spec, + pad_multiple=pad_multiple, + trace_token_uids=trace_token_uids, + target_device=target_device, + cp_group=cp_group, + ref_logprobs=ref_logprobs, + ) + if tensors.token_uids is not None: + state = state.model_copy(update={"trace_token_uids": tensors.token_uids}) + if prepare_execution_state: + from .executor import prepare_context_parallel_execution_state + + prepare_context_parallel_execution_state( + state=state, + device=tensors.tokens.device, + ) + return PreparedMegatronBatch( + tensors=tensors, + packed_seq_params=None, + attention_state=state, + rank_plan=rank_plan, + pad_multiple=pad_multiple, + ) + + +def prepare_megatron_context_parallel_state( + *, + micro: PackedTensors, + topology: ParallelTopology, + config: ContextParallelConfig, + cp_group: Any, + cp_rank: int, + build_gdn_execution_spec: bool = False, + target_device: torch.device | None = None, +) -> tuple[ArtContextParallelState, RankRuntimePlan, PackedBatchAttentionSpec, int]: + """Build CP runtime state from CPU metadata. + + This is the portion of CP prepare that must stay free of CUDA reads so the + training loop can run it after enqueueing backward for the previous + microbatch. If device metadata reaches this function, scalar reads, + cache-key hashing, and shared-prefix parsing can block the host on GPU work. + """ + if int(topology.cp) <= 1: + raise RuntimeError( + "prepare_cp_micro is CP-only. Non-CP runs must bypass the context parallel dispatcher in train.py." + ) + if int(micro["tokens"].shape[0]) != 1: + raise RuntimeError( + "ART context parallel currently supports exactly one packed sequence at a time, " + f"got token batch={int(micro['tokens'].shape[0])}." + ) + if int(micro["group_ids"].shape[0]) != 1: + raise RuntimeError( + "ART context parallel currently supports exactly one packed sequence at a time, " + f"got batch={int(micro['group_ids'].shape[0])}." + ) + group_ids_cpu = _planning_metadata_cpu(micro["group_ids"]) + parent_ids_cpu = _planning_metadata_cpu(micro["parent_ids"]) + runtime_config = _config_for_runtime_cp(topology=topology, config=config) + planning_key = _planning_bundle_cache_key( + group_ids=group_ids_cpu, + parent_ids=parent_ids_cpu, + topology=topology, + config=runtime_config, + original_seq_len=int(micro["tokens"].shape[1]), + build_gdn_execution_spec=build_gdn_execution_spec, + ) + bundle = _PLANNING_BUNDLE_CACHE.get(planning_key) + if bundle is None: + spec = build_shared_prefix_attention_spec( + group_ids=group_ids_cpu, + parent_ids=parent_ids_cpu, + ) + runtime_key = make_runtime_key(spec, topology=topology, config=runtime_config) + runtime_plan = get_or_build_runtime_plan( + spec, + topology=topology, + config=runtime_config, + runtime_key=runtime_key, + original_seq_len=int(micro["tokens"].shape[1]), + ) + gdn_execution_spec = None + if build_gdn_execution_spec: + from art.megatron.gdn.gdn_shared_prefix import ( + parse_gdn_shared_prefix_segments, + ) + + gdn_execution_spec = parse_gdn_shared_prefix_segments( + group_ids_cpu, + parent_ids_cpu, + min_completions_per_family=0, + ) + bundle = _PlanningBundle( + spec=spec, + runtime_key=runtime_key, + runtime_plan=runtime_plan, + gdn_execution_spec=gdn_execution_spec, + ) + _cache_put(_PLANNING_BUNDLE_CACHE, planning_key, bundle) + rank_plan = bundle.runtime_plan.rank_plans[int(cp_rank)] + gdn_execution_plan = None + if build_gdn_execution_spec: + if bundle.gdn_execution_spec is None: + raise RuntimeError("GDN CP planning requires a parsed execution spec") + gdn_plan_device = ( + target_device if target_device is not None else micro["tokens"].device + ) + rank_gdn_key = _rank_plan_cache_key( + planning_key=planning_key, + device=gdn_plan_device, + cp_rank=int(cp_rank), + ) + gdn_execution_plan = _GDN_RANK_PLAN_CACHE.get(rank_gdn_key) + if gdn_execution_plan is None: + from art.megatron.gdn.gdn_shared_prefix import ( + build_gdn_rank_execution_plan, + ) + + gdn_execution_plan = build_gdn_rank_execution_plan( + bundle.gdn_execution_spec, + device=gdn_plan_device, + cp_rank=int(cp_rank), + cp_size=int(topology.cp), + attention_token_layout_index=rank_plan.token_layout_index, + ) + _cache_put(_GDN_RANK_PLAN_CACHE, rank_gdn_key, gdn_execution_plan) + planner_provenance = _planner_provenance( + topology=topology, + config=runtime_config, + warn=int(cp_rank) == 0, + ) + pad_multiple = int(topology.tp) if bool(topology.sp) and int(topology.tp) > 1 else 1 + state = ArtContextParallelState( + runtime_key=bundle.runtime_key, + rank_plan=rank_plan, + cp_group=cp_group, + config=runtime_config, + group_ids=group_ids_cpu[0].contiguous(), + parent_ids=parent_ids_cpu[0].contiguous(), + gdn_execution_spec=bundle.gdn_execution_spec, + gdn_execution_plan=gdn_execution_plan, + planner_provenance=planner_provenance, + trace_token_uids=None, + ) + return state, rank_plan, bundle.spec, pad_multiple + + +def dispatch_megatron_context_parallel_training_tensors( + *, + micro: PackedTensors, + rank_plan: RankRuntimePlan, + spec: PackedBatchAttentionSpec, + pad_multiple: int, + trace_token_uids: bool = False, + target_device: torch.device | None = None, + cp_group: Any | None = None, + ref_logprobs: torch.Tensor | None = None, +) -> DispatchedPackedTensors: + """Gather this rank's training tensors and optionally move them to device. + + Dispatch may enqueue H2D copies when `target_device` is CUDA, but it must + not read CUDA metadata back to host. Padding control flow is therefore + derived from rank-plan shape metadata, not from scalar CUDA tensor reads. + """ + dispatch_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], int, str, int | None], + tuple[torch.Tensor, torch.Tensor], + ] = {} + assistant_mask = shift_tensor(micro["assistant_mask"], False) + labels = torch.where( + assistant_mask, + shift_tensor(micro["tokens"], -100), + torch.full_like(micro["tokens"], -100), + ) + old_logprobs = shift_tensor(micro["logprobs"], float("nan")) + advantages = shift_tensor(micro["advantages"], 0.0) + weights = shift_tensor(micro["weights"], 0.0) + shifted_group_ids = shift_tensor(micro["group_ids"], 0) + original_logprobs_source = cast(Any, micro).get("original_logprobs") + original_logprobs = ( + None + if original_logprobs_source is None + else shift_tensor(original_logprobs_source, 0.0) + ) + token_uids = ( + _build_token_uids(spec, seq_len=int(micro["tokens"].shape[1])) + if trace_token_uids + else None + ) + local_tokens = _dispatch_tensor( + micro["tokens"], + rank_plan=rank_plan, + pad_value=0, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + local_labels = _dispatch_tensor( + labels, + rank_plan=rank_plan, + pad_value=-100, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + local_input_pos = _dispatch_tensor( + micro["input_pos"], + rank_plan=rank_plan, + pad_value=0, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + local_assistant_mask = _dispatch_tensor( + assistant_mask, + rank_plan=rank_plan, + pad_value=False, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ).to(dtype=torch.bool) + local_group_ids = _dispatch_tensor( + shifted_group_ids, + rank_plan=rank_plan, + pad_value=0, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + local_old_logprobs = _dispatch_tensor( + old_logprobs, + rank_plan=rank_plan, + pad_value=float("nan"), + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + local_original_logprobs = ( + None + if original_logprobs is None + else _dispatch_tensor( + original_logprobs, + rank_plan=rank_plan, + pad_value=0.0, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + ) + local_ref_logprobs = ( + None + if ref_logprobs is None + else _dispatch_tensor( + ref_logprobs, + rank_plan=rank_plan, + pad_value=float("nan"), + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + ) + local_advantages = _dispatch_tensor( + advantages, + rank_plan=rank_plan, + pad_value=0.0, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + local_weights = _dispatch_tensor( + weights, + rank_plan=rank_plan, + pad_value=0.0, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + local_token_uids = ( + None + if token_uids is None + else _dispatch_tensor( + token_uids, + rank_plan=rank_plan, + pad_value=-1, + pad_multiple=pad_multiple, + dispatch_meta_cache=dispatch_meta_cache, + ) + ) + return DispatchedPackedTensors( + tokens=_to_target_device(local_tokens, target_device), + labels=_to_target_device(local_labels, target_device), + input_pos=_to_target_device(local_input_pos, target_device), + assistant_mask=_to_target_device(local_assistant_mask, target_device), + group_ids=_to_target_device(local_group_ids, target_device), + old_logprobs=_to_target_device(local_old_logprobs, target_device), + advantages=_to_target_device(local_advantages, target_device), + weights=_to_target_device(local_weights, target_device), + valid_lengths=rank_plan.local_valid_lengths, + original_logprobs=None + if local_original_logprobs is None + else _to_target_device(local_original_logprobs, target_device), + ref_logprobs=None + if local_ref_logprobs is None + else _to_target_device(local_ref_logprobs, target_device), + loss_all_reduce_group=cp_group, + token_uids=None if local_token_uids is None else local_token_uids.contiguous(), + ) + + +def get_or_build_runtime_plan( + spec: PackedBatchAttentionSpec, + *, + topology: ParallelTopology, + config: ContextParallelConfig, + runtime_key: ContextParallelRuntimeKey, + original_seq_len: int, +) -> ContextParallelRuntimePlan: + key = ( + _json_cache_key(runtime_key.model_dump(mode="json")), + int(original_seq_len), + ) + cached = _RUNTIME_PLAN_CACHE.get(key) + if cached is not None: + return cached + plan = _build_runtime_plan( + spec, + topology=topology, + config=config, + original_seq_len=original_seq_len, + ) + _cache_put(_RUNTIME_PLAN_CACHE, key, plan) + return plan + + +def get_or_build_rank_runtime_plan( + spec: PackedBatchAttentionSpec, + *, + topology: ParallelTopology, + config: ContextParallelConfig, + runtime_key: ContextParallelRuntimeKey, + original_seq_len: int, + target_rank: int, +) -> RankRuntimePlan: + del runtime_key + return _build_rank_runtime_plan_for_spec( + spec, + topology=topology, + config=config, + original_seq_len=original_seq_len, + target_rank=target_rank, + ) + + +def _runtime_plan_assignment( + spec: PackedBatchAttentionSpec, + *, + topology: ParallelTopology, + config: ContextParallelConfig, +) -> tuple[ + PackedRowAttentionSpec, tuple[TokenRange, ...], tuple[int, ...], tuple[int, ...] +]: + cp_size = max(int(topology.cp), 1) + if len(spec.rows) != 1: + raise RuntimeError( + "ART context parallel runtime planning expects exactly one packed sequence, " + f"got {len(spec.rows)} rows." + ) + row_spec = spec.rows[0] + chunk_size = _normalized_chunk_size( + valid_tokens=int(row_spec.valid_tokens), + block_size=int(config.block_size), + requested_chunk_size=int(config.planner_chunk_size), + cp_size=cp_size, + config=config, + ) + chunk_ranges = _build_chunk_ranges( + valid_tokens=int(row_spec.valid_tokens), + chunk_size=chunk_size, + ) + if len(chunk_ranges) < cp_size and int(row_spec.valid_tokens) >= cp_size: + chunk_ranges = _build_chunk_ranges( + valid_tokens=int(row_spec.valid_tokens), + chunk_size=max(1, int(row_spec.valid_tokens) // cp_size), + ) + pair_matrix, q_weights = _build_chunk_pair_program( + row_spec, + chunk_ranges=chunk_ranges, + ) + owners, wave_assignment, _planner_eval = _search_chunk_assignment( + chunk_ranges=chunk_ranges, + pair_matrix=pair_matrix, + q_weights=q_weights, + cp_size=cp_size, + config=config, + ) + return row_spec, chunk_ranges, owners, wave_assignment + + +def _build_rank_runtime_plan_for_spec( + spec: PackedBatchAttentionSpec, + *, + topology: ParallelTopology, + config: ContextParallelConfig, + original_seq_len: int, + target_rank: int, +) -> RankRuntimePlan: + row_spec, chunk_ranges, owners, wave_assignment = _runtime_plan_assignment( + spec, + topology=topology, + config=config, + ) + cp_size = max(int(topology.cp), 1) + token_layout_index = _build_runtime_token_layout_index( + chunk_ranges=chunk_ranges, + owners=owners, + cp_size=cp_size, + ) + return _build_rank_runtime_plan( + row_spec=row_spec, + chunk_ranges=chunk_ranges, + owners=owners, + wave_assignment=wave_assignment, + token_layout_index=token_layout_index, + cp_size=cp_size, + original_seq_len=original_seq_len, + target_rank=int(target_rank), + block_size=int(config.block_size), + ) + + +def _build_runtime_plan( + spec: PackedBatchAttentionSpec, + *, + topology: ParallelTopology, + config: ContextParallelConfig, + original_seq_len: int, +) -> ContextParallelRuntimePlan: + row_spec, chunk_ranges, owners, wave_assignment = _runtime_plan_assignment( + spec, + topology=topology, + config=config, + ) + cp_size = max(int(topology.cp), 1) + token_layout_index = _build_runtime_token_layout_index( + chunk_ranges=chunk_ranges, + owners=owners, + cp_size=cp_size, + ) + rank_plans = [ + _build_rank_runtime_plan( + row_spec=row_spec, + chunk_ranges=chunk_ranges, + owners=owners, + wave_assignment=wave_assignment, + token_layout_index=token_layout_index, + cp_size=cp_size, + original_seq_len=original_seq_len, + target_rank=rank, + block_size=int(config.block_size), + ) + for rank in range(cp_size) + ] + return ContextParallelRuntimePlan( + topology=topology, + config=config, + token_layout_index=token_layout_index, + rank_plans=tuple(rank_plans), + ) + + +def _build_runtime_token_layout_index( + *, + chunk_ranges: tuple[TokenRange, ...], + owners: tuple[int, ...], + cp_size: int, +) -> TokenLayoutIndex: + ranges_by_rank: list[list[tuple[int, int, int]]] = [[] for _ in range(cp_size)] + rank_positions = [0 for _ in range(cp_size)] + for chunk_range, owner in zip(chunk_ranges, owners, strict=True): + rank = int(owner) + position = rank_positions[rank] + ranges_by_rank[rank].append( + (int(chunk_range.start), int(chunk_range.end), position) + ) + rank_positions[rank] += int(chunk_range.size()) + return TokenLayoutIndex( + ownership_ranges_by_rank=tuple(tuple(ranges) for ranges in ranges_by_rank), + token_counts_by_rank=tuple(rank_positions), + ) + + +def _row_signature(row_spec: PackedRowAttentionSpec) -> str: + payload = { + "valid_tokens": row_spec.valid_tokens, + "slices": [slice_.model_dump(mode="json") for slice_ in row_spec.slices], + } + return json.dumps(payload, sort_keys=True) + + +def _range_key(range_: TokenRange) -> tuple[int, int]: + return (int(range_.start), int(range_.end)) + + +def _set_stage_token_indices( + *, + target_indices: torch.Tensor, + stage_range: TokenRange, + source_range: TokenRange, + source_indices: torch.Tensor, +) -> None: + if stage_range.size() != source_range.size(): + raise RuntimeError( + "Stage-local and packed-sequence token ranges must have matched sizes, got " + f"{stage_range} vs {source_range}" + ) + + current_indices = target_indices[stage_range.start : stage_range.end] + if not bool( + torch.logical_or(current_indices == -1, current_indices == source_indices) + .all() + .item() + ): + mismatch = torch.nonzero( + torch.logical_not( + torch.logical_or( + current_indices == -1, current_indices == source_indices + ) + ), + as_tuple=False, + ).flatten() + mismatch_offset = int(mismatch[0].item()) + mismatch_index = int(stage_range.start) + mismatch_offset + raise RuntimeError( + "Stage mask token index mismatch at stage index " + f"{mismatch_index}: {int(current_indices[mismatch_offset].item())} vs " + f"{int(source_indices[mismatch_offset].item())}" + ) + current_indices.copy_(source_indices) + + +def _token_costs(row_spec: PackedRowAttentionSpec) -> list[float]: + costs = [0.0] * row_spec.valid_tokens + for slice_ in row_spec.slices: + q_range = slice_.q_range + k_range = slice_.k_range + if slice_.mask_kind is AttnMaskKind.FULL: + cost = float(k_range.size()) + for q_idx in range(q_range.start, q_range.end): + costs[q_idx] += cost + continue + if q_range.size() != k_range.size(): + raise RuntimeError( + "The current planner only supports causal slices with matched q/k sizes, got " + f"{q_range} vs {k_range}" + ) + for q_idx in range(q_range.start, q_range.end): + costs[q_idx] += float(q_idx - q_range.start + 1) + return costs + + +def _split_row_by_cost( + row_spec: PackedRowAttentionSpec, + *, + cp_size: int, + block_size: int, +) -> tuple[TokenRange | None, ...]: + if cp_size == 1: + return (TokenRange(start=0, end=row_spec.valid_tokens),) + if row_spec.valid_tokens == 0: + return tuple(None for _ in range(cp_size)) + + costs = _token_costs(row_spec) + prefix = [0.0] + for cost in costs: + prefix.append(prefix[-1] + cost) + total_cost = prefix[-1] + boundaries = [0] + block_aligned_split = int(block_size) > 1 and row_spec.valid_tokens >= ( + cp_size * int(block_size) + ) + for split_index in range(1, cp_size): + remaining_ranks = cp_size - split_index + min_boundary = boundaries[-1] + max_boundary = row_spec.valid_tokens - remaining_ranks + if max_boundary <= min_boundary: + boundaries.append(min_boundary) + continue + target = ( + total_cost * split_index / cp_size + if total_cost > 0.0 + else row_spec.valid_tokens * split_index / cp_size + ) + best_boundary = min_boundary + 1 + best_error = float("inf") + candidate_boundaries = range(min_boundary + 1, max_boundary + 1) + if block_aligned_split: + aligned_start = ( + (min_boundary + 1 + block_size - 1) // block_size + ) * block_size + aligned_end = (max_boundary // block_size) * block_size + if aligned_start <= aligned_end: + candidate_boundaries = range(aligned_start, aligned_end + 1, block_size) + for boundary in candidate_boundaries: + current = prefix[boundary] if total_cost > 0.0 else float(boundary) + error = abs(current - target) + if error < best_error: + best_error = error + best_boundary = boundary + boundaries.append(best_boundary) + boundaries.append(row_spec.valid_tokens) + + ranges: list[TokenRange | None] = [] + for start, end in zip(boundaries[:-1], boundaries[1:]): + if end <= start: + ranges.append(None) + else: + ranges.append(TokenRange(start=start, end=end)) + return tuple(ranges) + + +def _intersections( + base_range: TokenRange, + owner_ranges: tuple[TokenRange | None, ...], +) -> list[tuple[int, TokenRange]]: + intersections: list[tuple[int, TokenRange]] = [] + for rank, owner_range in enumerate(owner_ranges): + if owner_range is None: + continue + start = max(base_range.start, owner_range.start) + end = min(base_range.end, owner_range.end) + if end > start: + intersections.append((rank, TokenRange(start=start, end=end))) + return intersections + + +def _resolve_stage_mask_kind( + *, + mask_kind: AttnMaskKind, + q_piece: TokenRange, + k_piece: TokenRange, +) -> AttnMaskKind | None: + if mask_kind is AttnMaskKind.FULL: + return AttnMaskKind.FULL + if k_piece.start >= q_piece.end: + return None + if k_piece.end <= q_piece.start: + return AttnMaskKind.FULL + return AttnMaskKind.CAUSAL + + +def _merge_ranges(ranges: list[TokenRange]) -> tuple[TokenRange, ...]: + if not ranges: + return tuple() + sorted_ranges = sorted(ranges, key=lambda range_: (range_.start, range_.end)) + merged: list[TokenRange] = [sorted_ranges[0]] + for range_ in sorted_ranges[1:]: + last = merged[-1] + if range_.start <= last.end: + merged[-1] = TokenRange(start=last.start, end=max(last.end, range_.end)) + continue + merged.append(range_) + return tuple(merged) + + +def _remap_subrange( + subrange: TokenRange, + merged_ranges: tuple[TokenRange, ...], +) -> TokenRange: + stage_offset = 0 + for merged_range in merged_ranges: + if subrange.start >= merged_range.start and subrange.end <= merged_range.end: + return TokenRange( + start=stage_offset + subrange.start - merged_range.start, + end=stage_offset + subrange.end - merged_range.start, + ) + stage_offset += merged_range.size() + raise RuntimeError( + "Failed to remap subrange into merged ranges: " + f"subrange={subrange}, merged_ranges={merged_ranges}" + ) + + +def _tensor_sha1(tensor: torch.Tensor) -> str: + cpu_tensor = tensor.detach().contiguous().to(device="cpu", dtype=torch.int64) + return hashlib.sha1(cpu_tensor.numpy().tobytes()).hexdigest() + + +def _exact_mask_metadata_cache_key( + *, + q_token_indices: torch.Tensor, + k_token_indices: torch.Tensor, +) -> str: + return json.dumps( + { + "q_token_indices_sha1": _tensor_sha1(q_token_indices), + "k_token_indices_sha1": _tensor_sha1(k_token_indices), + "q_len": int(q_token_indices.numel()), + "k_len": int(k_token_indices.numel()), + }, + sort_keys=True, + ) + + +def _build_token_uids( + spec: PackedBatchAttentionSpec, + *, + seq_len: int, +) -> torch.Tensor: + tensor = torch.full((len(spec.rows), seq_len), fill_value=-1, dtype=torch.int64) + cursor = 0 + for row_index, row_spec in enumerate(spec.rows): + if row_spec.valid_tokens <= 0: + continue + tensor[row_index, : row_spec.valid_tokens] = torch.arange( + cursor, + cursor + row_spec.valid_tokens, + dtype=torch.int64, + ) + cursor += row_spec.valid_tokens + return tensor + + +def _to_target_device( + tensor: torch.Tensor, + target_device: torch.device | None, +) -> torch.Tensor: + """Move dispatched local tensors without adding host-side device reads.""" + if target_device is None or tensor.device == target_device: + return tensor + return tensor.to(device=target_device, non_blocking=True) + + +def _dispatch_tensor( + tensor: torch.Tensor, + *, + rank_plan: RankRuntimePlan, + pad_value: int | float | bool, + pad_multiple: int = 1, + dispatch_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], int, str, int | None], + tuple[torch.Tensor, torch.Tensor], + ] + | None = None, +) -> torch.Tensor: + """Gather local rows without branching on CUDA tensor values. + + The old `bool(valid_mask.all())` branch synchronized whenever dispatch ran + on CUDA. Use the rank plan's Python length metadata to decide whether a pad + mask is needed. + """ + if tensor.ndim != 2: + raise RuntimeError( + f"_dispatch_tensor expected a rank-2 tensor, got shape {tuple(tensor.shape)}" + ) + if int(tensor.shape[0]) != 1: + raise RuntimeError( + "ART context parallel dispatch expects exactly one packed sequence, " + f"got tensor batch={int(tensor.shape[0])}." + ) + if len(rank_plan.local_valid_lengths) != 1: + raise RuntimeError( + "ART context parallel dispatch expects exactly one packed local sequence length, " + f"got local_valid_lengths={len(rank_plan.local_valid_lengths)}." + ) + max_local_len = max(int(rank_plan.local_valid_lengths[0]), 1) + if pad_multiple > 1 and max_local_len % pad_multiple != 0: + max_local_len = ( + (max_local_len + pad_multiple - 1) // pad_multiple + ) * pad_multiple + gather_index, valid_mask = _dispatch_meta( + rank_plan=rank_plan, + max_local_len=max_local_len, + device=tensor.device, + dispatch_meta_cache=dispatch_meta_cache, + ) + output = torch.gather(tensor, dim=1, index=gather_index) + if int(rank_plan.local_valid_lengths[0]) < max_local_len: + output = output.masked_fill(~valid_mask, pad_value) + return output + + +def _dispatch_meta( + *, + rank_plan: RankRuntimePlan, + max_local_len: int, + device: torch.device, + dispatch_meta_cache: dict[ + tuple[tuple[tuple[int, int], ...], int, str, int | None], + tuple[torch.Tensor, torch.Tensor], + ] + | None = None, +) -> tuple[torch.Tensor, torch.Tensor]: + owner_ranges = tuple( + range_ + for range_ in rank_plan.local_row_ranges + if isinstance(range_, TokenRange) and range_.size() > 0 + ) + key = ( + tuple((range_.start, range_.end) for range_ in owner_ranges), + max_local_len, + device.type, + device.index, + ) + if dispatch_meta_cache is not None: + cached = dispatch_meta_cache.get(key) + if cached is not None: + return cached + + flat_indices_parts = [ + torch.arange(range_.start, range_.end, device=device, dtype=torch.int64) + for range_ in owner_ranges + ] + flat_indices = ( + torch.cat(flat_indices_parts, dim=0) + if flat_indices_parts + else torch.empty((0,), device=device, dtype=torch.int64) + ) + valid_count = int(flat_indices.numel()) + if valid_count < max_local_len: + gather_index = torch.zeros((max_local_len,), device=device, dtype=torch.int64) + if valid_count > 0: + gather_index[:valid_count] = flat_indices + else: + gather_index = flat_indices[:max_local_len].contiguous() + valid_mask = torch.zeros((max_local_len,), device=device, dtype=torch.bool) + if valid_count > 0: + valid_mask[: min(valid_count, max_local_len)] = True + gather_index = gather_index.unsqueeze(0) + valid_mask = valid_mask.unsqueeze(0) + cached = (gather_index, valid_mask) + if dispatch_meta_cache is not None: + dispatch_meta_cache[key] = cached + return cached diff --git a/src/art/megatron/context_parallel/types.py b/src/art/megatron/context_parallel/types.py new file mode 100644 index 000000000..4e8e5250f --- /dev/null +++ b/src/art/megatron/context_parallel/types.py @@ -0,0 +1,311 @@ +from __future__ import annotations + +from enum import Enum +from typing import Any + +from megatron.core.packed_seq_params import PackedSeqParams +from pydantic import BaseModel, ConfigDict, Field +import torch + +from .layout_index import TokenLayoutIndex +from .loss_inputs import ContextParallelLossInputs + + +class AttnMaskKind(str, Enum): + FULL = "full" + CAUSAL = "causal" + + +class TokenRange(BaseModel): + model_config = ConfigDict(frozen=True) + + start: int + end: int + + def size(self) -> int: + return self.end - self.start + + def is_empty(self) -> bool: + return self.end <= self.start + + +class AttnSlice(BaseModel): + model_config = ConfigDict(frozen=True) + + q_range: TokenRange + k_range: TokenRange + mask_kind: AttnMaskKind + row_index: int + family_index: int | None = None + + +class PackedRowAttentionSpec(BaseModel): + model_config = ConfigDict(frozen=True) + + row_index: int + valid_tokens: int + slices: tuple[AttnSlice, ...] + + +class PackedBatchAttentionSpec(BaseModel): + model_config = ConfigDict(frozen=True) + + rows: tuple[PackedRowAttentionSpec, ...] + + +class SharedPrefixBuilderConfig(BaseModel): + model_config = ConfigDict(frozen=True) + + ignore_padding_group_id: int = -1 + require_contiguous_group_runs: bool = True + + +class PlannerCpOverride(BaseModel): + model_config = ConfigDict(frozen=True) + + cp_size: int + block_size: int | None = None + planner_chunk_size: int | None = None + planner_chunk_budget_base: int | None = None + planner_chunk_budget_per_cp_rank: int | None = None + planner_assignment_strategy: str | None = None + planner_stripe_group_size: int | None = None + planner_max_search_steps: int | None = None + planner_candidate_chunk_limit: int | None = None + planner_max_remote_waves: int | None = None + planner_stage_overhead_ms: float | None = None + planner_comm_stage_overhead_ms: float | None = None + planner_interval_overhead_ms: float | None = None + planner_merge_q_token_ms: float | None = None + planner_fetch_token_ms: float | None = None + planner_reduce_token_ms: float | None = None + planner_local_pair_ms: float | None = None + planner_remote_pair_ms: float | None = None + planner_local_backward_pair_ms: float | None = None + planner_remote_backward_pair_ms: float | None = None + planner_remote_stage_token_floor: int | None = None + planner_remote_stage_pair_floor: int | None = None + planner_remote_stage_underfill_ms: float | None = None + planner_tuned_backend: str | None = None + planner_tuned_hardware: str | None = None + planner_tuned_cp_sizes: tuple[int, ...] | None = None + + +class ContextParallelConfig(BaseModel): + model_config = ConfigDict(frozen=True, extra="forbid") + + block_size: int = 128 + planner_chunk_size: int = 512 + planner_chunk_budget_base: int = 128 + planner_chunk_budget_per_cp_rank: int = 16 + planner_assignment_strategy: str = "search" + planner_stripe_group_size: int = 16 + planner_max_search_steps: int = 8 + planner_candidate_chunk_limit: int = 8 + planner_max_remote_waves: int = 4 + planner_stage_overhead_ms: float = 0.287151 + planner_comm_stage_overhead_ms: float = 0.143576 + planner_interval_overhead_ms: float = 0.11486 + planner_merge_q_token_ms: float = 0.00011486 + planner_fetch_token_ms: float = 0.000287151 + planner_reduce_token_ms: float = 0.000287151 + planner_local_pair_ms: float = 0.000000045944 + planner_remote_pair_ms: float = 0.000000048816 + planner_local_backward_pair_ms: float = 0.000000137832 + planner_remote_backward_pair_ms: float = 0.000000149318 + planner_remote_stage_token_floor: int = 4096 + planner_remote_stage_pair_floor: int = 4_000_000 + planner_remote_stage_underfill_ms: float = 0.287151 + planner_tuned_backend: str | None = "art_context_parallel" + planner_tuned_hardware: str | None = "NVIDIA H200" + planner_tuned_cp_sizes: tuple[int, ...] = (2,) + planner_cp_overrides: tuple[PlannerCpOverride, ...] = () + + +class ParallelTopology(BaseModel): + model_config = ConfigDict(frozen=True) + + tp: int = 1 + cp: int = 1 + dp: int = 1 + pp: int = 1 + sp: bool = False + + +class ContextParallelRuntimeKey(BaseModel): + model_config = ConfigDict(frozen=True) + + topology: ParallelTopology + config: ContextParallelConfig + row_signatures: tuple[str, ...] + + +class KvFetchPlan(BaseModel): + model_config = ConfigDict(frozen=True) + + send_splits: tuple[int, ...] + recv_splits: tuple[int, ...] + send_ranges_by_peer: tuple[tuple[TokenRange, ...], ...] + + +class DkvReducePlan(BaseModel): + model_config = ConfigDict(frozen=True) + + send_splits: tuple[int, ...] + recv_splits: tuple[int, ...] + recv_ranges_by_peer: tuple[tuple[TokenRange, ...], ...] + + +class StagePlan(BaseModel): + model_config = ConfigDict(frozen=True) + + stage_index: int + source_rank: int + source_ranks: tuple[int, ...] = () + is_local_stage: bool + wave_index: int | None = None + slices: tuple[AttnSlice, ...] + global_q_ranges: tuple[TokenRange, ...] = () + global_k_ranges: tuple[TokenRange, ...] = () + owner_local_q_ranges: tuple[TokenRange, ...] + owner_local_k_ranges: tuple[TokenRange, ...] + mask_metadata: "ExactMaskMetadata | None" = None + remote_buffer_range: TokenRange | None = None + q_len: int + k_len: int + kv_fetch_plan: KvFetchPlan | None = None + dkv_reduce_plan: DkvReducePlan | None = None + + +class RankRuntimePlan(BaseModel): + model_config = ConfigDict(frozen=True) + + rank: int + original_seq_len: int + token_layout_index: TokenLayoutIndex + local_valid_lengths: tuple[int, ...] + local_row_ranges: tuple[TokenRange | None, ...] + local_token_count: int + stage_plans: tuple[StagePlan, ...] + backward_stage_indices: tuple[int, ...] = () + remote_kv_fetch_plan: KvFetchPlan + remote_dkv_reduce_plan: DkvReducePlan + + +class ContextParallelRuntimePlan(BaseModel): + model_config = ConfigDict(frozen=True) + + topology: ParallelTopology + config: ContextParallelConfig + token_layout_index: TokenLayoutIndex + rank_plans: tuple[RankRuntimePlan, ...] + + +class DispatchedPackedTensors(ContextParallelLossInputs): + model_config = ConfigDict(arbitrary_types_allowed=True) + + tokens: torch.Tensor + labels: torch.Tensor + input_pos: torch.Tensor + assistant_mask: torch.Tensor + group_ids: torch.Tensor + old_logprobs: torch.Tensor + advantages: torch.Tensor + weights: torch.Tensor + valid_lengths: tuple[int, ...] + original_logprobs: torch.Tensor | None = None + ref_logprobs: torch.Tensor | None = None + loss_all_reduce_group: Any | None = None + token_uids: torch.Tensor | None = None + + +class ContextParallelExecutionCache(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + block_masks: dict[Any, Any] = Field(default_factory=dict) + range_indices: dict[Any, torch.Tensor] = Field(default_factory=dict) + range_meta: dict[Any, tuple[torch.Tensor, torch.Tensor, torch.Tensor, int]] = Field( + default_factory=dict + ) + stage_execution_specs: dict[Any, "StageExecutionSpec"] = Field(default_factory=dict) + + +class StageExecutionSpec(BaseModel): + model_config = ConfigDict(frozen=True) + + q_len: int + k_len: int + compile_key: str + mask_metadata: "ExactMaskMetadata | None" = None + + +class PlannerProvenance(BaseModel): + model_config = ConfigDict(frozen=True) + + runtime_backend: str + runtime_hardware: str | None = None + runtime_cp_size: int + tuned_backend: str | None = None + tuned_hardware: str | None = None + tuned_cp_sizes: tuple[int, ...] = () + backend_match: bool + hardware_match: bool + cp_size_match: bool + using_best_effort: bool + warning_message: str | None = None + warning_emitted: bool = False + + +class ArtContextParallelState(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + runtime_key: ContextParallelRuntimeKey + rank_plan: RankRuntimePlan + cp_group: Any + config: ContextParallelConfig + group_ids: torch.Tensor + parent_ids: torch.Tensor + gdn_execution_spec: Any | None = None + gdn_execution_plan: Any | None = None + gdn_hidden_layout: str = "attention" + gdn_input_layout: str | None = None + gdn_output_layout: str | None = None + gdn_attention_original_shape: tuple[int, int, int] | None = None + gdn_attention_original_shapes: dict[int, tuple[int, int, int]] = Field( + default_factory=dict + ) + gdn_attention_token_uids: torch.Tensor | None = None + gdn_active_module: Any | None = None + planner_provenance: PlannerProvenance + trace_token_uids: torch.Tensor | None = None + execution_cache: ContextParallelExecutionCache = Field( + default_factory=ContextParallelExecutionCache + ) + + +class PreparedMegatronBatch(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + tensors: DispatchedPackedTensors + packed_seq_params: PackedSeqParams | None = None + attention_state: Any + rank_plan: RankRuntimePlan | None = None + pad_multiple: int = 1 + + +class FlexMaskSpec(BaseModel): + model_config = ConfigDict(frozen=True) + + q_len: int + k_len: int + block_size: int | tuple[int, int] + slices: tuple[AttnSlice, ...] + exact_mask: "ExactMaskMetadata" + + +class ExactMaskMetadata(BaseModel): + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + + q_token_indices: torch.Tensor + k_token_indices: torch.Tensor + cache_key: str diff --git a/src/art/megatron/flex_attn/__init__.py b/src/art/megatron/flex_attn/__init__.py new file mode 100644 index 000000000..7556f0283 --- /dev/null +++ b/src/art/megatron/flex_attn/__init__.py @@ -0,0 +1 @@ +"""ART Megatron flex-attention integration.""" diff --git a/src/art/megatron/flex_attention.py b/src/art/megatron/flex_attn/attention.py similarity index 85% rename from src/art/megatron/flex_attention.py rename to src/art/megatron/flex_attn/attention.py index 690300762..b5839a250 100644 --- a/src/art/megatron/flex_attention.py +++ b/src/art/megatron/flex_attn/attention.py @@ -1,7 +1,7 @@ """Flex attention plumbing for ART's Megatron backend.""" import math -from typing import Any, ClassVar, cast +from typing import Any, cast from megatron.core.packed_seq_params import PackedSeqParams from megatron.core.process_groups_config import ProcessGroupCollection @@ -11,12 +11,9 @@ from pydantic import BaseModel, ConfigDict import torch from torch import Tensor -from torch.nn.attention.flex_attention import ( - BlockMask, - FlexKernelOptions, - create_block_mask, - flex_attention, -) +from torch.nn.attention.flex_attention import BlockMask, create_block_mask + +from art.megatron.flex_attn.compiled import dense_compiled_flex_attention class SharedPrefixAttentionState(BaseModel): @@ -24,20 +21,11 @@ class SharedPrefixAttentionState(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) block_mask: BlockMask - group_ids: Tensor - parent_ids: Tensor class FlexAttentionWrapper(torch.nn.Module): """Compiled `flex_attention` wrapper with Torchtitan-style inductor options.""" - # Force the regular flex kernel. The flex-decoding specialization has hit - # shared-memory OOMs and symbolic-shape assertions on long packed training sequences. - _kernel_options: ClassVar[FlexKernelOptions] = { - "FORCE_USE_FLEX_ATTENTION": True, - } - _compiled_flex_attention: ClassVar = torch.compile(flex_attention) - def forward( self, q: Tensor, @@ -51,28 +39,28 @@ def forward( # q, k, v are [B, H, S, D] tensors expected by torch.flex_attention. return cast( Tensor, - FlexAttentionWrapper._compiled_flex_attention( + dense_compiled_flex_attention( q, k, v, block_mask=block_mask, scale=scale, enable_gqa=enable_gqa, - kernel_options=FlexAttentionWrapper._kernel_options, ), ) -# Sequence-length churn can break the Inductor backend here. Keep this -# on aot_eager instead. -_compiled_create_block_mask = torch.compile(create_block_mask, backend="aot_eager") +_compiled_create_block_mask = torch.compile( + create_block_mask, + backend="aot_eager", +) def create_shared_prefix_attention_state( group_ids: Tensor, parent_ids: Tensor, ) -> SharedPrefixAttentionState: - """Build a block mask for ART shared-prefix packing. + """Build a compiled block mask for ART shared-prefix packing. Initialized on the device of the group_ids tensor. @@ -103,11 +91,7 @@ def _shared_prefix_mask( group_ids.shape[1], device=group_ids.device, ) - return SharedPrefixAttentionState( - block_mask=block_mask, - group_ids=group_ids, - parent_ids=parent_ids, - ) + return SharedPrefixAttentionState(block_mask=block_mask) class FlexDotProductAttention(torch.nn.Module): diff --git a/src/art/megatron/flex_attn/compiled.py b/src/art/megatron/flex_attn/compiled.py new file mode 100644 index 000000000..ad976754d --- /dev/null +++ b/src/art/megatron/flex_attn/compiled.py @@ -0,0 +1,140 @@ +"""Compiled flex attention entrypoints.""" + +import math +from typing import Any, TypeAlias, cast + +import torch +from torch.nn.attention.flex_attention import ( + AuxRequest, + FlexKernelOptions, + flex_attention, +) + +from art.megatron.flex_attn.flash_dlse_patch import apply_flash_flex_dlse_patch + +apply_flash_flex_dlse_patch() + + +# Integration tests patch this module in-process when they need a non-default +# backend; production ART always uses FLASH here. +_FORCED_FLEX_BACKEND = "FLASH" +_FLASH_LSE_RESCALE = math.log(2.0) +SparseBlockSize: TypeAlias = int | tuple[int, int] + + +def normalize_flex_lse(lse: torch.Tensor) -> torch.Tensor: + if _FORCED_FLEX_BACKEND != "FLASH": + return lse + return lse / _FLASH_LSE_RESCALE + + +_FORCED_FLEX_KERNEL_OPTIONS = cast( + FlexKernelOptions, + {"BACKEND": _FORCED_FLEX_BACKEND}, +) + + +def normalize_sparse_block_size(block_size: SparseBlockSize) -> tuple[int, int]: + if isinstance(block_size, tuple): + if len(block_size) != 2: + raise RuntimeError(f"Expected 2D sparse block size, got {block_size!r}") + return int(block_size[0]), int(block_size[1]) + value = int(block_size) + return value, value + + +def flash_sparse_block_size_for_head_dim( + *, + head_dim: int, + head_dim_v: int, + device: torch.device, +) -> tuple[int, int]: + if _FORCED_FLEX_BACKEND != "FLASH": + return (128, 128) + if device.type != "cuda": + return (128, 128) + major, _minor = torch.cuda.get_device_capability(device) + if major != 9: + return (128, 128) + del head_dim_v + if int(head_dim) <= 128: + return (128, 128) + if int(head_dim) <= 192: + return (128, 96) + return (128, 64) + + +def _forced_flex_attention_dense( + q, + k, + v, + *, + block_mask, + scale, + enable_gqa, + return_aux: AuxRequest | None = None, +): + return flex_attention( + q, + k, + v, + block_mask=block_mask, + scale=scale, + enable_gqa=enable_gqa, + kernel_options=_FORCED_FLEX_KERNEL_OPTIONS, + return_aux=return_aux, + ) + + +def _forced_flex_attention_sparse( + q, + k, + v, + *, + block_mask, + scale, + enable_gqa, + return_aux: AuxRequest | None = None, +): + return flex_attention( + q, + k, + v, + block_mask=block_mask, + scale=scale, + enable_gqa=enable_gqa, + kernel_options=_FORCED_FLEX_KERNEL_OPTIONS, + return_aux=return_aux, + ) + + +def select_sparse_execution_family( + *, + is_local_stage: bool, + q_len: int, + k_len: int, + block_size: SparseBlockSize, +) -> tuple[int, int, str]: + del is_local_stage + q_block, k_block = normalize_sparse_block_size(block_size) + target_q_len = ( + 0 if int(q_len) <= 0 else ((int(q_len) + q_block - 1) // q_block) * q_block + ) + target_k_len = ( + 0 if int(k_len) <= 0 else ((int(k_len) + k_block - 1) // k_block) * k_block + ) + return int(target_q_len), int(target_k_len), "sparse" + + +def get_sparse_compiled_flex_attention(*, family_key: str) -> Any: + del family_key + return sparse_compiled_flex_attention + + +dense_compiled_flex_attention = torch.compile( + _forced_flex_attention_dense, +) + +sparse_compiled_flex_attention = torch.compile( + _forced_flex_attention_sparse, +) diff --git a/src/art/megatron/flex_attn/flash_dlse_patch.py b/src/art/megatron/flex_attn/flash_dlse_patch.py new file mode 100644 index 000000000..ee05012ea --- /dev/null +++ b/src/art/megatron/flex_attn/flash_dlse_patch.py @@ -0,0 +1,500 @@ +"""Torch flex-flash compatibility patches for ART context parallel. + +Remove the dLSE portion once torch upstream threads grad_logsumexp into the +flash flex backward path. Keep the block-sparse tile patch until FA4 exposes a +public autograd-compatible tile override for CUTE flex attention. +""" + +from __future__ import annotations + +import inspect +from typing import Any, cast + +import torch + +_PATCH_APPLIED = False +_TILE_PATCH_APPLIED = False + + +def _sm90_block_sparse_fwd_config(cute_interface: Any, head_dim: int, head_dim_v: int): + del head_dim_v + if int(head_dim) <= 128: + return cute_interface.FwdConfig(128, 128, True, True) + if int(head_dim) <= 192: + return cute_interface.FwdConfig(128, 96, True, True) + return cute_interface.FwdConfig(128, 64, True, True) + + +def _apply_flash_flex_block_sparse_tile_patch() -> None: + global _TILE_PATCH_APPLIED + if _TILE_PATCH_APPLIED: + return + + try: + import flash_attn.cute.interface as cute_interface + except ModuleNotFoundError: + _TILE_PATCH_APPLIED = True + return + + cute_interface_any = cast(Any, cute_interface) + original_tile_size_fwd_sm90 = cute_interface_any._tile_size_fwd_sm90 + + def tile_size_fwd_sm90_art( + head_dim, + head_dim_v, + is_causal, + is_local, + use_block_sparsity, + ): + if use_block_sparsity: + return _sm90_block_sparse_fwd_config( + cute_interface, + int(head_dim), + int(head_dim_v), + ) + return original_tile_size_fwd_sm90( + head_dim, + head_dim_v, + is_causal, + is_local, + use_block_sparsity, + ) + + cute_interface_any._tile_size_fwd_sm90 = tile_size_fwd_sm90_art + _TILE_PATCH_APPLIED = True + + +def _patched_flash_backward_template_source(source: str) -> str: + patched = source + kernel_replacements = ( + ( + '{{def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DK", "DV", "Q_NUM_BLKS", "Q_IDX", "FULL_Q_NUM_BLKS", "FULL_Q_IDX")}}', + '{{def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DLSE", "DK", "DV", "Q_NUM_BLKS", "Q_IDX", "FULL_Q_NUM_BLKS", "FULL_Q_IDX")}}', + ), + ( + '{{def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DK", "DV")}}', + '{{def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DLSE", "DK", "DV")}}', + ), + ( + 'def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DK", "DV")}}', + 'def_kernel("Q", "K", "V", "OUT", "D_OUT", "LSE", "DLSE", "DK", "DV")}}', + ), + ) + for before, after in kernel_replacements: + if before in patched: + patched = patched.replace(before, after, 1) + break + else: + raise RuntimeError( + "Unable to patch flash backward template: missing def_kernel signature" + ) + lse_line = " LSE,\n" + if lse_line not in patched: + raise RuntimeError( + f"Unable to patch flash backward template: missing {lse_line!r}" + ) + patched = patched.replace( + lse_line, + " LSE,\n dlse=DLSE,\n", + 1, + ) + return patched + + +def apply_flash_flex_dlse_patch() -> None: + global _PATCH_APPLIED + _apply_flash_flex_block_sparse_tile_patch() + if _PATCH_APPLIED: + return + + from torch._inductor.codegen.cutedsl.cutedsl_template import CuteDSLTemplate + import torch._inductor.kernel.flex.flex_attention as flex_attention_mod + import torch._inductor.kernel.flex.flex_flash_attention as flex_flash_mod + from torch._inductor.lowering import lowerings + + if ( + "grad_logsumexp" + in inspect.signature( + flex_flash_mod.create_flex_flash_attention_backward_kernel + ).parameters + ): + _PATCH_APPLIED = True + return + + patched_template = CuteDSLTemplate( + name="flash_attention_backward_cutedsl_dlse", + source=_patched_flash_backward_template_source( + flex_flash_mod.flash_attention_backward_cutedsl_template.source + ), + ) + original_lowering = flex_attention_mod.flex_attention_backward + original_flash_builder = flex_flash_mod.create_flex_flash_attention_backward_kernel + + def create_flex_flash_attention_backward_kernel_with_dlse( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + scale, + kernel_options, + sparse_q_block_size, + sparse_kv_block_size, + fw_subgraph_buffer=None, + joint_subgraph_buffer=None, + score_mod_other_buffers=None, + mask_graph_buffer=None, + q_num_blocks=None, + q_indices=None, + full_q_num_blocks=None, + full_q_indices=None, + ): + if grad_logsumexp is None: + return original_flash_builder( + query, + key, + value, + out, + logsumexp, + grad_out, + scale, + kernel_options, + sparse_q_block_size, + sparse_kv_block_size, + fw_subgraph_buffer=fw_subgraph_buffer, + joint_subgraph_buffer=joint_subgraph_buffer, + score_mod_other_buffers=score_mod_other_buffers, + mask_graph_buffer=mask_graph_buffer, + q_num_blocks=q_num_blocks, + q_indices=q_indices, + full_q_num_blocks=full_q_num_blocks, + full_q_indices=full_q_indices, + ) + + if not flex_flash_mod.ensure_flash_available(): + raise RuntimeError("CUTE flash attention not available") + + batch_size, num_heads, seq_len_q, head_dim = query.get_size() + _, num_heads_kv, seq_len_kv, v_head_dim = value.get_size() + device = query.get_device() + dtype = query.get_dtype() + assert device is not None + + grad_query_strides = flex_flash_mod.infer_dense_strides( + [batch_size, num_heads, seq_len_q, head_dim], query.get_stride() + ) + grad_query = flex_flash_mod.empty_strided( + size=[batch_size, num_heads, seq_len_q, head_dim], + stride=grad_query_strides, + dtype=dtype, + device=device, + ) + grad_key_strides = flex_flash_mod.infer_dense_strides( + [batch_size, num_heads_kv, seq_len_kv, head_dim], key.get_stride() + ) + grad_key = flex_flash_mod.empty_strided( + size=[batch_size, num_heads_kv, seq_len_kv, head_dim], + stride=grad_key_strides, + dtype=dtype, + device=device, + ) + grad_value_strides = flex_flash_mod.infer_dense_strides( + [batch_size, num_heads_kv, seq_len_kv, v_head_dim], value.get_stride() + ) + grad_value = flex_flash_mod.empty_strided( + size=[batch_size, num_heads_kv, seq_len_kv, v_head_dim], + stride=grad_value_strides, + dtype=dtype, + device=device, + ) + output_layout = flex_flash_mod.FixedLayout( + device=device, + dtype=dtype, + size=[batch_size, num_heads, seq_len_q, head_dim], + stride=[flex_flash_mod.sympy.sympify(s) for s in grad_query.get_stride()], + ) + + sparse_q_block_size = flex_flash_mod.V.graph.sizevars.guard_int( + sparse_q_block_size + ) + sparse_kv_block_size = flex_flash_mod.V.graph.sizevars.guard_int( + sparse_kv_block_size + ) + + choices: list[Any] = [] + input_nodes = [ + query, + key, + value, + out, + grad_out, + logsumexp, + grad_logsumexp, + grad_key, + grad_value, + ] + + has_block_mask = mask_graph_buffer is not None + if has_block_mask: + assert q_indices is not None + assert full_q_num_blocks is not None + assert full_q_indices is not None + input_nodes.extend( + [ + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ] + ) + + has_score_mod = ( + fw_subgraph_buffer is not None and joint_subgraph_buffer is not None + ) + subgraphs = [] + if has_score_mod: + subgraphs.append(fw_subgraph_buffer) + subgraphs.append(joint_subgraph_buffer) + if has_block_mask: + subgraphs.append(mask_graph_buffer) + + with flex_flash_mod.patch_fixed_layout_indexer_for_cutedsl(): + error = patched_template.maybe_append_choice( + choices, + input_nodes=input_nodes, + layout=output_layout, + mutated_inputs=[grad_key, grad_value], + subgraphs=subgraphs if subgraphs else None, + SM_SCALE=scale, + HAS_SCORE_MOD=has_score_mod, + HAS_BLOCK_MASK=has_block_mask, + SPARSE_Q_BLOCK_SIZE=sparse_q_block_size, + SPARSE_KV_BLOCK_SIZE=sparse_kv_block_size, + ) + + for choice in choices: + flex_flash_mod.wrap_choice_render_with_cutedsl_indexer(choice) + + if error or not choices: + raise RuntimeError(f"CuteDSL template failed: {error}") + + template_output = choices[0].output_node() + return (template_output, grad_key, grad_value, tuple()) + + def flex_attention_backward_with_flash_dlse(*args, **kwargs): + if kwargs: + return original_lowering(*args, **kwargs) + grad_logsumexp = args[6] + if grad_logsumexp is None: + return original_lowering(*args, **kwargs) + + ( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + fw_graph, + joint_graph, + block_mask, + scale, + kernel_options, + score_mod_other_buffers, + mask_mod_other_buffers, + ) = args + ( + _, + _, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + sparse_q_block_size, + sparse_kv_block_size, + mask_graph, + ) = block_mask + + kernel_options, backend = ( + flex_attention_mod._sanitize_kernel_options_for_triton(kernel_options) + ) + if backend != "FLASH": + return original_lowering(*args, **kwargs) + + ( + query, + key, + value, + logsumexp, + grad_out, + grad_logsumexp, + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ) = flex_attention_mod.maybe_realize( + [ + query, + key, + value, + logsumexp, + grad_out, + flex_attention_mod.ExternKernel.require_contiguous(grad_logsumexp), + kv_num_blocks, + kv_indices, + full_kv_num_blocks, + full_kv_indices, + q_num_blocks, + q_indices, + full_q_num_blocks, + full_q_indices, + ] + ) + + device = query.get_device() + dtype = query.get_dtype() + bq, _, seq_len_q, _ = query.get_size() + bkv, _, seq_len_kv, _ = value.get_size() + assert flex_attention_mod.V.graph.sizevars.evaluate_expr( + flex_flash_mod.sympy.Eq(bq, bkv) | flex_flash_mod.sympy.Eq(bkv, 1) + ), f"Bq and Bkv must broadcastable. Got Bq={bq} and Bkv={bkv}" + if query.dtype != key.dtype or query.dtype != value.dtype: + raise ValueError( + "Backward pass with mixed query, key, and value dtype is not supported, " + f"got query.dtype={query.dtype}, key.dtype={key.dtype}, and value.dtype={value.dtype}" + ) + + kernel_options = { + k: flex_attention_mod.V.graph.sizevars.guard_int(v) + if isinstance(v, flex_flash_mod.sympy.Symbol) + else v + for k, v in kernel_options.items() + } + kernel_options.setdefault( + "FLOAT32_PRECISION", flex_attention_mod.get_float32_precision() + ) + kernel_options.setdefault( + "IS_DIVISIBLE", + flex_attention_mod.V.graph.sizevars.statically_known_true( + seq_len_q % 128 == 0 + ) + and flex_attention_mod.V.graph.sizevars.statically_known_true( + seq_len_kv % 128 == 0 + ), + ) + + fwd_placeholder_inps = [ + flex_attention_mod.create_placeholder(name, dtype, device) + for name, dtype in [ + ("score", dtype), + ("b", torch.int32), + ("h", torch.int32), + ("m", torch.int32), + ("n", torch.int32), + ] + ] + fw_subgraph_buffer = flex_attention_mod.build_subgraph_buffer( + fwd_placeholder_inps + list(score_mod_other_buffers), fw_graph + ) + flex_attention_mod.freeze_irnodes(fw_subgraph_buffer) + + joint_placeholder_inps = fwd_placeholder_inps + [ + flex_attention_mod.create_placeholder("grad_score_mod", dtype, device) + ] + joint_graph.graph_module.graph.eliminate_dead_code() + flex_attention_mod.validate_joint_graph(joint_graph.graph_module.graph) + all_joint_outputs = flex_attention_mod.build_subgraph_buffer( + joint_placeholder_inps + list(score_mod_other_buffers), joint_graph + ) + flex_attention_mod.freeze_irnodes(all_joint_outputs) + joint_outputs = flex_attention_mod.process_joint_outputs( + all_joint_outputs, len(joint_placeholder_inps) + ) + + mask_graph_placeholder_inps = [ + flex_attention_mod.create_placeholder(name, dtype, device) + for name, dtype in [ + ("b", torch.int32), + ("h", torch.int32), + ("m", torch.int32), + ("n", torch.int32), + ] + ] + mask_graph_buffer = flex_attention_mod.build_subgraph_buffer( + mask_graph_placeholder_inps + list(mask_mod_other_buffers), mask_graph + ) + flex_attention_mod.freeze_irnodes(mask_graph_buffer) + + if not flex_flash_mod._use_flex_flash_attention_backward( + fw_graph, + mask_graph, + backend=backend, + joint_outputs=joint_outputs, + score_mod_other_buffers=score_mod_other_buffers, + ): + return original_lowering(*args, **kwargs) + + needs_block_mask = not flex_flash_mod.is_trivial_mask_graph( + mask_graph.graph_module + ) + if ( + torch.are_deterministic_algorithms_enabled() + and not torch.is_deterministic_algorithms_warn_only_enabled() + and needs_block_mask + ): + raise NotImplementedError( + "Deterministic backward for flex_attention with block_mask using the FLASH backend " + "is not yet implemented. The TRITON backend supports deterministic backward." + ) + if torch.is_deterministic_algorithms_warn_only_enabled() and needs_block_mask: + flex_attention_mod.warnings.warn( + "Deterministic backward for flex_attention with block_mask using the FLASH backend " + "is not yet implemented. Running non-deterministic backward.", + ) + + score_is_trivial = flex_flash_mod.is_trivial_score_graph(fw_graph.graph_module) + return create_flex_flash_attention_backward_kernel_with_dlse( + query, + key, + value, + out, + logsumexp, + grad_out, + grad_logsumexp, + scale, + kernel_options, + sparse_q_block_size, + sparse_kv_block_size, + fw_subgraph_buffer=None if score_is_trivial else fw_subgraph_buffer, + joint_subgraph_buffer=None + if score_is_trivial + else joint_outputs.grad_input, + score_mod_other_buffers=list(score_mod_other_buffers), + mask_graph_buffer=mask_graph_buffer if needs_block_mask else None, + q_num_blocks=q_num_blocks if needs_block_mask else None, + q_indices=q_indices if needs_block_mask else None, + full_q_num_blocks=full_q_num_blocks if needs_block_mask else None, + full_q_indices=full_q_indices if needs_block_mask else None, + ) + + cast( + Any, flex_flash_mod + ).create_flex_flash_attention_backward_kernel_with_dlse = ( + create_flex_flash_attention_backward_kernel_with_dlse + ) + flex_attention_mod.flex_attention_backward = flex_attention_backward_with_flash_dlse + lowerings[torch.ops.higher_order.flex_attention_backward] = ( + flex_attention_backward_with_flash_dlse + ) + _PATCH_APPLIED = True diff --git a/src/art/megatron/gdn/__init__.py b/src/art/megatron/gdn/__init__.py index 0c62a558d..cd3a0873a 100644 --- a/src/art/megatron/gdn/__init__.py +++ b/src/art/megatron/gdn/__init__.py @@ -1,15 +1,33 @@ """ART helpers for Megatron GatedDeltaNet integration.""" +from .fla_cp import chunk_gated_delta_rule_native_cp from .gdn_shared_prefix import ( GdnPackedExecutionSpec, GdnPackedFamilySpec, + GdnPlannerConfig, + GdnRankExecutionPlan, + GdnSegmentBucketPlan, GdnSegmentSpec, + build_gdn_cp_segment_schedule, + build_gdn_rank_execution_plan, + move_gdn_rank_execution_plan_to_device, parse_gdn_shared_prefix_segments, ) +from .layout import exchange_rank_tensor_all_to_all +from .operator import run_gdn_layer __all__ = [ + "chunk_gated_delta_rule_native_cp", "GdnPackedExecutionSpec", "GdnPackedFamilySpec", + "GdnPlannerConfig", + "GdnRankExecutionPlan", "GdnSegmentSpec", + "GdnSegmentBucketPlan", + "build_gdn_cp_segment_schedule", + "build_gdn_rank_execution_plan", + "exchange_rank_tensor_all_to_all", + "move_gdn_rank_execution_plan_to_device", "parse_gdn_shared_prefix_segments", + "run_gdn_layer", ] diff --git a/src/art/megatron/gdn/conv_gelu.py b/src/art/megatron/gdn/conv_gelu.py index 2da562d3b..2795665e8 100644 --- a/src/art/megatron/gdn/conv_gelu.py +++ b/src/art/megatron/gdn/conv_gelu.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import IntEnum -from typing import Any, cast +from typing import Any import torch from torch import Tensor @@ -86,226 +86,6 @@ def _packed_conv_token_metadata_kernel( tl.store(token_local_t + token, token - start, mask=mask) -@triton.jit -def _conv_gelu_fwd_kernel( - qkv, - conv_initial, - weight, - bias, - lengths, - out, - final, - C: tl.constexpr, - T: tl.constexpr, - K: tl.constexpr, - HAS_BIAS: tl.constexpr, - OUTPUT_FINAL: tl.constexpr, - BLOCK_C: tl.constexpr, - BLOCK_T: tl.constexpr, -): - pid_t = tl.program_id(0) - pid_c = tl.program_id(1) - b = tl.program_id(2) - tail: tl.constexpr = K - 1 - offs_c = pid_c * BLOCK_C + tl.arange(0, BLOCK_C) - offs_t = pid_t * BLOCK_T + tl.arange(0, BLOCK_T) - c = offs_c[:, None] - t = offs_t[None, :] - b64 = b.to(tl.int64) - c64 = c.to(tl.int64) - t64 = t.to(tl.int64) - offs_c64 = offs_c.to(tl.int64) - mask = (offs_c[:, None] < C) & (offs_t[None, :] < T) - acc = tl.zeros((BLOCK_C, BLOCK_T), dtype=tl.float32) - if HAS_BIAS: - acc += tl.load(bias + offs_c, mask=offs_c < C, other=0.0)[:, None].to( - tl.float32 - ) - for j in tl.static_range(0, K): - ext = t + j - ext64 = ext.to(tl.int64) - from_initial = ext < tail - init_idx = (b64 * C + c64) * tail + ext64 - qkv_idx = (b64 * C + c64) * T + (ext64 - tail) - x_init = tl.load(conv_initial + init_idx, mask=mask & from_initial, other=0.0) - x_qkv = tl.load(qkv + qkv_idx, mask=mask & ~from_initial, other=0.0) - w = tl.load(weight + offs_c * K + j, mask=offs_c < C, other=0.0).to(tl.float32) - acc += (x_init + x_qkv).to(tl.float32) * w[:, None] - tl.store(out + (b64 * C + c64) * T + t64, _gelu(acc), mask=mask) - - if OUTPUT_FINAL: - length = tl.load(lengths + b) - for r in tl.static_range(0, tail): - ext = length + r - ext64 = ext.to(tl.int64) - from_initial = ext < tail - init_idx = (b64 * C + offs_c64) * tail + ext64 - qkv_idx = (b64 * C + offs_c64) * T + (ext64 - tail) - x_init = tl.load( - conv_initial + init_idx, - mask=(pid_t == 0) & (offs_c < C) & from_initial, - other=0.0, - ) - x_qkv = tl.load( - qkv + qkv_idx, - mask=(pid_t == 0) & (offs_c < C) & ~from_initial, - other=0.0, - ) - tl.store( - final + (b64 * C + offs_c64) * tail + r, - x_init + x_qkv, - mask=(pid_t == 0) & (offs_c < C), - ) - - -@triton.jit -def _conv_gelu_grad_preact_kernel( - qkv, - conv_initial, - weight, - bias, - grad_out, - grad_preact, - C: tl.constexpr, - T: tl.constexpr, - K: tl.constexpr, - HAS_BIAS: tl.constexpr, - BLOCK_C: tl.constexpr, - BLOCK_T: tl.constexpr, -): - pid_t = tl.program_id(0) - pid_c = tl.program_id(1) - b = tl.program_id(2) - tail: tl.constexpr = K - 1 - offs_c = pid_c * BLOCK_C + tl.arange(0, BLOCK_C) - offs_t = pid_t * BLOCK_T + tl.arange(0, BLOCK_T) - c = offs_c[:, None] - t = offs_t[None, :] - b64 = b.to(tl.int64) - c64 = c.to(tl.int64) - t64 = t.to(tl.int64) - mask = (offs_c[:, None] < C) & (offs_t[None, :] < T) - acc = tl.zeros((BLOCK_C, BLOCK_T), dtype=tl.float32) - if HAS_BIAS: - acc += tl.load(bias + offs_c, mask=offs_c < C, other=0.0)[:, None].to( - tl.float32 - ) - for j in tl.static_range(0, K): - ext = t + j - ext64 = ext.to(tl.int64) - from_initial = ext < tail - init_idx = (b64 * C + c64) * tail + ext64 - qkv_idx = (b64 * C + c64) * T + (ext64 - tail) - x_init = tl.load(conv_initial + init_idx, mask=mask & from_initial, other=0.0) - x_qkv = tl.load(qkv + qkv_idx, mask=mask & ~from_initial, other=0.0) - w = tl.load(weight + offs_c * K + j, mask=offs_c < C, other=0.0).to(tl.float32) - acc += (x_init + x_qkv).to(tl.float32) * w[:, None] - out_idx = (b64 * C + c64) * T + t64 - go = tl.load(grad_out + out_idx, mask=mask, other=0.0).to(tl.float32) - tl.store(grad_preact + out_idx, go * _gelu_grad(acc), mask=mask) - - -@triton.jit -def _conv_gelu_bwd_input_kernel( - grad_preact, - weight, - lengths, - grad_final, - grad_qkv, - grad_initial, - C: tl.constexpr, - T: tl.constexpr, - K: tl.constexpr, - HAS_FINAL_GRAD: tl.constexpr, - BLOCK_C: tl.constexpr, - BLOCK_E: tl.constexpr, -): - pid_e = tl.program_id(0) - pid_c = tl.program_id(1) - b = tl.program_id(2) - tail: tl.constexpr = K - 1 - ext_len: tl.constexpr = T + K - 1 - offs_c = pid_c * BLOCK_C + tl.arange(0, BLOCK_C) - offs_e = pid_e * BLOCK_E + tl.arange(0, BLOCK_E) - c = offs_c[:, None] - e = offs_e[None, :] - b64 = b.to(tl.int64) - c64 = c.to(tl.int64) - e64 = e.to(tl.int64) - mask = (offs_c[:, None] < C) & (offs_e[None, :] < ext_len) - acc = tl.zeros((BLOCK_C, BLOCK_E), dtype=tl.float32) - for j in tl.static_range(0, K): - t = e - j - t64 = t.to(tl.int64) - valid = mask & (t >= 0) & (t < T) - gz = tl.load(grad_preact + (b64 * C + c64) * T + t64, mask=valid, other=0.0) - w = tl.load(weight + offs_c * K + j, mask=offs_c < C, other=0.0).to(tl.float32) - acc += gz.to(tl.float32) * w[:, None] - if HAS_FINAL_GRAD: - length = tl.load(lengths + b) - r = e - length - r64 = r.to(tl.int64) - valid_final = mask & (r >= 0) & (r < tail) - gf = tl.load( - grad_final + (b64 * C + c64) * tail + r64, - mask=valid_final, - other=0.0, - ) - acc += gf.to(tl.float32) - - init_mask = mask & (e < tail) - qkv_mask = mask & (e >= tail) - tl.store(grad_initial + (b64 * C + c64) * tail + e64, acc, mask=init_mask) - tl.store(grad_qkv + (b64 * C + c64) * T + (e64 - tail), acc, mask=qkv_mask) - - -@triton.jit -def _conv_gelu_bwd_weight_kernel( - qkv, - conv_initial, - grad_preact, - grad_weight, - grad_bias, - C: tl.constexpr, - B: tl.constexpr, - T: tl.constexpr, - K: tl.constexpr, - HAS_BIAS: tl.constexpr, - BLOCK_BT: tl.constexpr, -): - c = tl.program_id(0) - tail: tl.constexpr = K - 1 - bt_total: tl.constexpr = B * T - offsets = tl.arange(0, BLOCK_BT) - bias_acc = tl.zeros((BLOCK_BT,), dtype=tl.float32) - for j in tl.static_range(0, K): - weight_acc = tl.zeros((BLOCK_BT,), dtype=tl.float32) - for start in range(0, bt_total, BLOCK_BT): - bt = start + offsets - mask = bt < bt_total - b = bt // T - t = bt - b * T - b64 = b.to(tl.int64) - t64 = t.to(tl.int64) - c64 = c.to(tl.int64) - gz = tl.load(grad_preact + (b64 * C + c64) * T + t64, mask=mask, other=0.0) - ext = t + j - ext64 = ext.to(tl.int64) - from_initial = ext < tail - init_idx = (b64 * C + c64) * tail + ext64 - qkv_idx = (b64 * C + c64) * T + (ext64 - tail) - x_init = tl.load( - conv_initial + init_idx, mask=mask & from_initial, other=0.0 - ) - x_qkv = tl.load(qkv + qkv_idx, mask=mask & ~from_initial, other=0.0) - weight_acc += gz.to(tl.float32) * (x_init + x_qkv).to(tl.float32) - if HAS_BIAS and j == 0: - bias_acc += gz.to(tl.float32) - tl.store(grad_weight + c * K + j, tl.sum(weight_acc, axis=0)) - if HAS_BIAS: - tl.store(grad_bias + c, tl.sum(bias_acc, axis=0)) - - @triton.jit(do_not_specialize=["TOTAL_TOKENS"]) def _packed_conv_fwd_kernel( conv_in, @@ -635,141 +415,16 @@ def _packed_conv_bwd_bias_reduce_kernel( tl.store(grad_bias + offs_c, tl.sum(bias_acc, axis=0), mask=c_mask) -class _VarlenCausalConvGelu(torch.autograd.Function): - @staticmethod - def forward( - ctx: Any, - qkv: Tensor, - conv_initial: Tensor, - weight: Tensor, - bias: Tensor | None, - lengths: Tensor, - output_final_state: bool, - ) -> tuple[Tensor, Tensor | None]: - _validate_inputs(qkv, conv_initial, weight, bias, lengths) - qkv = qkv.contiguous() - conv_initial = conv_initial.contiguous() - weight = weight.contiguous() - bias_tensor = ( - bias.contiguous() - if bias is not None - else torch.empty((0,), device=qkv.device, dtype=qkv.dtype) - ) - lengths = lengths.contiguous() - batch, channels, max_len = qkv.shape - kernel_width = int(weight.shape[1]) - out = torch.empty_like(qkv) - final = ( - torch.empty( - (batch, channels, kernel_width - 1), - device=qkv.device, - dtype=qkv.dtype, - ) - if output_final_state - else None - ) - block_c, block_t, num_warps = _tile_config(channels, max_len) - grid = (triton.cdiv(max_len, block_t), triton.cdiv(channels, block_c), batch) - cast(Any, _conv_gelu_fwd_kernel)[grid]( - qkv, - conv_initial, - weight, - bias_tensor, - lengths, - out, - out if final is None else final, - channels, - max_len, - kernel_width, - HAS_BIAS=bias is not None, - OUTPUT_FINAL=output_final_state, - BLOCK_C=block_c, - BLOCK_T=block_t, - num_warps=num_warps, - ) - ctx.save_for_backward(qkv, conv_initial, weight, bias_tensor, lengths) - ctx.has_bias = bias is not None - ctx.output_final_state = bool(output_final_state) - ctx.tile = (block_c, block_t, num_warps) - return out, final - - @staticmethod - def backward( - ctx: Any, *grad_outputs: Any - ) -> tuple[Tensor, Tensor, Tensor, Tensor | None, None, None]: - grad_out, grad_final = grad_outputs - qkv, conv_initial, weight, bias, lengths = ctx.saved_tensors - grad_out = grad_out.contiguous() - grad_final_tensor = ( - grad_final.contiguous() - if grad_final is not None - else torch.empty((0,), device=qkv.device, dtype=qkv.dtype) - ) - batch, channels, max_len = qkv.shape - kernel_width = int(weight.shape[1]) - grad_qkv = torch.empty_like(qkv) - grad_initial = torch.empty_like(conv_initial) - grad_weight = torch.empty_like(weight) - grad_bias = torch.empty_like(bias) if bool(ctx.has_bias) else None - grad_preact = torch.empty(qkv.shape, device=qkv.device, dtype=torch.float32) - block_c, block_t, num_warps = ctx.tile - grid_t = ( - triton.cdiv(max_len, block_t), - triton.cdiv(channels, block_c), - batch, - ) - cast(Any, _conv_gelu_grad_preact_kernel)[grid_t]( - qkv, - conv_initial, - weight, - bias, - grad_out, - grad_preact, - channels, - max_len, - kernel_width, - HAS_BIAS=bool(ctx.has_bias), - BLOCK_C=block_c, - BLOCK_T=block_t, - num_warps=num_warps, - ) - ext_len = max_len + kernel_width - 1 - grid_e = ( - triton.cdiv(ext_len, block_t), - triton.cdiv(channels, block_c), - batch, - ) - cast(Any, _conv_gelu_bwd_input_kernel)[grid_e]( - grad_preact, - weight, - lengths, - grad_final_tensor, - grad_qkv, - grad_initial, - channels, - max_len, - kernel_width, - HAS_FINAL_GRAD=grad_final is not None, - BLOCK_C=block_c, - BLOCK_E=block_t, - num_warps=num_warps, - ) - reduce_block = 1024 - cast(Any, _conv_gelu_bwd_weight_kernel)[(channels,)]( - qkv, - conv_initial, - grad_preact, - grad_weight, - grad_bias if grad_bias is not None else grad_weight, - channels, - batch, - max_len, - kernel_width, - HAS_BIAS=bool(ctx.has_bias), - BLOCK_BT=reduce_block, - num_warps=8, - ) - return grad_qkv, grad_initial, grad_weight, grad_bias, None, None +_packed_conv_token_metadata_kernel_any: Any = _packed_conv_token_metadata_kernel +_packed_conv_fwd_kernel_any: Any = _packed_conv_fwd_kernel +_packed_conv_final_kernel_any: Any = _packed_conv_final_kernel +_packed_conv_grad_preact_weight_partial_kernel_any: Any = ( + _packed_conv_grad_preact_weight_partial_kernel +) +_packed_conv_bwd_input_kernel_any: Any = _packed_conv_bwd_input_kernel +_packed_conv_bwd_weight_reduce_kernel_any: Any = _packed_conv_bwd_weight_reduce_kernel +_packed_conv_bwd_bias_reduce_kernel_any: Any = _packed_conv_bwd_bias_reduce_kernel +_packed_conv_bwd_initial_kernel_any: Any = _packed_conv_bwd_initial_kernel class _PackedVarlenCausalConv(torch.autograd.Function): @@ -822,7 +477,7 @@ def forward( token_local_t = torch.empty_like(token_segment) if total_tokens > 0: metadata_block_n = 256 - cast(Any, _packed_conv_token_metadata_kernel)[ + _packed_conv_token_metadata_kernel_any[ (triton.cdiv(total_tokens, metadata_block_n),) ]( cu_seqlens, @@ -834,7 +489,7 @@ def forward( BLOCK_N=metadata_block_n, num_warps=4, ) - cast(Any, _packed_conv_fwd_kernel)[ + _packed_conv_fwd_kernel_any[ (triton.cdiv(total_tokens, block_n), triton.cdiv(channels, block_c)) ]( conv_in, @@ -855,7 +510,7 @@ def forward( ) if final is not None and kernel_width > 1 and segments > 0: block_r = _tail_block(kernel_width - 1) - cast(Any, _packed_conv_final_kernel)[ + _packed_conv_final_kernel_any[ ( triton.cdiv(kernel_width - 1, block_r), triton.cdiv(channels, block_c), @@ -889,9 +544,12 @@ def forward( @staticmethod def backward( - ctx: Any, *grad_outputs: Any + ctx: Any, *grad_outputs: Tensor | None ) -> tuple[Tensor, None, Tensor, Tensor, Tensor | None, None, None]: - grad_out, grad_final = grad_outputs + if len(grad_outputs) != 2 or grad_outputs[0] is None: + raise RuntimeError("expected output gradient for packed causal conv") + grad_out = grad_outputs[0] + grad_final = grad_outputs[1] ( conv_in, cu_seqlens, @@ -939,7 +597,7 @@ def backward( token_tiles, channel_tiles, ) - cast(Any, _packed_conv_grad_preact_weight_partial_kernel)[grid_n]( + _packed_conv_grad_preact_weight_partial_kernel_any[grid_n]( conv_in, token_segment, token_local_t, @@ -960,7 +618,7 @@ def backward( BLOCK_C=block_c, num_warps=num_warps, ) - cast(Any, _packed_conv_bwd_input_kernel)[grid_n]( + _packed_conv_bwd_input_kernel_any[grid_n]( cu_seqlens, token_segment, weight, @@ -975,9 +633,7 @@ def backward( BLOCK_C=block_c, num_warps=num_warps, ) - cast(Any, _packed_conv_bwd_weight_reduce_kernel)[ - (channel_tiles, kernel_width) - ]( + _packed_conv_bwd_weight_reduce_kernel_any[(channel_tiles, kernel_width)]( grad_weight_partial, grad_weight, channels, @@ -989,7 +645,7 @@ def backward( num_warps=4, ) if grad_bias is not None: - cast(Any, _packed_conv_bwd_bias_reduce_kernel)[(channel_tiles,)]( + _packed_conv_bwd_bias_reduce_kernel_any[(channel_tiles,)]( grad_bias_partial, grad_bias, channels, @@ -1006,7 +662,7 @@ def backward( grad_bias = torch.zeros_like(bias) if kernel_width > 1 and segments > 0: block_r = _tail_block(kernel_width - 1) - cast(Any, _packed_conv_bwd_initial_kernel)[ + _packed_conv_bwd_initial_kernel_any[ ( triton.cdiv(kernel_width - 1, block_r), triton.cdiv(channels, block_c), @@ -1079,60 +735,6 @@ def packed_varlen_causal_conv_gelu( ) -def varlen_causal_conv_gelu( - qkv: Tensor, - conv_initial: Tensor, - weight: Tensor, - bias: Tensor | None, - lengths: Tensor, - *, - output_final_state: bool = True, -) -> tuple[Tensor, Tensor | None]: - """Run ART GDN's prepared-varlen causal depthwise conv followed by GELU. - - Inputs use the existing prepared GDN layout: ``qkv`` is ``[segments, channels, - max_len]`` with padded positions already zeroed, ``conv_initial`` is - ``[segments, channels, kernel_width - 1]``, and ``lengths`` contains each - segment's real token count. The dense output intentionally matches the - current production conv path over the padded tensor; callers can keep using - the existing real-token mask after this fused operation. - """ - - return _VarlenCausalConvGelu.apply( - qkv, conv_initial, weight, bias, lengths, output_final_state - ) - - -def gdn_varlen_causal_conv_gelu( - gdn: Any, - qkv: Tensor, - conv_initial: Tensor, - lengths: Tensor, - *, - output_final_state: bool = True, -) -> tuple[Tensor, Tensor | None]: - if str(getattr(gdn, "activation", "")) != "gelu": - raise ValueError( - "fused varlen causal conv is only defined for GDN GELU activation, " - f"got {getattr(gdn, 'activation', None)!r}" - ) - return varlen_causal_conv_gelu( - qkv, - conv_initial, - gdn.conv1d.weight.squeeze(1), - gdn.conv1d.bias, - lengths, - output_final_state=output_final_state, - ) - - -def _tile_config(channels: int, max_len: int) -> tuple[int, int, int]: - del channels - if max_len >= 512: - return 2, 128, 4 - return 4, 64, 4 - - def _packed_tile_config(channels: int) -> tuple[int, int, int]: del channels return 128, 16, 4 @@ -1170,57 +772,6 @@ def _assert_valid_cu_seqlens(cu_seqlens: Tensor, total_tokens: int) -> None: torch._assert_async(torch.all(cu_seqlens[1:] >= cu_seqlens[:-1])) -def _validate_inputs( - qkv: Tensor, - conv_initial: Tensor, - weight: Tensor, - bias: Tensor | None, - lengths: Tensor, -) -> None: - if not qkv.is_cuda: - raise ValueError("qkv must be a CUDA tensor") - if qkv.ndim != 3: - raise ValueError(f"qkv must be [segments, channels, max_len], got {qkv.shape}") - if conv_initial.ndim != 3: - raise ValueError( - "conv_initial must be [segments, channels, kernel_width - 1], " - f"got {conv_initial.shape}" - ) - if weight.ndim != 2: - raise ValueError(f"weight must be [channels, kernel_width], got {weight.shape}") - batch, channels, _ = qkv.shape - kernel_width = int(weight.shape[1]) - if kernel_width < 1: - raise ValueError("kernel_width must be at least 1") - if tuple(conv_initial.shape) != (batch, channels, kernel_width - 1): - raise ValueError( - "conv_initial shape must match qkv and weight tail, got " - f"qkv={tuple(qkv.shape)} conv_initial={tuple(conv_initial.shape)} " - f"weight={tuple(weight.shape)}" - ) - if int(weight.shape[0]) != channels: - raise ValueError( - f"weight channels {int(weight.shape[0])} must match qkv channels {channels}" - ) - if bias is not None and tuple(bias.shape) != (channels,): - raise ValueError(f"bias must be [channels], got {tuple(bias.shape)}") - if tuple(lengths.shape) != (batch,): - raise ValueError(f"lengths must be [segments], got {tuple(lengths.shape)}") - if lengths.device != qkv.device: - raise ValueError("lengths must be on the same CUDA device as qkv") - if lengths.dtype not in (torch.int32, torch.int64): - raise ValueError(f"lengths must be int32 or int64, got {lengths.dtype}") - for name, tensor in ( - ("conv_initial", conv_initial), - ("weight", weight), - ("bias", bias), - ): - if tensor is not None and tensor.device != qkv.device: - raise ValueError(f"{name} must be on the same CUDA device as qkv") - if tensor is not None and tensor.dtype != qkv.dtype: - raise ValueError(f"{name} dtype {tensor.dtype} must match qkv {qkv.dtype}") - - def _validate_packed_inputs( conv_in: Tensor, cu_seqlens: Tensor, diff --git a/src/art/megatron/gdn/fla_cp.py b/src/art/megatron/gdn/fla_cp.py new file mode 100644 index 000000000..3629185c6 --- /dev/null +++ b/src/art/megatron/gdn/fla_cp.py @@ -0,0 +1,504 @@ +from __future__ import annotations + +from typing import Any, cast + +import torch +from torch import Tensor +import torch.distributed as dist + + +def chunk_gated_delta_rule_native_cp( + q: Tensor, + k: Tensor, + v: Tensor, + *, + g: Tensor, + beta: Tensor, + initial_state: Tensor, + group: Any, + output_final_state: bool, + cu_seqlens: Tensor | None = None, + cu_seqlens_cpu: Tensor | None = None, + lengths_by_rank_cpu: Tensor | None = None, + scale: float | None = None, +) -> tuple[Tensor, Tensor | None]: + """Run FLA gated-delta recurrence on one CP-sharded logical chain. + + This is the ART-owned extension missing from FLA's public CP surface: + parent recurrent state is injected at rank 0, FLA summary scans seed every + rank-local shard, and chain-tail state is emitted on every rank. + """ + + if group is None: + raise ValueError("native FLA CP GDN requires a process group") + if not dist.is_available() or not dist.is_initialized(): # ty: ignore[possibly-missing-attribute] + raise RuntimeError("torch.distributed must be initialized for native FLA CP") + if q.ndim != 4 or int(q.shape[0]) != 1: + raise ValueError(f"q must be [1, T, H, K], got {tuple(q.shape)}") + if tuple(k.shape) != tuple(q.shape): + raise ValueError(f"k shape must match q, got {tuple(k.shape)}") + if v.ndim != 4 or tuple(v.shape[:3]) != tuple(q.shape[:3]): + raise ValueError(f"v must be [1, T, H, V], got {tuple(v.shape)}") + if tuple(g.shape) != tuple(q.shape[:3]): + raise ValueError(f"g must be [1, T, H], got {tuple(g.shape)}") + if tuple(beta.shape) != tuple(q.shape[:3]): + raise ValueError(f"beta must be [1, T, H], got {tuple(beta.shape)}") + if int(q.shape[1]) <= 0: + raise ValueError("native FLA CP GDN currently requires non-empty rank shards") + if initial_state.ndim != 4: + raise ValueError( + f"initial_state must be [N, H, K, V], got {tuple(initial_state.shape)}" + ) + if cu_seqlens is None and int(initial_state.shape[0]) != 1: + raise ValueError("single-chain native FLA CP requires one initial state") + if cu_seqlens is not None: + if cu_seqlens_cpu is None: + raise ValueError("native FLA CP varlen requires CPU cu_seqlens metadata") + if cu_seqlens.ndim != 1: + raise ValueError( + f"cu_seqlens must be rank 1, got {tuple(cu_seqlens.shape)}" + ) + if cu_seqlens_cpu.ndim != 1: + raise ValueError( + f"cu_seqlens_cpu must be rank 1, got {tuple(cu_seqlens_cpu.shape)}" + ) + if cu_seqlens_cpu.device.type != "cpu": + raise ValueError("native FLA CP cu_seqlens_cpu must stay on CPU") + if int(cu_seqlens.numel()) != int(initial_state.shape[0]) + 1: + raise ValueError( + "cu_seqlens entries must equal initial_state batch + 1, got " + f"{int(cu_seqlens.numel())} and {int(initial_state.shape[0])}" + ) + if int(cu_seqlens_cpu.numel()) != int(cu_seqlens.numel()): + raise ValueError( + "cu_seqlens_cpu entries must match cu_seqlens, got " + f"{int(cu_seqlens_cpu.numel())} and {int(cu_seqlens.numel())}" + ) + if tuple(initial_state.shape[1:3]) != tuple(q.shape[2:4]): + raise ValueError( + "initial_state H/K must match q, got " + f"{tuple(initial_state.shape)} for q {tuple(q.shape)}" + ) + if int(initial_state.shape[-1]) != int(v.shape[-1]): + raise ValueError( + "initial_state V must match v, got " + f"{tuple(initial_state.shape)} for v {tuple(v.shape)}" + ) + if scale is None: + scale = float(k.shape[-1] ** -0.5) + if lengths_by_rank_cpu is None: + raise ValueError("native FLA CP requires static all-rank sequence lengths") + if lengths_by_rank_cpu.device.type != "cpu": + raise ValueError("native FLA CP lengths_by_rank_cpu must stay on CPU") + if tuple(lengths_by_rank_cpu.shape) != ( + dist.get_world_size(group), # ty: ignore[possibly-missing-attribute] + int(initial_state.shape[0]), + ): + raise ValueError( + "native FLA CP lengths_by_rank_cpu must be [world_size, segments], got " + f"{tuple(lengths_by_rank_cpu.shape)}" + ) + if not _fla_chunk_boundaries_aligned_cpu(lengths_by_rank_cpu): + raise ValueError( + "native FLA CP GDN requires 64-token aligned non-final rank " + f"boundaries; lengths_by_rank={lengths_by_rank_cpu.tolist()}" + ) + return _NativeCpChunkGatedDeltaRule.apply( + q, + k, + v, + g, + beta, + initial_state, + cu_seqlens, + cu_seqlens_cpu, + group, + bool(output_final_state), + float(scale), + ) + + +def _fla_chunk_boundaries_aligned_cpu(lengths_by_rank: Tensor) -> bool: + if int(lengths_by_rank.shape[0]) <= 1: + return True + starts = torch.cumsum(lengths_by_rank, dim=0)[:-1] + return bool(torch.all(starts.remainder(64) == 0).item()) + + +class _NativeCpChunkGatedDeltaRule(torch.autograd.Function): + @staticmethod + def forward( + ctx: Any, + q: Tensor, + k: Tensor, + v: Tensor, + g: Tensor, + beta: Tensor, + initial_state: Tensor, + cu_seqlens: Tensor | None, + cu_seqlens_cpu: Tensor | None, + group: Any, + output_final_state: bool, + scale: float, + ) -> tuple[Tensor, Tensor | None]: + from fla.ops.common.chunk_delta_h import chunk_gated_delta_rule_fwd_h + from fla.ops.common.chunk_o import chunk_fwd_o + from fla.ops.common.chunk_scaled_dot_kkt import chunk_scaled_dot_kkt_fwd + from fla.ops.gated_delta_rule.wy_fast import recompute_w_u_fwd + from fla.ops.utils import chunk_local_cumsum, prepare_chunk_indices, solve_tril + + chunk_indices = ( + prepare_chunk_indices(cu_seqlens, 64, cu_seqlens_cpu=cu_seqlens_cpu) + if cu_seqlens is not None + else None + ) + chunk_local_cumsum = cast(Any, chunk_local_cumsum) + chunk_fwd_o = cast(Any, chunk_fwd_o) + chunk_scaled_dot_kkt_fwd = cast(Any, chunk_scaled_dot_kkt_fwd) + solve_tril = cast(Any, solve_tril) + recompute_w_u_fwd = cast(Any, recompute_w_u_fwd) + g_cumsum = chunk_local_cumsum( + g, + chunk_size=64, + cu_seqlens=cu_seqlens, + chunk_indices=chunk_indices, + ) + a = chunk_scaled_dot_kkt_fwd( + k=k, + g=g_cumsum, + beta=beta, + cu_seqlens=cu_seqlens, + chunk_indices=chunk_indices, + output_dtype=torch.float32, + ) + a = solve_tril( + A=a, + cu_seqlens=cu_seqlens, + chunk_indices=chunk_indices, + output_dtype=k.dtype, + ) + w, u = recompute_w_u_fwd( + k=k, + v=v, + beta=beta, + A=a, + g=g_cumsum, + cu_seqlens=cu_seqlens, + chunk_indices=chunk_indices, + ) + summary = _fwd_summary(k=k, w=w, u=u, g=g_cumsum, cu_seqlens=cu_seqlens) + gathered_summary = _all_gather_summary(summary, group) + local_initial = _scan_fwd_initial_state( + gathered_summary, + initial_state, + rank=dist.get_rank(group), # ty: ignore[possibly-missing-attribute] + ) + h, v_new, local_final_state = chunk_gated_delta_rule_fwd_h( + k=k, + w=w, + u=u, + g=g_cumsum, + initial_state=local_initial, + output_final_state=output_final_state, + cu_seqlens=cu_seqlens, + chunk_indices=chunk_indices, + ) + out = chunk_fwd_o( + q=q, + k=k, + v=v_new, + h=h, + g=g_cumsum, + scale=scale, + cu_seqlens=cu_seqlens, + chunk_indices=chunk_indices, + ) + final_state = ( + _broadcast_chain_final_state(local_final_state, group) + if output_final_state + else None + ) + ctx.save_for_backward(q, k, v, g_cumsum, beta, a, local_initial) + ctx.cu_seqlens = cu_seqlens + ctx.chunk_indices = chunk_indices + ctx.group = group + ctx.scale = scale + ctx.output_final_state = output_final_state + return out.to(q.dtype), final_state + + @staticmethod + def backward(ctx: Any, *grad_outputs: Tensor | None) -> tuple[Any, ...]: + from fla.ops.common.chunk_o import chunk_bwd_dv_local + from fla.ops.gated_delta_rule.chunk import chunk_gated_delta_rule_bwd + from fla.ops.gated_delta_rule.wy_fast import recompute_w_u_fwd + + q, k, v, g, beta, a, local_initial = ctx.saved_tensors + do = grad_outputs[0] + if do is None: + do = v.new_zeros(v.shape) + dht = grad_outputs[1] + do = cast(Tensor, do) + recompute_w_u_fwd = cast(Any, recompute_w_u_fwd) + chunk_bwd_dv_local = cast(Any, chunk_bwd_dv_local) + chunk_gated_delta_rule_bwd = cast(Any, chunk_gated_delta_rule_bwd) + w, _ = recompute_w_u_fwd( + k=k, + v=v, + beta=beta, + A=a, + g=g, + cu_seqlens=ctx.cu_seqlens, + chunk_indices=ctx.chunk_indices, + ) + dv_local = chunk_bwd_dv_local( + q=q, + k=k, + g=g, + do=do, + scale=ctx.scale, + cu_seqlens=ctx.cu_seqlens, + chunk_indices=ctx.chunk_indices, + ) + external_dht = _external_final_state_grad( + dht, + local_initial, + group=ctx.group, + enabled=ctx.output_final_state, + ) + bwd_summary = _bwd_summary( + q=q, + k=k, + w=w, + g=g, + do=do, + dv=dv_local, + scale=ctx.scale, + cu_seqlens=ctx.cu_seqlens, + ) + gathered_bwd_summary = _all_gather_summary(bwd_summary, ctx.group) + local_dht = _scan_bwd_local_final_grad( + gathered_bwd_summary, + external_dht, + rank=dist.get_rank(ctx.group), # ty: ignore[possibly-missing-attribute] + ) + dq, dk, dv, db, dg, _dh0, _dA_log, _ddt_bias = chunk_gated_delta_rule_bwd( + q=q, + k=k, + v=v, + g=g, + beta=beta, + A=a, + scale=ctx.scale, + initial_state=local_initial, + do=do, + dht=local_dht, + cu_seqlens=ctx.cu_seqlens, + chunk_indices=ctx.chunk_indices, + use_exp2=False, + ) + dh0 = _scan_bwd_initial_state_grad(gathered_bwd_summary, external_dht) + return ( + dq.to(q), + dk.to(k), + dv.to(v), + dg.to(g), + db.to(beta), + dh0.to(local_initial), + None, + None, + None, + None, + None, + ) + + +def _fwd_summary( + *, k: Tensor, w: Tensor, u: Tensor, g: Tensor, cu_seqlens: Tensor | None = None +) -> Tensor: + from fla.ops.cp.chunk_delta_h import pre_process_fwd_kernel_merged + import triton + + _, token_count, head_count, key_dim = k.shape + value_dim = u.shape[-1] + sequence_count = 1 if cu_seqlens is None else int(cu_seqlens.numel()) - 1 + summary_shape = ( + (head_count, key_dim, value_dim + key_dim) + if cu_seqlens is None + else (sequence_count, head_count, key_dim, value_dim + key_dim) + ) + summary = k.new_zeros(*summary_shape, dtype=torch.float32) + block_size = 32 if key_dim <= 64 else 64 + grid = ( + triton.cdiv(value_dim, block_size) + triton.cdiv(key_dim, block_size), + head_count, + *(() if cu_seqlens is None else (sequence_count,)), + ) + pre_process_fwd_kernel_merged[grid]( + k=k, + v=u, + w=w, + g=g, + gk=None, + bg=None, + u=u, + hm=summary, + cu_seqlens=cu_seqlens, + T=token_count, + H=head_count, + HV=head_count, + K=key_dim, + V=value_dim, + BT=64, + BK1=max(16, triton.next_power_of_2(key_dim)), + USE_EXP2=False, + BLOCK_SIZE=block_size, + MULTI_SEQS=cu_seqlens is not None, + ) + return summary + + +def _bwd_summary( + *, + q: Tensor, + k: Tensor, + w: Tensor, + g: Tensor, + do: Tensor, + dv: Tensor, + scale: float, + cu_seqlens: Tensor | None = None, +) -> Tensor: + from fla.ops.cp.chunk_delta_h import pre_process_bwd_kernel_merged + import triton + + if cu_seqlens is not None: + from .fla_cp_kernels import pre_process_bwd_summary_multi + + return pre_process_bwd_summary_multi( + q=q, + k=k, + w=w, + g=g, + do=do, + dv=dv, + cu_seqlens=cu_seqlens, + scale=scale, + ) + + _, token_count, head_count, key_dim = q.shape + value_dim = do.shape[-1] + sequence_count = 1 if cu_seqlens is None else int(cu_seqlens.numel()) - 1 + summary_shape = ( + (head_count, key_dim, value_dim + key_dim) + if cu_seqlens is None + else (sequence_count, head_count, key_dim, value_dim + key_dim) + ) + summary = q.new_zeros(*summary_shape, dtype=torch.float32) + block_size = 32 if key_dim <= 64 else 64 + grid = ( + triton.cdiv(value_dim, block_size) + triton.cdiv(key_dim, block_size), + head_count, + *(() if cu_seqlens is None else (sequence_count,)), + ) + pre_process_bwd_kernel_merged[grid]( + q=q, + k=k, + w=w, + g=g, + gk=None, + do=do, + dhm=summary, + dv=dv, + cu_seqlens=cu_seqlens, + scale=scale, + T=token_count, + H=head_count, + HV=head_count, + K=key_dim, + V=value_dim, + BT=64, + BK1=max(16, triton.next_power_of_2(key_dim)), + USE_BG=False, + USE_EXP2=False, + BLOCK_SIZE=block_size, + ) + return summary + + +def _all_gather_summary(summary: Tensor, group: Any) -> Tensor: + world_size = dist.get_world_size(group) # ty: ignore[possibly-missing-attribute] + gathered = torch.empty( + world_size, + *summary.shape, + device=summary.device, + dtype=summary.dtype, + ) + dist.all_gather_into_tensor( # ty: ignore[possibly-missing-attribute] + gathered, summary.contiguous(), group=group + ) + return gathered + + +def _scan_fwd_initial_state(summaries: Tensor, h0: Tensor, *, rank: int) -> Tensor: + multi = summaries.ndim == 5 + state = h0.float() if multi else h0[0].float() + for peer in range(rank): + state = _apply_summary(summaries[peer], state) + return state if multi else state.unsqueeze(0) + + +def _broadcast_chain_final_state(final_state: Tensor | None, group: Any) -> Tensor: + if final_state is None: + raise RuntimeError("native FLA CP did not produce a local final state") + owner = dist.get_world_size(group) - 1 # ty: ignore[possibly-missing-attribute] + final_state = final_state.contiguous() + dist.broadcast(final_state, src=owner, group=group) # ty: ignore[possibly-missing-attribute] + return final_state + + +def _scan_bwd_local_final_grad( + summaries: Tensor, + dht: Tensor, + *, + rank: int, +) -> Tensor: + multi = summaries.ndim == 5 + state = dht.float() if multi else dht[0].float() + for peer in range(int(summaries.shape[0]) - 1, rank, -1): + state = _apply_summary(summaries[peer], state) + return state if multi else state.unsqueeze(0) + + +def _scan_bwd_initial_state_grad(summaries: Tensor, dht: Tensor) -> Tensor: + multi = summaries.ndim == 5 + state = dht.float() if multi else dht[0].float() + for peer in range(int(summaries.shape[0]) - 1, -1, -1): + state = _apply_summary(summaries[peer], state) + return state if multi else state.unsqueeze(0) + + +def _apply_summary(summary: Tensor, state: Tensor) -> Tensor: + value_dim = state.shape[-1] + he = summary[..., :value_dim] + transition = summary[..., value_dim:] + return torch.matmul(transition.float(), state.float()) + he.float() + + +def _external_final_state_grad( + dht: Tensor | None, + reference: Tensor, + *, + group: Any, + enabled: bool, +) -> Tensor: + grad = reference.new_zeros(reference.shape, dtype=torch.float32) + if not enabled: + return grad + if dht is not None: + grad = dht.contiguous().float() + dist.all_reduce( # ty: ignore[possibly-missing-attribute] + grad, + op=dist.ReduceOp.SUM, # ty: ignore[possibly-missing-attribute] + group=group, + ) + return grad diff --git a/src/art/megatron/gdn/fla_cp_kernels.py b/src/art/megatron/gdn/fla_cp_kernels.py new file mode 100644 index 000000000..95f3106f0 --- /dev/null +++ b/src/art/megatron/gdn/fla_cp_kernels.py @@ -0,0 +1,474 @@ +# ruff: noqa: E501, PLR0913, PLR0915 +from __future__ import annotations + +from fla.ops.utils.op import exp, exp2 +import torch +from torch import Tensor +import triton +import triton.language as tl + + +def pre_process_bwd_summary_multi( + *, + q: Tensor, + k: Tensor, + w: Tensor, + g: Tensor, + do: Tensor, + dv: Tensor, + cu_seqlens: Tensor, + scale: float, +) -> Tensor: + """Compute FLA CP backward summaries for all varlen chains on device.""" + + _, token_count, head_count, key_dim = q.shape + value_dim = do.shape[-1] + sequence_count = int(cu_seqlens.numel()) - 1 + summary = q.new_zeros( + sequence_count, + head_count, + key_dim, + value_dim + key_dim, + dtype=torch.float32, + ) + block_size = 32 if key_dim <= 64 else 64 + grid = ( + triton.cdiv(value_dim, block_size) + triton.cdiv(key_dim, block_size), + head_count, + sequence_count, + ) + _pre_process_bwd_kernel_merged_multi[grid]( + q=q, + k=k, + w=w, + g=g, + gk=None, + do=do, + dhm=summary, + dv=dv, + cu_seqlens=cu_seqlens, + scale=scale, + T=token_count, + H=head_count, + K=key_dim, + V=value_dim, + BT=64, + BK1=max(16, triton.next_power_of_2(key_dim)), + USE_EXP2=False, + BLOCK_SIZE=block_size, + ) + return summary + + +@triton.heuristics( + { + "USE_G": lambda args: args["g"] is not None, + "USE_GK": lambda args: args["gk"] is not None, + } +) +@triton.jit(do_not_specialize=["T"]) +def _pre_process_bwd_kernel_merged_multi( + q, + k, + w, + g, + gk, + do, + dhm, + dv, + cu_seqlens, + scale, + T, + H: tl.constexpr, + K: tl.constexpr, + V: tl.constexpr, + BT: tl.constexpr, + BLOCK_SIZE: tl.constexpr, + BK1: tl.constexpr, + USE_G: tl.constexpr, + USE_GK: tl.constexpr, + USE_EXP2: tl.constexpr, +): + i_col, i_h, i_n = tl.program_id(0), tl.program_id(1), tl.program_id(2) + bos = tl.load(cu_seqlens + i_n).to(tl.int64) + eos = tl.load(cu_seqlens + i_n + 1).to(tl.int64) + T = (eos - bos).to(tl.int32) + NT = tl.cdiv(T, BT) + + is_dh_part = i_col * BLOCK_SIZE < V + + q += ((bos * H + i_h) * K).to(tl.int64) + k += ((bos * H + i_h) * K).to(tl.int64) + w += ((bos * H + i_h) * K).to(tl.int64) + dhm += ((i_n * H + i_h) * K * (V + K)).to(tl.int64) + stride_k = H * K + + if is_dh_part: + do += ((bos * H + i_h) * V).to(tl.int64) + dv += ((bos * H + i_h) * V).to(tl.int64) + stride_v = H * V + i_v = i_col + + b_dh1 = tl.zeros([64, BLOCK_SIZE], dtype=tl.float32) + if K > 64: + b_dh2 = tl.zeros([64, BLOCK_SIZE], dtype=tl.float32) + if K > 128: + b_dh3 = tl.zeros([64, BLOCK_SIZE], dtype=tl.float32) + if K > 192: + b_dh4 = tl.zeros([64, BLOCK_SIZE], dtype=tl.float32) + + for i_t in range(NT - 1, -1, -1): + last_idx = min((i_t + 1) * BT, T) - 1 + + if USE_G: + bg_last = tl.load(g + (bos + last_idx) * H + i_h).to(tl.float32) + bg_last_exp = exp(bg_last) + p_g = tl.make_block_ptr( + g + bos * H + i_h, + (T,), + (H,), + (i_t * BT,), + (BT,), + (0,), + ) + b_g = tl.load(p_g, boundary_check=(0,)).to(tl.float32) + b_g_exp = exp(b_g) + + p_dv = tl.make_block_ptr( + dv, + (T, V), + (stride_v, 1), + (i_t * BT, i_v * BLOCK_SIZE), + (BT, BLOCK_SIZE), + (1, 0), + ) + p_do = tl.make_block_ptr( + do, + (T, V), + (stride_v, 1), + (i_t * BT, i_v * BLOCK_SIZE), + (BT, BLOCK_SIZE), + (1, 0), + ) + b_do = tl.load(p_do, boundary_check=(0, 1)) + + p_k = tl.make_block_ptr( + k, + (T, K), + (stride_k, 1), + (i_t * BT, 0), + (BT, 64), + (1, 0), + ) + b_k = tl.load(p_k, boundary_check=(0, 1)) + if USE_GK: + o_k1 = tl.arange(0, 64) + b_gk_last1 = tl.load( + gk + last_idx * H * K + o_k1, + mask=o_k1 < K, + other=0.0, + ).to(tl.float32) + b_dv = tl.dot(b_k, b_dh1.to(b_k.dtype)) + + if K > 64: + p_k = tl.make_block_ptr( + k, + (T, K), + (stride_k, 1), + (i_t * BT, 64), + (BT, 64), + (1, 0), + ) + b_k = tl.load(p_k, boundary_check=(0, 1)) + if USE_GK: + o_k2 = 64 + o_k1 + b_gk_last2 = tl.load( + gk + last_idx * H * K + o_k2, + mask=o_k2 < K, + other=0.0, + ).to(tl.float32) + b_dv += tl.dot(b_k, b_dh2.to(b_k.dtype)) + + if K > 128: + p_k = tl.make_block_ptr( + k, + (T, K), + (stride_k, 1), + (i_t * BT, 128), + (BT, 64), + (1, 0), + ) + b_k = tl.load(p_k, boundary_check=(0, 1)) + if USE_GK: + o_k3 = 128 + o_k1 + b_gk_last3 = tl.load( + gk + last_idx * H * K + o_k3, + mask=o_k3 < K, + other=0.0, + ).to(tl.float32) + b_dv += tl.dot(b_k, b_dh3.to(b_k.dtype)) + + if K > 192: + p_k = tl.make_block_ptr( + k, + (T, K), + (stride_k, 1), + (i_t * BT, 192), + (BT, 64), + (1, 0), + ) + b_k = tl.load(p_k, boundary_check=(0, 1)) + if USE_GK: + o_k4 = 192 + o_k1 + b_gk_last4 = tl.load( + gk + last_idx * H * K + o_k4, + mask=o_k4 < K, + other=0.0, + ).to(tl.float32) + b_dv += tl.dot(b_k, b_dh4.to(b_k.dtype)) + + if USE_G: + m_t = (i_t * BT + tl.arange(0, BT)) < T + b_dv *= tl.where(m_t, exp(bg_last - b_g), 0)[:, None] + b_dv += tl.load(p_dv, boundary_check=(0, 1)) + + p_w = tl.make_block_ptr( + w, + (K, T), + (1, stride_k), + (0, i_t * BT), + (64, BT), + (0, 1), + ) + p_q = tl.make_block_ptr( + q, + (K, T), + (1, stride_k), + (0, i_t * BT), + (64, BT), + (0, 1), + ) + b_w = tl.load(p_w, boundary_check=(0, 1)) + b_q = tl.load(p_q, boundary_check=(0, 1)) + if USE_G: + b_dh1 *= bg_last_exp + b_q = b_q * b_g_exp[None, :] + if USE_GK: + if USE_EXP2: + b_dh1 *= exp2(b_gk_last1[:, None]) + else: + b_dh1 *= exp(b_gk_last1[:, None]) + b_dh1 += tl.dot(b_q.to(b_q.dtype), b_do.to(b_q.dtype)) * scale - tl.dot( + b_w, + b_dv.to(b_w.dtype), + ) + + if K > 64: + p_q = tl.make_block_ptr( + q, + (K, T), + (1, stride_k), + (64, i_t * BT), + (64, BT), + (0, 1), + ) + p_w = tl.make_block_ptr( + w, + (K, T), + (1, stride_k), + (64, i_t * BT), + (64, BT), + (0, 1), + ) + b_q = tl.load(p_q, boundary_check=(0, 1)) + b_w = tl.load(p_w, boundary_check=(0, 1)) + if USE_G: + b_dh2 *= bg_last_exp + b_q = b_q * b_g_exp[None, :] + if USE_GK: + if USE_EXP2: + b_dh2 *= exp2(b_gk_last2[:, None]) + else: + b_dh2 *= exp(b_gk_last2[:, None]) + b_dh2 += tl.dot( + b_q.to(b_q.dtype), + b_do.to(b_q.dtype), + ) * scale - tl.dot(b_w, b_dv.to(b_w.dtype)) + + if K > 128: + p_q = tl.make_block_ptr( + q, + (K, T), + (1, stride_k), + (128, i_t * BT), + (64, BT), + (0, 1), + ) + p_w = tl.make_block_ptr( + w, + (K, T), + (1, stride_k), + (128, i_t * BT), + (64, BT), + (0, 1), + ) + b_q = tl.load(p_q, boundary_check=(0, 1)) + b_w = tl.load(p_w, boundary_check=(0, 1)) + if USE_G: + b_dh3 *= bg_last_exp + b_q = b_q * b_g_exp[None, :] + if USE_GK: + if USE_EXP2: + b_dh3 *= exp2(b_gk_last3[:, None]) + else: + b_dh3 *= exp(b_gk_last3[:, None]) + b_dh3 += tl.dot( + b_q.to(b_q.dtype), + b_do.to(b_q.dtype), + ) * scale - tl.dot(b_w, b_dv.to(b_w.dtype)) + + if K > 192: + p_q = tl.make_block_ptr( + q, + (K, T), + (1, stride_k), + (192, i_t * BT), + (64, BT), + (0, 1), + ) + p_w = tl.make_block_ptr( + w, + (K, T), + (1, stride_k), + (192, i_t * BT), + (64, BT), + (0, 1), + ) + b_q = tl.load(p_q, boundary_check=(0, 1)) + b_w = tl.load(p_w, boundary_check=(0, 1)) + if USE_G: + b_dh4 *= bg_last_exp + b_q = b_q * b_g_exp[None, :] + if USE_GK: + if USE_EXP2: + b_dh4 *= exp2(b_gk_last4[:, None]) + else: + b_dh4 *= exp(b_gk_last4[:, None]) + b_dh4 += tl.dot( + b_q.to(b_q.dtype), + b_do.to(b_q.dtype), + ) * scale - tl.dot(b_w, b_dv.to(b_w.dtype)) + + p_dh1 = tl.make_block_ptr( + dhm, + (K, V), + (V + K, 1), + (0, i_v * BLOCK_SIZE), + (64, BLOCK_SIZE), + (1, 0), + ) + tl.store(p_dh1, b_dh1.to(p_dh1.dtype.element_ty), boundary_check=(0, 1)) + if K > 64: + p_dh2 = tl.make_block_ptr( + dhm, + (K, V), + (V + K, 1), + (64, i_v * BLOCK_SIZE), + (64, BLOCK_SIZE), + (1, 0), + ) + tl.store(p_dh2, b_dh2.to(p_dh2.dtype.element_ty), boundary_check=(0, 1)) + if K > 128: + p_dh3 = tl.make_block_ptr( + dhm, + (K, V), + (V + K, 1), + (128, i_v * BLOCK_SIZE), + (64, BLOCK_SIZE), + (1, 0), + ) + tl.store(p_dh3, b_dh3.to(p_dh3.dtype.element_ty), boundary_check=(0, 1)) + if K > 192: + p_dh4 = tl.make_block_ptr( + dhm, + (K, V), + (V + K, 1), + (192, i_v * BLOCK_SIZE), + (64, BLOCK_SIZE), + (1, 0), + ) + tl.store(p_dh4, b_dh4.to(p_dh4.dtype.element_ty), boundary_check=(0, 1)) + else: + i_k_col = i_col - tl.cdiv(V, BLOCK_SIZE) + row = tl.arange(0, BK1) + col = tl.arange(0, BLOCK_SIZE) + i_k_col * BLOCK_SIZE + b_m = tl.where(row[:, None] == col[None, :], 1.0, 0.0) + + for _i_t in range(NT): + i_t = NT - 1 - _i_t + p_k = tl.make_block_ptr( + k, + (T, K), + (stride_k, 1), + (i_t * BT, 0), + (BT, BK1), + (1, 0), + ) + b_k = tl.load(p_k, boundary_check=(0, 1)) + p_w = tl.make_block_ptr( + w, + (T, K), + (stride_k, 1), + (i_t * BT, 0), + (BT, BK1), + (1, 0), + ) + b_w = tl.load(p_w, boundary_check=(0, 1)) + last_idx = min((i_t + 1) * BT, T) - 1 + + if USE_G: + m_t = (i_t * BT + tl.arange(0, BT)) < T + b_g_last = tl.load(g + bos * H + last_idx * H + i_h).to(tl.float32) + p_g = tl.make_block_ptr( + g + bos * H + i_h, + (T,), + (H,), + (i_t * BT,), + (BT,), + (0,), + ) + b_g = tl.load(p_g, boundary_check=(0,)).to(tl.float32) + if USE_EXP2: + b_k = b_k * tl.where(m_t, exp2(b_g_last - b_g), 0)[:, None] + b_g_last = exp2(b_g_last) + else: + b_k = b_k * tl.where(m_t, exp(b_g_last - b_g), 0)[:, None] + b_g_last = exp(b_g_last) + b_diag = tl.where(row[:, None] == row[None, :], b_g_last, 0.0) + elif USE_GK: + b_gk_last = tl.load( + gk + (bos + last_idx) * H * K + i_h * K + row, + mask=row < K, + other=0.0, + ).to(tl.float32) + if USE_EXP2: + b_gk_last = exp2(b_gk_last) + else: + b_gk_last = exp(b_gk_last) + b_diag = tl.where(row[:, None] == row[None, :], b_gk_last[:, None], 0.0) + else: + b_diag = tl.where(row[:, None] == row[None, :], 1.0, 0.0) + + b_kw = tl.dot(tl.trans(b_w), b_k.to(b_w.dtype)) + b_m_i = b_diag - b_kw + b_m = tl.dot(b_m_i.to(tl.float32), b_m.to(tl.float32)) + + p_m = tl.make_block_ptr( + dhm + V, + (K, K), + (V + K, 1), + (0, i_k_col * BLOCK_SIZE), + (BK1, BLOCK_SIZE), + (1, 0), + ) + tl.store(p_m, b_m.to(p_m.dtype.element_ty), boundary_check=(0, 1)) diff --git a/src/art/megatron/gdn/gdn_shared_prefix.py b/src/art/megatron/gdn/gdn_shared_prefix.py index 86f39fdd2..3fb693891 100644 --- a/src/art/megatron/gdn/gdn_shared_prefix.py +++ b/src/art/megatron/gdn/gdn_shared_prefix.py @@ -9,6 +9,7 @@ from art.megatron.context_parallel.layout_index import TokenLayoutIndex GdnSegmentKind = Literal["prefix", "completion"] +GdnSegmentDecisionKey = tuple[int, int, int] # FLA's public chunk_gated_delta_rule hard-codes 64-token WY chunks. FLA_CHUNK_SIZE = 64 _PydanticModelT = TypeVar("_PydanticModelT", bound=BaseModel) @@ -137,8 +138,11 @@ class GdnSegmentBucketPlan(BaseModel): length: int = Field(ge=1) lengths: torch.Tensor + lengths_cpu: torch.Tensor + lengths_by_rank_cpu: torch.Tensor | None = None real_mask: torch.Tensor cu_seqlens: torch.Tensor + cu_seqlens_cpu: torch.Tensor row_indices: torch.Tensor position_indices: torch.Tensor family_indices: torch.Tensor @@ -165,40 +169,6 @@ class GdnParentStateTransferPlan(BaseModel): family_indices_tensor: torch.Tensor | None = None -class GdnCpPeerTransfer(BaseModel): - """Token rows sent from one source rank to one destination rank.""" - - model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) - - source_rank: int = Field(ge=0) - dest_rank: int = Field(ge=0) - token_count: int = Field(ge=0) - source_positions_tensor: torch.Tensor | None = None - dest_positions_tensor: torch.Tensor | None = None - - -class GdnCpExchangePlan(BaseModel): - """Minimal exchange metadata for local GDN plans.""" - - model_config = ConfigDict(frozen=True) - - cp_size: int = Field(ge=1) - source_token_counts_by_rank: tuple[int, ...] - dest_token_counts_by_rank: tuple[int, ...] - transfers: tuple[GdnCpPeerTransfer, ...] - cross_rank_token_count_override: int | None = Field(default=None, ge=0) - - @property - def cross_rank_token_count(self) -> int: - if self.cross_rank_token_count_override is not None: - return int(self.cross_rank_token_count_override) - return sum( - int(transfer.token_count) - for transfer in self.transfers - if transfer.source_rank != transfer.dest_rank - ) - - class GdnPlannerConfig(BaseModel): """Tunable cost coefficients for one packed-row GDN execution plan.""" @@ -211,13 +181,27 @@ class GdnPlannerConfig(BaseModel): cp_chain_min_prefix_only_tokens: int = Field(default=32768, ge=1) local_fork_launch_penalty_tokens: int = Field(default=256, ge=0) cp_collective_latency_tokens: int = Field(default=512, ge=0) - parent_state_exchange_penalty_tokens: int = Field(default=2048, ge=0) - layout_cross_rank_token_cost: float = Field(default=2.0, ge=0.0) + parent_state_exchange_penalty_tokens: int = Field(default=16384, ge=0) + layout_cross_rank_token_cost: float = Field(default=6.0, ge=0.0) rank_idle_token_cost: float = Field(default=1.0, ge=0.0) empty_rank_penalty_tokens: int = Field(default=65536, ge=0) max_zero_exchange_load_imbalance: float = Field(default=1.5, ge=1.0) local_completion_rebalance_min_imbalance: float = Field(default=1.08, ge=1.0) - cp_schedule_improve_iters: int = Field(default=0, ge=0) + cp_chain_beam_width: int = Field(default=2, ge=1) + cp_chain_beam_branch_factor: int = Field(default=4, ge=1) + cp_chain_beam_candidate_limit: int = Field(default=16, ge=1) + cp_chain_beam_max_steps: int = Field(default=4, ge=0) + cp_chain_beam_min_score_delta_tokens: float = Field(default=512.0, ge=0.0) + cp_chain_min_score_delta_ms: float = Field(default=0.25, ge=0.0) + planner_local_token_ms: float = Field(default=0.00065, ge=0.0) + planner_chain_token_ms: float = Field(default=0.00055, ge=0.0) + planner_local_bucket_ms: float = Field(default=0.25, ge=0.0) + planner_chain_bucket_ms: float = Field(default=22.0, ge=0.0) + planner_local_segment_ms: float = Field(default=0.010, ge=0.0) + planner_layout_cross_rank_token_ms: float = Field(default=0.00008, ge=0.0) + planner_parent_state_exchange_base_ms: float = Field(default=40.0, ge=0.0) + planner_parent_state_exchange_ms: float = Field(default=0.5, ge=0.0) + planner_empty_rank_ms: float = Field(default=32.0, ge=0.0) class GdnRankExecutionPlan(BaseModel): @@ -234,8 +218,6 @@ class GdnRankExecutionPlan(BaseModel): real_token_mask: torch.Tensor family_count: int = Field(ge=0) completion_count: int = Field(ge=0) - prefix_buckets: tuple[GdnSegmentBucketPlan, ...] - completion_buckets: tuple[GdnSegmentBucketPlan, ...] local_prefix_buckets: tuple[GdnSegmentBucketPlan, ...] = () local_completion_buckets: tuple[GdnSegmentBucketPlan, ...] = () ready_local_completion_buckets: tuple[GdnSegmentBucketPlan, ...] = () @@ -253,7 +235,12 @@ class GdnRankExecutionPlan(BaseModel): parent_state_transfers: tuple[GdnParentStateTransferPlan, ...] = () prefix_boundary_buckets: tuple[GdnSegmentBucketPlan, ...] = () prefix_tail_buckets: tuple[GdnSegmentBucketPlan, ...] = () - completion_warmup_buckets: tuple[GdnSegmentBucketPlan, ...] = () + completion_with_prefix_tail_buckets: tuple[GdnSegmentBucketPlan, ...] = () + remote_prefix_tail_buckets: tuple[GdnSegmentBucketPlan, ...] = () + remote_completion_with_prefix_tail_buckets: tuple[GdnSegmentBucketPlan, ...] = () + remote_prefix_tail_exchange: Any | None = None + remote_prefix_tail_backward_exchange: Any | None = None + remote_prefix_tail_state_transfers: tuple[GdnParentStateTransferPlan, ...] = () @property def attention_token_indices(self) -> tuple[int, ...]: @@ -280,6 +267,14 @@ class GdnCpSegmentSchedule(BaseModel): parent_state_transfers: tuple[GdnParentStateTransferPlan, ...] = () +class _GdnCpSegmentSearchDecision(BaseModel): + model_config = ConfigDict(frozen=True) + + chain_segment_keys: frozenset[GdnSegmentDecisionKey] + co_locate_local_families: bool + score: float + + class _ExplicitBucketColumn(BaseModel): model_config = ConfigDict(frozen=True) @@ -293,6 +288,21 @@ def length(self) -> int: return len(self.positions) +def _explicit_bucket_column( + *, + row_index: int, + family_index: int, + positions: tuple[int, ...], + output_mask: tuple[bool, ...], +) -> _ExplicitBucketColumn: + return _ExplicitBucketColumn.model_construct( + row_index=row_index, + family_index=family_index, + positions=positions, + output_mask=output_mask, + ) + + class _AttentionLayoutIndex(BaseModel): """Counting index for CP attention token ownership.""" @@ -375,7 +385,7 @@ def build_gdn_rank_execution_plan( ( prefix_boundary_buckets, prefix_tail_buckets, - completion_warmup_buckets, + completion_with_prefix_tail_buckets, ) = _build_chunk_aligned_cp1_bucket_plans( spec, device=device, @@ -405,8 +415,6 @@ def build_gdn_rank_execution_plan( real_token_mask=positions.unsqueeze(0) < valid_lengths.unsqueeze(1), family_count=spec.family_count, completion_count=spec.completion_count, - prefix_buckets=(), - completion_buckets=(), local_prefix_buckets=(), local_completion_buckets=(), ready_local_completion_buckets=(), @@ -420,7 +428,7 @@ def build_gdn_rank_execution_plan( gdn_token_count=spec.real_token_count, prefix_boundary_buckets=prefix_boundary_buckets, prefix_tail_buckets=prefix_tail_buckets, - completion_warmup_buckets=completion_warmup_buckets, + completion_with_prefix_tail_buckets=completion_with_prefix_tail_buckets, ) @@ -442,8 +450,6 @@ def move_gdn_rank_execution_plan_to_device( real_token_mask=_move_planner_tensor(plan.real_token_mask, device), family_count=plan.family_count, completion_count=plan.completion_count, - prefix_buckets=_move_bucket_plans(plan.prefix_buckets, device), - completion_buckets=_move_bucket_plans(plan.completion_buckets, device), local_prefix_buckets=_move_bucket_plans(plan.local_prefix_buckets, device), local_completion_buckets=_move_bucket_plans( plan.local_completion_buckets, device @@ -473,8 +479,23 @@ def move_gdn_rank_execution_plan_to_device( plan.prefix_boundary_buckets, device ), prefix_tail_buckets=_move_bucket_plans(plan.prefix_tail_buckets, device), - completion_warmup_buckets=_move_bucket_plans( - plan.completion_warmup_buckets, device + completion_with_prefix_tail_buckets=_move_bucket_plans( + plan.completion_with_prefix_tail_buckets, device + ), + remote_prefix_tail_buckets=_move_bucket_plans( + plan.remote_prefix_tail_buckets, device + ), + remote_completion_with_prefix_tail_buckets=_move_bucket_plans( + plan.remote_completion_with_prefix_tail_buckets, device + ), + remote_prefix_tail_exchange=move_cp_exchange_plan_to_device( + plan.remote_prefix_tail_exchange, device + ), + remote_prefix_tail_backward_exchange=move_cp_exchange_plan_to_device( + plan.remote_prefix_tail_backward_exchange, device + ), + remote_prefix_tail_state_transfers=_move_parent_state_transfers( + plan.remote_prefix_tail_state_transfers, device ), ) @@ -487,8 +508,11 @@ def _move_bucket_plans( GdnSegmentBucketPlan.model_construct( length=bucket.length, lengths=_move_planner_tensor(bucket.lengths, device), + lengths_cpu=bucket.lengths_cpu, + lengths_by_rank_cpu=bucket.lengths_by_rank_cpu, real_mask=_move_planner_tensor(bucket.real_mask, device), cu_seqlens=_move_planner_tensor(bucket.cu_seqlens, device), + cu_seqlens_cpu=bucket.cu_seqlens_cpu, row_indices=_move_planner_tensor(bucket.row_indices, device), position_indices=_move_planner_tensor(bucket.position_indices, device), family_indices=_move_planner_tensor(bucket.family_indices, device), @@ -522,287 +546,6 @@ def _move_parent_state_transfers( ) -def build_gdn_chain_only_rank_execution_plan( - spec: GdnPackedExecutionSpec, - *, - device: torch.device | str, - cp_rank: int, - cp_size: int, - planner_config: GdnPlannerConfig | None = None, -) -> GdnRankExecutionPlan | None: - """Build the rank-local plan for rows that are entirely native CP chains. - - This avoids a large Python-object schedule broadcast for long pure-chain rows - such as `64k + 8x64k`. Mixed local/chain rows still use the general planner. - """ - - planner_config = planner_config or GdnPlannerConfig() - if cp_size <= 1: - return None - if cp_rank < 0 or cp_rank >= cp_size: - raise ValueError(f"cp_rank must be in [0, {cp_size}), got {cp_rank}") - if not spec.families: - return None - for family in spec.families: - if not _can_chain_prefix_segment( - family.prefix, cp_size=cp_size, planner_config=planner_config - ): - return None - if any( - not _can_chain_segment( - completion, cp_size=cp_size, planner_config=planner_config - ) - for completion in family.completions - ): - return None - - local_tokens: list[int] = [] - prefix_segments: list[GdnSegmentSpec] = [] - completion_segments: list[GdnSegmentSpec] = [] - for family in spec.families: - prefix_segments.append(family.prefix) - local_tokens.extend( - _chain_rank_token_indices( - family.prefix, - spec, - cp_rank=cp_rank, - cp_size=cp_size, - ) - ) - for completion in family.completions: - completion_segments.append(completion) - local_tokens.extend( - _chain_rank_token_indices( - completion, - spec, - cp_rank=cp_rank, - cp_size=cp_size, - ) - ) - local_token_tuple = tuple(local_tokens) - local_token_ranges = _local_token_ranges(local_token_tuple) - token_counts_by_rank = tuple( - len(local_token_tuple) if rank == cp_rank else 0 for rank in range(cp_size) - ) - identity_exchange = GdnCpExchangePlan.model_construct( - cp_size=cp_size, - source_token_counts_by_rank=token_counts_by_rank, - dest_token_counts_by_rank=token_counts_by_rank, - transfers=tuple( - GdnCpPeerTransfer.model_construct( - source_rank=rank, - dest_rank=rank, - token_count=count, - source_positions_tensor=None, - dest_positions_tensor=None, - ) - for rank, count in enumerate(token_counts_by_rank) - if count - ), - ) - chain_prefix_buckets = _batch_segments_by_padded_work( - tuple(prefix_segments), - max_padding_ratio=planner_config.max_padding_ratio, - max_segments_per_batch=planner_config.max_segments_per_batch, - ) - chain_completion_buckets = _batch_segments_by_padded_work( - tuple(completion_segments), - max_padding_ratio=planner_config.max_padding_ratio, - max_segments_per_batch=planner_config.max_segments_per_batch, - ) - prefix_family_order = tuple( - segment.family_index for bucket in chain_prefix_buckets for segment in bucket - ) - return GdnRankExecutionPlan.model_construct( - cp_rank=cp_rank, - cp_size=cp_size, - batch_size=1, - sequence_length=len(local_token_tuple), - packed_batch_size=spec.batch_size, - packed_sequence_length=spec.sequence_length, - real_token_mask=torch.ones( - 1, len(local_token_tuple), device=device, dtype=torch.bool - ), - family_count=spec.family_count, - completion_count=spec.completion_count, - prefix_buckets=(), - completion_buckets=(), - local_prefix_buckets=(), - local_completion_buckets=(), - ready_local_completion_buckets=(), - remote_local_completion_buckets=(), - chain_prefix_buckets=_build_position_bucket_plans( - chain_prefix_buckets, - local_token_ranges, - sequence_length=spec.sequence_length, - device=device, - ), - chain_completion_buckets=_build_position_bucket_plans( - chain_completion_buckets, - local_token_ranges, - sequence_length=spec.sequence_length, - device=device, - ), - prefix_table_is_dense_ordered=( - prefix_family_order == tuple(range(spec.family_count)) - ), - attention_to_gdn=identity_exchange, - gdn_to_attention=identity_exchange, - attention_token_ranges=local_token_ranges, - gdn_token_ranges=local_token_ranges, - attention_token_count=len(local_token_tuple), - gdn_token_count=len(local_token_tuple), - parent_state_exchange_family_indices=(), - parent_state_transfers=(), - ) - - -def _build_chain_attention_layout_rank_execution_plan( - spec: GdnPackedExecutionSpec, - *, - device: torch.device | str, - cp_rank: int, - cp_size: int, - attention_token_layout_index: TokenLayoutIndex | None, - planner_config: GdnPlannerConfig, -) -> GdnRankExecutionPlan | None: - if cp_size <= 1 or not spec.families: - return None - for family in spec.families: - if not _can_chain_prefix_segment( - family.prefix, cp_size=cp_size, planner_config=planner_config - ): - return None - if any( - not _can_chain_segment( - completion, cp_size=cp_size, planner_config=planner_config - ) - for completion in family.completions - ): - return None - - from art.megatron.gdn.layout import ( - _reverse_exchange_plan, - build_local_rank_cp_exchange_plan_from_dest_ranges, - ) - - source_layout = _attention_source_layout( - spec, - cp_size=cp_size, - attention_token_layout_index=attention_token_layout_index, - planner_config=planner_config, - ) - attention_layout_index = _build_attention_layout_index_from_token_layout( - source_layout, - max_ranges=max(1, 2 * spec.real_token_count // len(tuple(spec.segments()))), - ) - rank_loads = [0] * cp_size - gdn_ranges_by_rank: list[list[tuple[int, int, int]]] = [[] for _ in range(cp_size)] - prefix_segments: list[GdnSegmentSpec] = [] - completion_segments: list[GdnSegmentSpec] = [] - cross_rank_token_count = 0 - for family in spec.families: - for segment in (family.prefix, *family.completions): - if segment.kind == "prefix": - prefix_segments.append(segment) - else: - completion_segments.append(segment) - token_start = _segment_token_start(segment, spec.sequence_length) - shards = _attention_contiguous_chain_shards( - token_start, - segment.length, - cp_size=cp_size, - attention_layout_index=attention_layout_index, - ) - if shards is None: - shards = tuple( - _chain_rank_token_indices( - segment, - spec, - cp_rank=rank, - cp_size=cp_size, - ) - for rank in range(cp_size) - ) - for rank, shard in enumerate(shards): - position_start = rank_loads[rank] - gdn_ranges_by_rank[rank].append( - (shard.start, shard.stop, position_start) - ) - rank_loads[rank] += len(shard) - cross_rank_token_count += len(shard) - _attention_overlap_count( - attention_layout_index, - rank, - shard.start, - shard.stop, - ) - local_token_ranges = tuple(gdn_ranges_by_rank[cp_rank]) - local_token_count = rank_loads[cp_rank] - attention_to_gdn = build_local_rank_cp_exchange_plan_from_dest_ranges( - source_layout=source_layout, - device=device, - dest_ranges_by_rank=tuple(tuple(ranges) for ranges in gdn_ranges_by_rank), - local_rank=cp_rank, - cross_rank_token_count=cross_rank_token_count, - ) - gdn_to_attention = _reverse_exchange_plan(attention_to_gdn) - chain_prefix_buckets = _batch_segments_by_padded_work( - tuple(prefix_segments), - max_padding_ratio=planner_config.max_padding_ratio, - max_segments_per_batch=planner_config.max_segments_per_batch, - ) - chain_completion_buckets = _batch_segments_by_padded_work( - tuple(completion_segments), - max_padding_ratio=planner_config.max_padding_ratio, - max_segments_per_batch=planner_config.max_segments_per_batch, - ) - prefix_family_order = tuple( - segment.family_index for bucket in chain_prefix_buckets for segment in bucket - ) - return GdnRankExecutionPlan.model_construct( - cp_rank=cp_rank, - cp_size=cp_size, - batch_size=1, - sequence_length=local_token_count, - packed_batch_size=spec.batch_size, - packed_sequence_length=spec.sequence_length, - real_token_mask=torch.ones( - 1, local_token_count, device=device, dtype=torch.bool - ), - family_count=spec.family_count, - completion_count=spec.completion_count, - prefix_buckets=(), - completion_buckets=(), - local_prefix_buckets=(), - local_completion_buckets=(), - ready_local_completion_buckets=(), - remote_local_completion_buckets=(), - chain_prefix_buckets=_build_position_bucket_plans( - chain_prefix_buckets, - local_token_ranges, - sequence_length=spec.sequence_length, - device=device, - ), - chain_completion_buckets=_build_position_bucket_plans( - chain_completion_buckets, - local_token_ranges, - sequence_length=spec.sequence_length, - device=device, - ), - prefix_table_is_dense_ordered=( - prefix_family_order == tuple(range(spec.family_count)) - ), - attention_to_gdn=attention_to_gdn, - gdn_to_attention=gdn_to_attention, - attention_token_ranges=source_layout.ownership_ranges_by_rank[cp_rank], - gdn_token_ranges=local_token_ranges, - attention_token_count=source_layout.token_counts_by_rank[cp_rank], - gdn_token_count=local_token_count, - parent_state_exchange_family_indices=(), - parent_state_transfers=(), - ) - - def _build_local_attention_layout_rank_execution_plan( spec: GdnPackedExecutionSpec, *, @@ -815,7 +558,7 @@ def _build_local_attention_layout_rank_execution_plan( if cp_size <= 1 or not spec.families: return None if any( - _can_chain_family(family, cp_size=cp_size, planner_config=planner_config) + _has_chainable_segment(family, cp_size=cp_size, planner_config=planner_config) for family in spec.families ): return None @@ -847,20 +590,15 @@ def _build_local_attention_layout_rank_execution_plan( co_locate_local_families=False, planner_config=planner_config, ) - if _can_zero_exchange_colocate_families( + co_located = _assign_local_attention_segments( spec, cp_size=cp_size, segment_attention_counts=segment_attention_counts, - ): - co_located = _assign_local_attention_segments( - spec, - cp_size=cp_size, - segment_attention_counts=segment_attention_counts, - co_locate_local_families=True, - planner_config=planner_config, - ) - if co_located[3] == 0 and co_located[4] < best[4]: - best = co_located + co_locate_local_families=True, + planner_config=planner_config, + ) + if co_located[4] < best[4]: + best = co_located ( prefix_owner_by_family, completion_owners_by_family, @@ -871,6 +609,10 @@ def _build_local_attention_layout_rank_execution_plan( local_prefix_segments: list[GdnSegmentSpec] = [] local_completion_segments: list[GdnSegmentSpec] = [] + prefix_segments_by_rank: list[list[GdnSegmentSpec]] = [[] for _ in range(cp_size)] + completion_segments_by_rank: list[list[GdnSegmentSpec]] = [ + [] for _ in range(cp_size) + ] gdn_ranges_by_rank: list[list[tuple[int, int, int]]] = [[] for _ in range(cp_size)] rank_loads = [0] * cp_size parent_state_exchange_families: set[int] = set() @@ -888,6 +630,7 @@ def append_segment(rank: int, segment: GdnSegmentSpec) -> None: prefix_owner = prefix_owner_by_family[family.family_index] if prefix_owner == cp_rank: local_prefix_segments.append(family.prefix) + prefix_segments_by_rank[prefix_owner].append(family.prefix) append_segment(prefix_owner, family.prefix) completion_owners = completion_owners_by_family[family.family_index] for completion, completion_owner in zip( @@ -895,6 +638,7 @@ def append_segment(rank: int, segment: GdnSegmentSpec) -> None: ): if completion_owner == cp_rank: local_completion_segments.append(completion) + completion_segments_by_rank[completion_owner].append(completion) append_segment(completion_owner, completion) if completion_owner != prefix_owner: parent_state_exchange_families.add(family.family_index) @@ -904,20 +648,63 @@ def append_segment(rank: int, segment: GdnSegmentSpec) -> None: local_token_ranges = tuple(gdn_ranges_by_rank[cp_rank]) local_token_count = rank_loads[cp_rank] - attention_to_gdn = build_local_rank_cp_exchange_plan_from_dest_ranges( - source_layout=source_layout, - device=device, - dest_ranges_by_rank=tuple(tuple(ranges) for ranges in gdn_ranges_by_rank), - local_rank=cp_rank, + schedule = GdnCpSegmentSchedule.model_construct( + gdn_token_counts_by_rank=tuple(rank_loads), + gdn_token_ranges_by_rank=tuple(tuple(ranges) for ranges in gdn_ranges_by_rank), cross_rank_token_count=cross_rank_token_count, - ) - gdn_to_attention = _reverse_exchange_plan(attention_to_gdn) - local_prefix_family_indices = { - segment.family_index for segment in local_prefix_segments - } - local_prefix_buckets = _batch_segments_by_padded_work( - (), - max_padding_ratio=planner_config.max_padding_ratio, + chain_prefix_buckets=(), + chain_completion_buckets=(), + local_prefix_segments_by_rank=tuple( + tuple(segments) for segments in prefix_segments_by_rank + ), + local_completion_segments_by_rank=tuple( + tuple(segments) for segments in completion_segments_by_rank + ), + parent_state_exchange_family_indices=tuple( + sorted(parent_state_exchange_families) + ), + parent_state_transfers=_build_parent_state_transfer_plans( + parent_state_transfer_families + ), + ) + if parent_state_transfer_families: + ( + remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers, + remote_prefix_tail_families, + ) = _build_remote_prefix_tail_plans( + spec, + schedule, + cp_rank=cp_rank, + device=device, + planner_config=planner_config, + ) + else: + ( + remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers, + remote_prefix_tail_families, + ) = _empty_remote_prefix_tail_plans() + attention_to_gdn = build_local_rank_cp_exchange_plan_from_dest_ranges( + source_layout=source_layout, + device=device, + dest_ranges_by_rank=tuple(tuple(ranges) for ranges in gdn_ranges_by_rank), + local_rank=cp_rank, + cross_rank_token_count=cross_rank_token_count, + ) + gdn_to_attention = _reverse_exchange_plan(attention_to_gdn) + local_prefix_family_indices = { + segment.family_index for segment in local_prefix_segments + } + local_prefix_buckets = _batch_segments_by_padded_work( + (), + max_padding_ratio=planner_config.max_padding_ratio, max_segments_per_batch=planner_config.max_segments_per_batch, ) chunk_local_completion_segments = tuple( @@ -929,6 +716,7 @@ def append_segment(rank: int, segment: GdnSegmentSpec) -> None: segment for segment in local_completion_segments if segment.family_index not in local_prefix_family_indices + and segment.family_index not in remote_prefix_tail_families ) ready_completion_segments, remote_completion_segments = ( _split_ready_and_remote_completion_segments( @@ -965,7 +753,7 @@ def append_segment(rank: int, segment: GdnSegmentSpec) -> None: ( prefix_boundary_buckets, prefix_tail_buckets, - completion_warmup_buckets, + completion_with_prefix_tail_buckets, ) = _build_chunk_aligned_position_bucket_plans( tuple(local_prefix_segments), chunk_local_completion_segments, @@ -986,8 +774,6 @@ def append_segment(rank: int, segment: GdnSegmentSpec) -> None: ), family_count=spec.family_count, completion_count=spec.completion_count, - prefix_buckets=(), - completion_buckets=(), local_prefix_buckets=_build_position_bucket_plans( local_prefix_buckets, local_token_ranges, @@ -1012,15 +798,21 @@ def append_segment(rank: int, segment: GdnSegmentSpec) -> None: attention_token_count=source_layout.token_counts_by_rank[cp_rank], gdn_token_count=local_token_count, parent_state_exchange_family_indices=tuple( - sorted(parent_state_exchange_families) + sorted(parent_state_exchange_families - remote_prefix_tail_families) ), - parent_state_transfers=_transfer_plans_to_device( + parent_state_transfers=_filter_parent_state_transfers( _build_parent_state_transfer_plans(parent_state_transfer_families), + excluded_families=remote_prefix_tail_families, device=device, ), prefix_boundary_buckets=prefix_boundary_buckets, prefix_tail_buckets=prefix_tail_buckets, - completion_warmup_buckets=completion_warmup_buckets, + completion_with_prefix_tail_buckets=completion_with_prefix_tail_buckets, + remote_prefix_tail_buckets=remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets=remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange=remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange=remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers=remote_prefix_tail_state_transfers, ) @@ -1095,18 +887,16 @@ def append_owner(rank: int, segment: GdnSegmentSpec) -> None: parent_state_exchange_families.add(family.family_index) completion_owners_by_family.append(tuple(completion_owners)) - max_load = max(rank_loads, default=0) - idle_tokens = sum(max_load - load for load in rank_loads) - empty_rank_count = sum(1 for load in rank_loads if load == 0) - local_launches = sum(has_prefix) + sum(has_completion) - score = ( - max_load - + planner_config.rank_idle_token_cost * idle_tokens - + planner_config.empty_rank_penalty_tokens * empty_rank_count - + planner_config.local_fork_launch_penalty_tokens * local_launches - + planner_config.layout_cross_rank_token_cost * cross_rank_token_count - + planner_config.parent_state_exchange_penalty_tokens - * len(parent_state_exchange_families) + del has_prefix, has_completion + score = _score_local_segment_assignment( + spec, + cp_size=cp_size, + prefix_owner_by_family=tuple(prefix_owner_by_family), + completion_owners_by_family=tuple(completion_owners_by_family), + rank_loads=tuple(rank_loads), + cross_rank_token_count=cross_rank_token_count, + parent_state_exchange_family_count=len(parent_state_exchange_families), + planner_config=planner_config, ) return ( tuple(prefix_owner_by_family), @@ -1117,6 +907,53 @@ def append_owner(rank: int, segment: GdnSegmentSpec) -> None: ) +def _score_local_segment_assignment( + spec: GdnPackedExecutionSpec, + *, + cp_size: int, + prefix_owner_by_family: tuple[int, ...], + completion_owners_by_family: tuple[tuple[int, ...], ...], + rank_loads: tuple[int, ...], + cross_rank_token_count: int, + parent_state_exchange_family_count: int, + planner_config: GdnPlannerConfig, +) -> float: + local_prefix_segments_by_rank: list[list[GdnSegmentSpec]] = [ + [] for _ in range(cp_size) + ] + local_completion_segments_by_rank: list[list[GdnSegmentSpec]] = [ + [] for _ in range(cp_size) + ] + for family in spec.families: + prefix_owner = prefix_owner_by_family[family.family_index] + local_prefix_segments_by_rank[prefix_owner].append(family.prefix) + completion_owners = completion_owners_by_family[family.family_index] + for completion, completion_owner in zip( + family.completions, completion_owners, strict=True + ): + local_completion_segments_by_rank[completion_owner].append(completion) + ( + local_work_by_rank, + local_bucket_count, + local_segment_count, + ) = _estimate_local_rank_kernel_work( + tuple(tuple(segments) for segments in local_prefix_segments_by_rank), + tuple(tuple(segments) for segments in local_completion_segments_by_rank), + planner_config=planner_config, + ) + return _score_cp_segment_stats( + rank_local_work=local_work_by_rank, + rank_chain_work=tuple(0 for _ in range(cp_size)), + rank_real_tokens=rank_loads, + cross_rank_token_count=cross_rank_token_count, + parent_state_exchange_family_count=parent_state_exchange_family_count, + local_bucket_count=local_bucket_count, + local_segment_count=local_segment_count, + chain_bucket_count=0, + planner_config=planner_config, + ) + + def _can_zero_exchange_colocate_families( spec: GdnPackedExecutionSpec, *, @@ -1217,18 +1054,22 @@ def _build_chunk_aligned_cp1_bucket_plans( boundary_segments.append( _segment_with_bounds(prefix, prefix.start, boundary_end) ) - if boundary_end < prefix.end and not family.completions: + prefix_tail_positions = tuple(range(boundary_end, prefix.end)) + if prefix_tail_positions and not family.completions: tail_segments.append(_segment_with_bounds(prefix, boundary_end, prefix.end)) - warmup_positions = tuple(range(boundary_end, prefix.end)) - for completion in family.completions: - warmup_mask = (completion.child_index == 0,) * len(warmup_positions) - completion_positions = tuple(range(completion.start, completion.end)) + for child_offset, completion in enumerate(family.completions): + completion_positions = prefix_tail_positions + tuple( + range(completion.start, completion.end) + ) completion_columns.append( - _ExplicitBucketColumn( + _explicit_bucket_column( row_index=completion.row_index, family_index=completion.family_index, - positions=warmup_positions + completion_positions, - output_mask=warmup_mask + (True,) * len(completion_positions), + positions=completion_positions, + output_mask=( + ((child_offset == 0),) * len(prefix_tail_positions) + + (True,) * completion.length + ), ) ) boundary_buckets = _batch_segments_by_padded_work( @@ -1241,7 +1082,7 @@ def _build_chunk_aligned_cp1_bucket_plans( max_padding_ratio=planner_config.max_padding_ratio, max_segments_per_batch=planner_config.max_segments_per_batch, ) - completion_buckets = _batch_explicit_bucket_columns( + completion_column_batches = _batch_explicit_bucket_columns( tuple(completion_columns), max_padding_ratio=planner_config.max_padding_ratio, max_segments_per_batch=planner_config.max_segments_per_batch, @@ -1249,7 +1090,7 @@ def _build_chunk_aligned_cp1_bucket_plans( return ( _build_segment_bucket_plans(boundary_buckets, device=device), _build_segment_bucket_plans(tail_buckets, device=device), - _build_explicit_bucket_plans(completion_buckets, device=device), + _build_explicit_bucket_plans(completion_column_batches, device=device), ) @@ -1267,6 +1108,10 @@ def _build_chunk_aligned_position_bucket_plans( tuple[GdnSegmentBucketPlan, ...], ]: local_range_ends = tuple(token_end for _, token_end, _ in local_token_ranges) + local_range_positions = { + (token_start, token_end): position_start + for token_start, token_end, position_start in local_token_ranges + } completions_by_family: dict[int, list[GdnSegmentSpec]] = {} for completion in completion_segments: completions_by_family.setdefault(completion.family_index, []).append(completion) @@ -1279,23 +1124,19 @@ def _build_chunk_aligned_position_bucket_plans( boundary_segments.append( _segment_with_bounds(prefix, prefix.start, boundary_end) ) - family_completions = tuple( - sorted( - completions_by_family.get(prefix.family_index, ()), - key=lambda segment: segment.child_index or 0, - ) - ) - if boundary_end < prefix.end and not family_completions: - tail_segments.append(_segment_with_bounds(prefix, boundary_end, prefix.end)) - warmup_positions = _local_positions_for_span( + family_completions = tuple(completions_by_family.get(prefix.family_index, ())) + prefix_tail_positions = _local_positions_for_span( prefix.row_index, boundary_end, prefix.end, sequence_length=sequence_length, local_token_ranges=local_token_ranges, local_range_ends=local_range_ends, + local_range_positions=local_range_positions, ) - for completion in family_completions: + if prefix_tail_positions and not family_completions: + tail_segments.append(_segment_with_bounds(prefix, boundary_end, prefix.end)) + for child_offset, completion in enumerate(family_completions): completion_positions = _local_positions_for_span( completion.row_index, completion.start, @@ -1303,14 +1144,18 @@ def _build_chunk_aligned_position_bucket_plans( sequence_length=sequence_length, local_token_ranges=local_token_ranges, local_range_ends=local_range_ends, + local_range_positions=local_range_positions, ) + positions = prefix_tail_positions + completion_positions completion_columns.append( - _ExplicitBucketColumn( + _explicit_bucket_column( row_index=0, family_index=completion.family_index, - positions=warmup_positions + completion_positions, - output_mask=(completion.child_index == 0,) * len(warmup_positions) - + (True,) * len(completion_positions), + positions=positions, + output_mask=( + ((child_offset == 0),) * len(prefix_tail_positions) + + (True,) * len(completion_positions) + ), ) ) boundary_buckets = _batch_segments_by_padded_work( @@ -1323,7 +1168,7 @@ def _build_chunk_aligned_position_bucket_plans( max_padding_ratio=planner_config.max_padding_ratio, max_segments_per_batch=planner_config.max_segments_per_batch, ) - completion_buckets = _batch_explicit_bucket_columns( + completion_column_batches = _batch_explicit_bucket_columns( tuple(completion_columns), max_padding_ratio=planner_config.max_padding_ratio, max_segments_per_batch=planner_config.max_segments_per_batch, @@ -1341,7 +1186,220 @@ def _build_chunk_aligned_position_bucket_plans( sequence_length=sequence_length, device=device, ), - _build_explicit_bucket_plans(completion_buckets, device=device), + _build_explicit_bucket_plans(completion_column_batches, device=device), + ) + + +def _build_remote_prefix_tail_plans( + spec: GdnPackedExecutionSpec, + schedule: GdnCpSegmentSchedule, + *, + cp_rank: int, + device: torch.device | str, + planner_config: GdnPlannerConfig, +) -> tuple[ + tuple[GdnSegmentBucketPlan, ...], + tuple[GdnSegmentBucketPlan, ...], + Any | None, + Any | None, + tuple[GdnParentStateTransferPlan, ...], + frozenset[int], +]: + from art.megatron.gdn.layout import ( + GdnCpExchangePlan, + GdnCpPeerTransfer, + _reverse_exchange_plan, + ) + + family_by_index = {family.family_index: family for family in spec.families} + prefix_owner_by_family = _prefix_owner_by_family(schedule) + source_positions_by_pair: dict[tuple[int, int], list[int]] = {} + dest_positions_by_pair: dict[tuple[int, int], list[int]] = {} + dest_counts = [0 for _ in schedule.gdn_token_counts_by_rank] + state_transfer_families: dict[tuple[int, int], set[int]] = {} + remote_tail_family_indices: set[int] = set() + local_tail_columns: list[_ExplicitBucketColumn] = [] + local_completion_columns: list[_ExplicitBucketColumn] = [] + tail_positions_by_dest_family: dict[tuple[int, int], tuple[int, ...]] = {} + local_tail_column_families: set[int] = set() + rank_ranges = schedule.gdn_token_ranges_by_rank + rank_range_ends = tuple( + tuple(end for _, end, _ in ranges) for ranges in rank_ranges + ) + rank_range_positions = tuple( + { + (token_start, token_end): position_start + for token_start, token_end, position_start in ranges + } + for ranges in rank_ranges + ) + + for dest_rank, completions in enumerate(schedule.local_completion_segments_by_rank): + for completion in completions: + source_rank = prefix_owner_by_family.get(completion.family_index) + if source_rank is None or source_rank == dest_rank: + continue + family = family_by_index[completion.family_index] + boundary_end = _prefix_chunk_boundary_end(family.prefix) + if boundary_end == family.prefix.end: + continue + dest_family = (dest_rank, family.family_index) + dest_positions = tail_positions_by_dest_family.get(dest_family) + if dest_positions is None: + source_positions = _local_positions_for_span( + family.prefix.row_index, + boundary_end, + family.prefix.end, + sequence_length=spec.sequence_length, + local_token_ranges=rank_ranges[source_rank], + local_range_ends=rank_range_ends[source_rank], + local_range_positions=rank_range_positions[source_rank], + ) + if len(source_positions) != family.prefix.end - boundary_end: + raise ValueError( + "remote prefix-tail exchange could not locate all source tokens " + f"for family {family.family_index}" + ) + dest_start = dest_counts[dest_rank] + dest_positions = tuple( + range(dest_start, dest_start + len(source_positions)) + ) + tail_positions_by_dest_family[dest_family] = dest_positions + dest_counts[dest_rank] += len(source_positions) + pair = (source_rank, dest_rank) + source_positions_by_pair.setdefault(pair, []).extend(source_positions) + dest_positions_by_pair.setdefault(pair, []).extend(dest_positions) + state_transfer_families.setdefault(pair, set()).add(family.family_index) + remote_tail_family_indices.add(family.family_index) + + if dest_rank != cp_rank: + continue + completion_positions = _local_positions_for_span( + completion.row_index, + completion.start, + completion.end, + sequence_length=spec.sequence_length, + local_token_ranges=rank_ranges[dest_rank], + local_range_ends=rank_range_ends[dest_rank], + local_range_positions=rank_range_positions[dest_rank], + ) + if len(completion_positions) != completion.length: + raise ValueError( + "remote prefix-tail bucket could not locate all completion tokens " + f"for family {family.family_index}" + ) + remote_base = int(schedule.gdn_token_counts_by_rank[dest_rank]) + if ( + len(dest_positions) > 0 + and family.family_index not in local_tail_column_families + ): + local_tail_column_families.add(family.family_index) + local_tail_columns.append( + _explicit_bucket_column( + row_index=0, + family_index=family.family_index, + positions=tuple(remote_base + pos for pos in dest_positions), + output_mask=(False,) * len(dest_positions), + ) + ) + local_completion_columns.append( + _explicit_bucket_column( + row_index=0, + family_index=family.family_index, + positions=completion_positions, + output_mask=(True,) * len(completion_positions), + ) + ) + + if not source_positions_by_pair: + return (), (), None, None, (), frozenset() + + transfers = tuple( + GdnCpPeerTransfer.model_construct( + source_rank=source_rank, + dest_rank=dest_rank, + token_count=len(source_positions), + source_positions_tensor=_move_planner_tensor( + torch.tensor(source_positions, dtype=torch.long), device + ), + dest_positions_tensor=_move_planner_tensor( + torch.tensor( + dest_positions_by_pair[(source_rank, dest_rank)], + dtype=torch.long, + ), + device, + ), + ) + for (source_rank, dest_rank), source_positions in sorted( + source_positions_by_pair.items() + ) + ) + exchange = GdnCpExchangePlan.model_construct( + cp_size=len(schedule.gdn_token_counts_by_rank), + source_token_counts_by_rank=schedule.gdn_token_counts_by_rank, + dest_token_counts_by_rank=tuple(dest_counts), + transfers=transfers, + cross_rank_token_count_override=sum(dest_counts), + ) + tail_column_batches = _batch_explicit_bucket_columns( + tuple(local_tail_columns), + max_padding_ratio=planner_config.max_padding_ratio, + max_segments_per_batch=planner_config.max_segments_per_batch, + ) + completion_column_batches = _batch_explicit_bucket_columns( + tuple(local_completion_columns), + max_padding_ratio=planner_config.max_padding_ratio, + max_segments_per_batch=planner_config.max_segments_per_batch, + ) + return ( + _build_explicit_bucket_plans(tail_column_batches, device=device), + _build_explicit_bucket_plans(completion_column_batches, device=device), + exchange, + _reverse_exchange_plan(exchange), + _transfer_plans_to_device( + _build_parent_state_transfer_plans(state_transfer_families), + device=device, + ), + frozenset(remote_tail_family_indices), + ) + + +def _empty_remote_prefix_tail_plans() -> tuple[ + tuple[GdnSegmentBucketPlan, ...], + tuple[GdnSegmentBucketPlan, ...], + Any | None, + Any | None, + tuple[GdnParentStateTransferPlan, ...], + frozenset[int], +]: + return (), (), None, None, (), frozenset() + + +def _prefix_owner_by_family(schedule: GdnCpSegmentSchedule) -> dict[int, int]: + owners: dict[int, int] = {} + for rank, segments in enumerate(schedule.local_prefix_segments_by_rank): + for segment in segments: + owners[segment.family_index] = rank + return owners + + +def _filter_parent_state_transfers( + transfers: tuple[GdnParentStateTransferPlan, ...], + *, + excluded_families: frozenset[int], + device: torch.device | str, +) -> tuple[GdnParentStateTransferPlan, ...]: + if not excluded_families: + return _transfer_plans_to_device(transfers, device=device) + kept: dict[tuple[int, int], set[int]] = {} + for transfer in transfers: + families = set(transfer.family_indices) - excluded_families + if families: + kept.setdefault((transfer.source_rank, transfer.dest_rank), set()).update( + families + ) + return _transfer_plans_to_device( + _build_parent_state_transfer_plans(kept), device=device ) @@ -1353,9 +1411,22 @@ def _local_positions_for_span( sequence_length: int, local_token_ranges: tuple[tuple[int, int, int], ...], local_range_ends: tuple[int, ...], + local_range_positions: dict[tuple[int, int], int] | None = None, ) -> tuple[int, ...]: if start == end: return () + token_start = row_index * sequence_length + start + token_end = row_index * sequence_length + end + if local_range_positions is not None: + position_start = local_range_positions.get((token_start, token_end)) + if position_start is not None: + return tuple(range(position_start, position_start + end - start)) + range_index = bisect_left(local_range_ends, token_start + 1) + if range_index < len(local_token_ranges): + range_start, range_end, position_start = local_token_ranges[range_index] + if range_start <= token_start and token_end <= range_end: + local_start = position_start + token_start - range_start + return tuple(range(local_start, local_start + end - start)) segment = _trusted_pydantic_construct( GdnSegmentSpec, _GDN_SEGMENT_SPEC_FIELDS, @@ -1456,32 +1527,44 @@ def _build_explicit_bucket_plan( device: torch.device | str, ) -> GdnSegmentBucketPlan: max_length = max(column.length for column in columns) - lengths_cpu = torch.tensor([column.length for column in columns], dtype=torch.long) + column_count = len(columns) + lengths = [column.length for column in columns] + lengths_cpu = torch.tensor(lengths, dtype=torch.long) offsets_cpu = torch.arange(max_length, dtype=torch.long).unsqueeze(1) real_mask_cpu = offsets_cpu < lengths_cpu.unsqueeze(0) - row_indices_cpu = torch.zeros(max_length, len(columns), dtype=torch.long) - position_indices_cpu = torch.zeros(max_length, len(columns), dtype=torch.long) - output_mask_cpu = torch.zeros(max_length, len(columns), dtype=torch.bool) + padded_element_count = max_length * column_count + row_indices = [0] * padded_element_count + position_indices = [0] * padded_element_count + output_mask = [False] * padded_element_count for column_index, column in enumerate(columns): length = column.length - row_indices_cpu[:length, column_index] = column.row_index - position_indices_cpu[:length, column_index] = torch.tensor( - column.positions, dtype=torch.long - ) - output_mask_cpu[:length, column_index] = torch.tensor( - column.output_mask, dtype=torch.bool - ) + column_slice = slice(column_index, length * column_count, column_count) + row_indices[column_slice] = [column.row_index] * length + position_indices[column_slice] = column.positions + output_mask[column_slice] = column.output_mask + row_indices_cpu = torch.tensor(row_indices, dtype=torch.long).reshape( + max_length, column_count + ) + position_indices_cpu = torch.tensor(position_indices, dtype=torch.long).reshape( + max_length, column_count + ) + output_mask_cpu = torch.tensor(output_mask, dtype=torch.bool).reshape( + max_length, column_count + ) family_indices_cpu = torch.tensor( [column.family_index for column in columns], dtype=torch.long ) + cu_seqlens_cpu = torch.cat( + [lengths_cpu.new_zeros(1), torch.cumsum(lengths_cpu, dim=0)] + ) return GdnSegmentBucketPlan.model_construct( length=max_length, lengths=_move_planner_tensor(lengths_cpu, device), + lengths_cpu=lengths_cpu, + lengths_by_rank_cpu=None, real_mask=_move_planner_tensor(real_mask_cpu, device), - cu_seqlens=_move_planner_tensor( - torch.cat([lengths_cpu.new_zeros(1), torch.cumsum(lengths_cpu, dim=0)]), - device, - ), + cu_seqlens=_move_planner_tensor(cu_seqlens_cpu, device), + cu_seqlens_cpu=cu_seqlens_cpu, row_indices=_move_planner_tensor(row_indices_cpu, device), position_indices=_move_planner_tensor(position_indices_cpu, device), family_indices=_move_planner_tensor(family_indices_cpu, device), @@ -1542,17 +1625,13 @@ def _build_cp_rank_execution_plan( f"{_layout_cp_size(attention_token_layout_index)} and {cp_size}" ) + from art.megatron.gdn.layout import ( + _reverse_exchange_plan, + build_local_rank_cp_exchange_plan_from_dest_ranges, + ) + has_explicit_attention_layout = attention_token_layout_index is not None if cp_segment_schedule is None and not has_explicit_attention_layout: - chain_only_plan = build_gdn_chain_only_rank_execution_plan( - spec, - device=device, - cp_rank=cp_rank, - cp_size=cp_size, - planner_config=planner_config, - ) - if chain_only_plan is not None: - return chain_only_plan local_family_plan = _build_local_family_rank_execution_plan( spec, device=device, @@ -1563,16 +1642,6 @@ def _build_cp_rank_execution_plan( if local_family_plan is not None: return local_family_plan if cp_segment_schedule is None and has_explicit_attention_layout: - chain_layout_plan = _build_chain_attention_layout_rank_execution_plan( - spec, - device=device, - cp_rank=cp_rank, - cp_size=cp_size, - attention_token_layout_index=attention_token_layout_index, - planner_config=planner_config, - ) - if chain_layout_plan is not None: - return chain_layout_plan local_layout_plan = _build_local_attention_layout_rank_execution_plan( spec, device=device, @@ -1584,11 +1653,6 @@ def _build_cp_rank_execution_plan( if local_layout_plan is not None: return local_layout_plan - from art.megatron.gdn.layout import ( - _reverse_exchange_plan, - build_local_rank_cp_exchange_plan_from_dest_ranges, - ) - source_layout = _attention_source_layout( spec, cp_size=cp_size, @@ -1622,6 +1686,30 @@ def _build_cp_rank_execution_plan( gdn_to_attention = _reverse_exchange_plan(attention_to_gdn) local_token_ranges = schedule.gdn_token_ranges_by_rank[cp_rank] local_gdn_token_count = schedule.gdn_token_counts_by_rank[cp_rank] + if schedule.parent_state_exchange_family_indices: + ( + remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers, + remote_prefix_tail_families, + ) = _build_remote_prefix_tail_plans( + spec, + schedule, + cp_rank=cp_rank, + device=device, + planner_config=planner_config, + ) + else: + ( + remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers, + remote_prefix_tail_families, + ) = _empty_remote_prefix_tail_plans() chain_prefix_buckets = tuple( bucket for bucket in schedule.chain_prefix_buckets if bucket @@ -1650,6 +1738,7 @@ def _build_cp_rank_execution_plan( segment for segment in local_completion_segments if segment.family_index not in local_prefix_family_indices + and segment.family_index not in remote_prefix_tail_families ) ready_completion_segments, remote_completion_segments = ( _split_ready_and_remote_completion_segments( @@ -1682,7 +1771,7 @@ def _build_cp_rank_execution_plan( ( prefix_boundary_buckets, prefix_tail_buckets, - completion_warmup_buckets, + completion_with_prefix_tail_buckets, ) = _build_chunk_aligned_position_bucket_plans( local_prefix_segments, chunk_local_completion_segments, @@ -1703,8 +1792,6 @@ def _build_cp_rank_execution_plan( ), family_count=spec.family_count, completion_count=spec.completion_count, - prefix_buckets=(), - completion_buckets=(), local_prefix_buckets=_build_position_bucket_plans( local_prefix_buckets, local_token_ranges, @@ -1734,12 +1821,14 @@ def _build_cp_rank_execution_plan( local_token_ranges, sequence_length=spec.sequence_length, device=device, + token_ranges_by_rank=schedule.gdn_token_ranges_by_rank, ), chain_completion_buckets=_build_position_bucket_plans( chain_completion_buckets, local_token_ranges, sequence_length=spec.sequence_length, device=device, + token_ranges_by_rank=schedule.gdn_token_ranges_by_rank, ), prefix_table_is_dense_ordered=( not local_prefix_segments @@ -1752,14 +1841,25 @@ def _build_cp_rank_execution_plan( attention_token_count=source_layout.token_counts_by_rank[cp_rank], gdn_token_count=local_gdn_token_count, parent_state_exchange_family_indices=( - schedule.parent_state_exchange_family_indices + tuple( + family_index + for family_index in schedule.parent_state_exchange_family_indices + if family_index not in remote_prefix_tail_families + ) ), - parent_state_transfers=_transfer_plans_to_device( - schedule.parent_state_transfers, device=device + parent_state_transfers=_filter_parent_state_transfers( + schedule.parent_state_transfers, + excluded_families=remote_prefix_tail_families, + device=device, ), prefix_boundary_buckets=prefix_boundary_buckets, prefix_tail_buckets=prefix_tail_buckets, - completion_warmup_buckets=completion_warmup_buckets, + completion_with_prefix_tail_buckets=completion_with_prefix_tail_buckets, + remote_prefix_tail_buckets=remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets=remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange=remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange=remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers=remote_prefix_tail_state_transfers, ) @@ -1802,145 +1902,662 @@ def _build_cp_segment_schedule( cp_size=cp_size, attention_layout_index=attention_layout_index, ) - legal_chain_families = tuple( - family.family_index + legal_chain_segments = tuple( + segment for family in spec.families - if _can_chain_family(family, cp_size=cp_size, planner_config=planner_config) + for segment in (family.prefix, *family.completions) + if ( + _can_chain_prefix_segment( + segment, cp_size=cp_size, planner_config=planner_config + ) + if segment.kind == "prefix" + else _can_chain_segment( + segment, cp_size=cp_size, planner_config=planner_config + ) + ) ) - chain_family_indices = frozenset(legal_chain_families) - best = _materialize_cp_segment_schedule( + decision = _beam_search_cp_segment_schedule_decision( spec, cp_size=cp_size, attention_layout_index=attention_layout_index, segment_attention_counts=segment_attention_counts, - chain_family_indices=chain_family_indices, - co_locate_local_families=False, + legal_chain_segments=legal_chain_segments, planner_config=planner_config, ) - best_score = _score_cp_segment_schedule( - best, + return _materialize_cp_segment_schedule( + spec, + cp_size=cp_size, + attention_layout_index=attention_layout_index, + segment_attention_counts=segment_attention_counts, + chain_segment_keys=decision.chain_segment_keys, + co_locate_local_families=decision.co_locate_local_families, planner_config=planner_config, ) - has_local_families = len(chain_family_indices) != spec.family_count - if has_local_families: - local_family_trial = _materialize_cp_segment_schedule( + + +def _beam_search_cp_segment_schedule_decision( + spec: GdnPackedExecutionSpec, + *, + cp_size: int, + attention_layout_index: _AttentionLayoutIndex, + segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], + legal_chain_segments: tuple[GdnSegmentSpec, ...], + planner_config: GdnPlannerConfig, +) -> _GdnCpSegmentSearchDecision: + legal_chain_keys = frozenset( + _segment_key(segment) for segment in legal_chain_segments + ) + chain_rank_counts_by_key: dict[GdnSegmentDecisionKey, tuple[int, ...]] = {} + chain_cross_rank_tokens_by_key: dict[GdnSegmentDecisionKey, int] = {} + for segment in legal_chain_segments: + key = _segment_key(segment) + ( + chain_rank_counts_by_key[key], + chain_cross_rank_tokens_by_key[key], + ) = _chain_segment_rank_counts_and_cross_rank_tokens( + segment, spec, cp_size=cp_size, attention_layout_index=attention_layout_index, - segment_attention_counts=segment_attention_counts, - chain_family_indices=chain_family_indices, - co_locate_local_families=True, - planner_config=planner_config, - ) - local_family_score = _score_cp_segment_schedule( - local_family_trial, - planner_config=planner_config, ) - if ( - local_family_trial.cross_rank_token_count == 0 - and local_family_score < best_score - ): - best = local_family_trial - best_score = local_family_score - if _is_balanced_zero_exchange_schedule( - best, - planner_config=planner_config, - ): - return best - candidate_sets = _candidate_chain_family_sets( - spec, - legal_chain_families=legal_chain_families, - cp_size=cp_size, - ) - for trial_chain in candidate_sets: - if trial_chain == chain_family_indices: - continue - trial = _materialize_cp_segment_schedule( + + score_cache: dict[ + frozenset[GdnSegmentDecisionKey], _GdnCpSegmentSearchDecision + ] = {} + + def decision_for( + chain_segment_keys: frozenset[GdnSegmentDecisionKey], + ) -> _GdnCpSegmentSearchDecision: + cached = score_cache.get(chain_segment_keys) + if cached is not None: + return cached + non_colocated_score = _score_cp_segment_decisions( spec, cp_size=cp_size, - attention_layout_index=attention_layout_index, segment_attention_counts=segment_attention_counts, - chain_family_indices=trial_chain, + chain_rank_counts_by_key=chain_rank_counts_by_key, + chain_cross_rank_tokens_by_key=chain_cross_rank_tokens_by_key, + chain_segment_keys=chain_segment_keys, co_locate_local_families=False, planner_config=planner_config, ) - trial_score = _score_cp_segment_schedule( - trial, - planner_config=planner_config, - ) - if trial.cross_rank_token_count == 0 and trial_score < best_score: - best = trial - best_score = trial_score - chain_family_indices = trial_chain - trial = _materialize_cp_segment_schedule( + colocated_score = _score_cp_segment_decisions( spec, cp_size=cp_size, - attention_layout_index=attention_layout_index, segment_attention_counts=segment_attention_counts, - chain_family_indices=trial_chain, + chain_rank_counts_by_key=chain_rank_counts_by_key, + chain_cross_rank_tokens_by_key=chain_cross_rank_tokens_by_key, + chain_segment_keys=chain_segment_keys, co_locate_local_families=True, planner_config=planner_config, ) - trial_score = _score_cp_segment_schedule( - trial, - planner_config=planner_config, + co_locate = colocated_score < non_colocated_score + decision = _GdnCpSegmentSearchDecision.model_construct( + chain_segment_keys=chain_segment_keys, + co_locate_local_families=co_locate, + score=colocated_score if co_locate else non_colocated_score, ) - if trial_score < best_score: - best = trial - best_score = trial_score - chain_family_indices = trial_chain - for _ in range(planner_config.cp_schedule_improve_iters): - improved = False - for family_index in legal_chain_families: - for trial_chain in ( - chain_family_indices - {family_index}, - chain_family_indices | {family_index}, + score_cache[chain_segment_keys] = decision + return decision + + best = decision_for(frozenset()) + beam_by_keys = {best.chain_segment_keys: best} + if legal_chain_keys: + all_chain = decision_for(legal_chain_keys) + beam_by_keys[all_chain.chain_segment_keys] = all_chain + if best.score - all_chain.score > planner_config.cp_chain_min_score_delta_ms: + best = all_chain + candidate_groups = _bounded_chain_candidate_groups( + spec, + legal_chain_segments, + segment_attention_counts=segment_attention_counts, + chain_rank_counts_by_key=chain_rank_counts_by_key, + planner_config=planner_config, + ) + beam = _best_cp_segment_search_decisions( + beam_by_keys.values(), + limit=planner_config.cp_chain_beam_width, + ) + stale_steps = 0 + for _ in range(planner_config.cp_chain_beam_max_steps): + if not candidate_groups: + break + expanded: dict[ + frozenset[GdnSegmentDecisionKey], _GdnCpSegmentSearchDecision + ] = {} + for decision in beam: + neighbors = [] + for segment_keys in _chain_beam_neighbor_groups( + decision.chain_segment_keys, + candidate_groups=candidate_groups, + branch_factor=planner_config.cp_chain_beam_branch_factor, ): - if trial_chain == chain_family_indices: - continue - trial = _materialize_cp_segment_schedule( - spec, - cp_size=cp_size, - attention_layout_index=attention_layout_index, - segment_attention_counts=segment_attention_counts, - chain_family_indices=trial_chain, - co_locate_local_families=False, - planner_config=planner_config, + if segment_keys.issubset(decision.chain_segment_keys): + next_keys = decision.chain_segment_keys - segment_keys + else: + next_keys = decision.chain_segment_keys | segment_keys + neighbors.append(decision_for(frozenset(next_keys))) + for neighbor in _best_cp_segment_search_decisions( + neighbors, + limit=planner_config.cp_chain_beam_branch_factor, + ): + expanded[neighbor.chain_segment_keys] = neighbor + if not expanded: + break + beam = _best_cp_segment_search_decisions( + (*beam, *expanded.values()), + limit=planner_config.cp_chain_beam_width, + ) + step_best = beam[0] + if best.score - step_best.score > planner_config.cp_chain_min_score_delta_ms: + best = step_best + stale_steps = 0 + else: + stale_steps += 1 + if stale_steps >= 2: + break + return best + + +def _chain_beam_neighbor_groups( + chain_segment_keys: frozenset[GdnSegmentDecisionKey], + *, + candidate_groups: tuple[frozenset[GdnSegmentDecisionKey], ...], + branch_factor: int, +) -> tuple[frozenset[GdnSegmentDecisionKey], ...]: + selected: list[frozenset[GdnSegmentDecisionKey]] = [] + for group in candidate_groups: + if group and not group.issubset(chain_segment_keys): + selected.append(group) + if len(selected) >= branch_factor: + return tuple(selected) + for group in reversed(candidate_groups): + if group and group.intersection(chain_segment_keys) and group not in selected: + selected.append(group) + if len(selected) >= branch_factor: + break + return tuple(selected) + + +def _best_cp_segment_search_decisions( + decisions: Any, + *, + limit: int, +) -> tuple[_GdnCpSegmentSearchDecision, ...]: + return tuple( + sorted( + decisions, + key=lambda decision: ( + decision.score, + len(decision.chain_segment_keys), + tuple(sorted(decision.chain_segment_keys)), + ), + )[:limit] + ) + + +def _bounded_chain_candidate_groups( + spec: GdnPackedExecutionSpec, + legal_chain_segments: tuple[GdnSegmentSpec, ...], + *, + segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], + chain_rank_counts_by_key: dict[GdnSegmentDecisionKey, tuple[int, ...]], + planner_config: GdnPlannerConfig, +) -> tuple[frozenset[GdnSegmentDecisionKey], ...]: + legal_key_set = frozenset(_segment_key(segment) for segment in legal_chain_segments) + if not legal_key_set: + return () + prefix_keys = frozenset( + _segment_key(family.prefix) + for family in spec.families + if _segment_key(family.prefix) in legal_key_set + ) + completion_keys = legal_key_set - prefix_keys + groups: list[frozenset[GdnSegmentDecisionKey]] = [] + for group in (legal_key_set, prefix_keys, completion_keys): + if group and group not in groups: + groups.append(group) + for group in _ranked_chain_beam_groups( + spec, + legal_chain_segments, + segment_attention_counts=segment_attention_counts, + chain_rank_counts_by_key=chain_rank_counts_by_key, + planner_config=planner_config, + ): + if group and group not in groups: + groups.append(group) + return tuple(groups[: planner_config.cp_chain_beam_candidate_limit]) + + +def _ranked_chain_beam_groups( + spec: GdnPackedExecutionSpec, + legal_chain_segments: tuple[GdnSegmentSpec, ...], + *, + segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], + chain_rank_counts_by_key: dict[GdnSegmentDecisionKey, tuple[int, ...]], + planner_config: GdnPlannerConfig, +) -> tuple[frozenset[GdnSegmentDecisionKey], ...]: + if not legal_chain_segments: + return () + priority_by_key = { + _segment_key(segment): _chain_beam_segment_priority( + segment, + segment_attention_counts=segment_attention_counts, + chain_rank_counts_by_key=chain_rank_counts_by_key, + ) + for segment in legal_chain_segments + } + legal_key_set = frozenset(priority_by_key) + groups: set[frozenset[GdnSegmentDecisionKey]] = { + frozenset((key,)) for key in legal_key_set + } + for family in spec.families: + completion_keys = frozenset( + _segment_key(completion) + for completion in family.completions + if _segment_key(completion) in legal_key_set + ) + if len(completion_keys) > 1: + groups.add(completion_keys) + family_keys = completion_keys + prefix_key = _segment_key(family.prefix) + if prefix_key in legal_key_set: + family_keys = family_keys | frozenset((prefix_key,)) + if len(family_keys) > 1: + groups.add(family_keys) + ranked = tuple( + sorted( + groups, + key=lambda group: _chain_beam_group_priority( + group, priority_by_key=priority_by_key + ), + reverse=True, + ) + ) + limit = planner_config.cp_chain_beam_candidate_limit + if len(ranked) <= limit: + return ranked + high_count = (limit + 1) // 2 + low_count = limit - high_count + selected = [*ranked[:high_count]] + for group in ranked[-low_count:]: + if group not in selected: + selected.append(group) + return tuple(selected) + + +def _chain_beam_group_priority( + group: frozenset[GdnSegmentDecisionKey], + *, + priority_by_key: dict[GdnSegmentDecisionKey, tuple[int, int, int, int]], +) -> tuple[int, int, int, int, int]: + priorities = tuple(priority_by_key[key] for key in group) + return ( + sum(priority[0] for priority in priorities), + sum(priority[1] for priority in priorities), + max((priority[2] for priority in priorities), default=0), + sum(priority[3] for priority in priorities), + len(group), + ) + + +def _chain_beam_segment_priority( + segment: GdnSegmentSpec, + *, + segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], + chain_rank_counts_by_key: dict[GdnSegmentDecisionKey, tuple[int, ...]], +) -> tuple[int, int, int, int]: + key = _segment_key(segment) + chain_max_load = max(chain_rank_counts_by_key[key], default=0) + best_attention_locality = max(segment_attention_counts[key], default=0) + chain_load_relief = segment.length - chain_max_load + minimum_local_exchange = segment.length - best_attention_locality + return ( + chain_load_relief, + segment.length, + best_attention_locality, + -minimum_local_exchange, + ) + + +def _score_cp_segment_decisions( + spec: GdnPackedExecutionSpec, + *, + cp_size: int, + segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], + chain_rank_counts_by_key: dict[GdnSegmentDecisionKey, tuple[int, ...]], + chain_cross_rank_tokens_by_key: dict[GdnSegmentDecisionKey, int], + chain_segment_keys: frozenset[GdnSegmentDecisionKey], + co_locate_local_families: bool, + planner_config: GdnPlannerConfig, +) -> float: + rank_loads = [0] * cp_size + local_prefix_segments_by_rank: list[list[GdnSegmentSpec]] = [ + [] for _ in range(cp_size) + ] + local_completion_segments_by_rank: list[list[GdnSegmentSpec]] = [ + [] for _ in range(cp_size) + ] + chain_prefix_segments: list[GdnSegmentSpec] = [] + chain_completion_segments: list[GdnSegmentSpec] = [] + parent_state_exchange_families: set[int] = set() + cross_rank_token_count = 0 + + for family in spec.families: + prefix_key = _segment_key(family.prefix) + chain_prefix = prefix_key in chain_segment_keys + local_completions = tuple( + completion + for completion in family.completions + if _segment_key(completion) not in chain_segment_keys + ) + prefix_owner: int | None = None + if chain_prefix: + chain_prefix_segments.append(family.prefix) + cross_rank_token_count += _add_chain_search_load( + rank_loads, + family.prefix, + chain_rank_counts_by_key=chain_rank_counts_by_key, + chain_cross_rank_tokens_by_key=chain_cross_rank_tokens_by_key, + ) + else: + owner_segments = ( + (family.prefix, *local_completions) + if co_locate_local_families + else (family.prefix,) + ) + prefix_owner = _best_segment_owner( + owner_segments, + rank_loads, + segment_attention_counts=segment_attention_counts, + planner_config=planner_config, + ) + local_prefix_segments_by_rank[prefix_owner].append(family.prefix) + cross_rank_token_count += _add_local_search_load( + rank_loads, + prefix_owner, + family.prefix, + segment_attention_counts=segment_attention_counts, + ) + for completion in family.completions: + completion_key = _segment_key(completion) + if completion_key in chain_segment_keys: + chain_completion_segments.append(completion) + cross_rank_token_count += _add_chain_search_load( + rank_loads, + completion, + chain_rank_counts_by_key=chain_rank_counts_by_key, + chain_cross_rank_tokens_by_key=chain_cross_rank_tokens_by_key, ) - trial_score = _score_cp_segment_schedule( - trial, + if not chain_prefix: + parent_state_exchange_families.add(family.family_index) + continue + if co_locate_local_families and not chain_prefix: + if prefix_owner is None: + raise RuntimeError( + "co-located local completion planning lost the prefix owner" + ) + owner = prefix_owner + else: + owner = _best_segment_owner( + (completion,), + rank_loads, + segment_attention_counts=segment_attention_counts, planner_config=planner_config, ) - if trial_score < best_score: - best = trial - best_score = trial_score - chain_family_indices = trial_chain - improved = True - break - if improved: - break - if not improved: - break - return best + if not chain_prefix: + if prefix_owner is None: + raise RuntimeError( + "local completion planning lost the prefix owner" + ) + if owner != prefix_owner: + parent_state_exchange_families.add(family.family_index) + local_completion_segments_by_rank[owner].append(completion) + cross_rank_token_count += _add_local_search_load( + rank_loads, + owner, + completion, + segment_attention_counts=segment_attention_counts, + ) + ( + local_work_by_rank, + local_bucket_count, + local_segment_count, + ) = _estimate_local_rank_kernel_work( + tuple(tuple(segments) for segments in local_prefix_segments_by_rank), + tuple(tuple(segments) for segments in local_completion_segments_by_rank), + planner_config=planner_config, + ) + chain_work_by_rank, chain_bucket_count = _estimate_chain_rank_kernel_work( + cp_size=cp_size, + chain_prefix_segments=tuple(chain_prefix_segments), + chain_completion_segments=tuple(chain_completion_segments), + chain_rank_counts_by_key=chain_rank_counts_by_key, + planner_config=planner_config, + ) + return _score_cp_segment_stats( + rank_local_work=local_work_by_rank, + rank_chain_work=chain_work_by_rank, + rank_real_tokens=tuple(rank_loads), + cross_rank_token_count=cross_rank_token_count, + parent_state_exchange_family_count=len(parent_state_exchange_families), + local_bucket_count=local_bucket_count, + local_segment_count=local_segment_count, + chain_bucket_count=chain_bucket_count, + planner_config=planner_config, + ) -def _is_balanced_zero_exchange_schedule( - schedule: GdnCpSegmentSchedule, +def _estimate_local_rank_kernel_work( + local_prefix_segments_by_rank: tuple[tuple[GdnSegmentSpec, ...], ...], + local_completion_segments_by_rank: tuple[tuple[GdnSegmentSpec, ...], ...], *, planner_config: GdnPlannerConfig, -) -> bool: - rank_loads = list(schedule.gdn_token_counts_by_rank) - if not rank_loads or any(load == 0 for load in rank_loads): - return False - if schedule.cross_rank_token_count: - return False - if schedule.parent_state_exchange_family_indices: - return False - if max(rank_loads) > planner_config.max_zero_exchange_load_imbalance * ( - sum(rank_loads) / len(rank_loads) +) -> tuple[tuple[int, ...], int, int]: + rank_work: list[int] = [] + rank_bucket_counts: list[int] = [] + rank_segment_counts: list[int] = [] + for prefix_segments, completion_segments in zip( + local_prefix_segments_by_rank, + local_completion_segments_by_rank, + strict=True, ): - return False - return True + prefix_family_indices = {segment.family_index for segment in prefix_segments} + chunk_local_completion_segments = tuple( + segment + for segment in completion_segments + if segment.family_index in prefix_family_indices + ) + plain_local_completion_segments = tuple( + segment + for segment in completion_segments + if segment.family_index not in prefix_family_indices + ) + chunk_work, chunk_bucket_count = _estimate_chunk_aligned_local_work( + prefix_segments, + chunk_local_completion_segments, + planner_config=planner_config, + ) + completion_work, completion_bucket_count = _padded_work_from_lengths( + tuple(segment.length for segment in plain_local_completion_segments), + max_padding_ratio=planner_config.max_padding_ratio, + max_segments_per_batch=planner_config.max_segments_per_batch, + ) + rank_work.append(chunk_work + completion_work) + rank_bucket_counts.append(chunk_bucket_count + completion_bucket_count) + rank_segment_counts.append(len(prefix_segments) + len(completion_segments)) + return ( + tuple(rank_work), + max(rank_bucket_counts, default=0), + max(rank_segment_counts, default=0), + ) + + +def _estimate_chunk_aligned_local_work( + prefix_segments: tuple[GdnSegmentSpec, ...], + completion_segments: tuple[GdnSegmentSpec, ...], + *, + planner_config: GdnPlannerConfig, +) -> tuple[int, int]: + completions_by_family: dict[int, list[GdnSegmentSpec]] = {} + for completion in completion_segments: + completions_by_family.setdefault(completion.family_index, []).append(completion) + boundary_lengths: list[int] = [] + tail_lengths: list[int] = [] + completion_column_lengths: list[int] = [] + for prefix in prefix_segments: + boundary_end = _prefix_chunk_boundary_end(prefix) + boundary_length = boundary_end - prefix.start + if boundary_length > 0: + boundary_lengths.append(boundary_length) + tail_length = prefix.end - boundary_end + family_completions = tuple(completions_by_family.get(prefix.family_index, ())) + if tail_length > 0 and not family_completions: + tail_lengths.append(tail_length) + for completion in family_completions: + completion_column_lengths.append(tail_length + completion.length) + boundary_work, boundary_bucket_count = _padded_work_from_lengths( + tuple(boundary_lengths), + max_padding_ratio=planner_config.max_padding_ratio, + max_segments_per_batch=planner_config.max_segments_per_batch, + ) + tail_work, tail_bucket_count = _padded_work_from_lengths( + tuple(tail_lengths), + max_padding_ratio=planner_config.max_padding_ratio, + max_segments_per_batch=planner_config.max_segments_per_batch, + ) + completion_work, completion_bucket_count = _padded_work_from_lengths( + tuple(completion_column_lengths), + max_padding_ratio=planner_config.max_padding_ratio, + max_segments_per_batch=planner_config.max_segments_per_batch, + ) + return ( + boundary_work + tail_work + completion_work, + boundary_bucket_count + tail_bucket_count + completion_bucket_count, + ) + + +def _estimate_chain_rank_kernel_work( + *, + cp_size: int, + chain_prefix_segments: tuple[GdnSegmentSpec, ...], + chain_completion_segments: tuple[GdnSegmentSpec, ...], + chain_rank_counts_by_key: dict[GdnSegmentDecisionKey, tuple[int, ...]], + planner_config: GdnPlannerConfig, +) -> tuple[tuple[int, ...], int]: + rank_work = [0] * cp_size + bucket_count = 0 + for segments in (chain_prefix_segments, chain_completion_segments): + buckets = _batch_segments_by_padded_work( + segments, + max_padding_ratio=planner_config.max_padding_ratio, + max_segments_per_batch=planner_config.max_segments_per_batch, + ) + bucket_count += len(buckets) + for bucket in buckets: + for rank in range(cp_size): + lengths = tuple( + chain_rank_counts_by_key[_segment_key(segment)][rank] + for segment in bucket + ) + rank_work[rank] += max(lengths, default=0) * len(lengths) + return tuple(rank_work), bucket_count + + +def _padded_work_from_lengths( + lengths: tuple[int, ...], + *, + max_padding_ratio: float, + max_segments_per_batch: int, +) -> tuple[int, int]: + if not lengths: + return 0, 0 + ordered = sorted(length for length in lengths if length > 0) + if not ordered: + return 0, 0 + bucket_count = 0 + padded_work = 0 + current_count = 0 + current_tokens = 0 + current_max = 0 + for length in ordered: + next_count = current_count + 1 + next_tokens = current_tokens + length + next_max = max(current_max, length) + next_padded = next_max * next_count + can_extend = current_count == 0 or ( + next_count <= max_segments_per_batch + and next_padded <= max_padding_ratio * next_tokens + ) + if not can_extend: + bucket_count += 1 + padded_work += current_max * current_count + current_count = 0 + current_tokens = 0 + current_max = 0 + current_count += 1 + current_tokens += length + current_max = max(current_max, length) + if current_count: + bucket_count += 1 + padded_work += current_max * current_count + return padded_work, bucket_count + + +def _add_chain_search_load( + rank_loads: list[int], + segment: GdnSegmentSpec, + *, + chain_rank_counts_by_key: dict[GdnSegmentDecisionKey, tuple[int, ...]], + chain_cross_rank_tokens_by_key: dict[GdnSegmentDecisionKey, int], +) -> int: + key = _segment_key(segment) + for rank, token_count in enumerate(chain_rank_counts_by_key[key]): + rank_loads[rank] += token_count + return chain_cross_rank_tokens_by_key[key] + + +def _add_local_search_load( + rank_loads: list[int], + rank: int, + segment: GdnSegmentSpec, + *, + segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], +) -> int: + rank_loads[rank] += segment.length + return segment.length - segment_attention_counts[_segment_key(segment)][rank] + + +def _chain_segment_rank_counts_and_cross_rank_tokens( + segment: GdnSegmentSpec, + spec: GdnPackedExecutionSpec, + *, + cp_size: int, + attention_layout_index: _AttentionLayoutIndex, +) -> tuple[tuple[int, ...], int]: + token_start = _segment_token_start(segment, spec.sequence_length) + attention_shards = _attention_contiguous_chain_shards( + token_start, + segment.length, + cp_size=cp_size, + attention_layout_index=attention_layout_index, + ) + if attention_shards is not None: + return tuple(len(shard) for shard in attention_shards), 0 + shard_lengths = _fla_aligned_chain_shard_lengths(segment.length, cp_size=cp_size) + cross_rank_tokens = 0 + start = 0 + for rank, shard_length in enumerate(shard_lengths): + end = start + shard_length + shard_start = token_start + start + cross_rank_tokens += shard_length - _attention_overlap_count( + attention_layout_index, + rank, + shard_start, + shard_start + shard_length, + ) + start = end + return shard_lengths, cross_rank_tokens def _materialize_cp_segment_schedule( @@ -1949,7 +2566,7 @@ def _materialize_cp_segment_schedule( cp_size: int, attention_layout_index: _AttentionLayoutIndex, segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], - chain_family_indices: frozenset[int], + chain_segment_keys: frozenset[GdnSegmentDecisionKey], co_locate_local_families: bool, planner_config: GdnPlannerConfig, ) -> GdnCpSegmentSchedule: @@ -1968,7 +2585,15 @@ def _materialize_cp_segment_schedule( cross_rank_token_count = 0 for family in spec.families: - if family.family_index in chain_family_indices: + prefix_key = _segment_key(family.prefix) + chain_prefix = prefix_key in chain_segment_keys + local_completions = tuple( + completion + for completion in family.completions + if _segment_key(completion) not in chain_segment_keys + ) + prefix_owner: int | None = None + if chain_prefix: chain_prefix_segments.append(family.prefix) cross_rank_token_count += _append_chain_segment( gdn_ranges_by_rank, @@ -1977,64 +2602,14 @@ def _materialize_cp_segment_schedule( spec, attention_layout_index=attention_layout_index, ) - for completion in family.completions: - if _can_chain_segment( - completion, cp_size=cp_size, planner_config=planner_config - ): - chain_completion_segments.append(completion) - cross_rank_token_count += _append_chain_segment( - gdn_ranges_by_rank, - rank_loads, - completion, - spec, - attention_layout_index=attention_layout_index, - ) - continue - owner = _best_segment_owner( - (completion,), - rank_loads, - segment_attention_counts=segment_attention_counts, - planner_config=planner_config, - ) - local_completion_segments_by_rank[owner].append(completion) - cross_rank_token_count += _append_local_segment( - gdn_ranges_by_rank, - rank_loads, - owner, - completion, - spec, - segment_attention_counts=segment_attention_counts, - ) else: - if co_locate_local_families: - owner = _best_segment_owner( - (family.prefix, *family.completions), - rank_loads, - segment_attention_counts=segment_attention_counts, - planner_config=planner_config, - ) - local_prefix_segments_by_rank[owner].append(family.prefix) - cross_rank_token_count += _append_local_segment( - gdn_ranges_by_rank, - rank_loads, - owner, - family.prefix, - spec, - segment_attention_counts=segment_attention_counts, - ) - for completion in family.completions: - local_completion_segments_by_rank[owner].append(completion) - cross_rank_token_count += _append_local_segment( - gdn_ranges_by_rank, - rank_loads, - owner, - completion, - spec, - segment_attention_counts=segment_attention_counts, - ) - continue + owner_segments = ( + (family.prefix, *local_completions) + if co_locate_local_families + else (family.prefix,) + ) prefix_owner = _best_segment_owner( - (family.prefix,), + owner_segments, rank_loads, segment_attention_counts=segment_attention_counts, planner_config=planner_config, @@ -2048,27 +2623,61 @@ def _materialize_cp_segment_schedule( spec, segment_attention_counts=segment_attention_counts, ) - for completion in family.completions: + for completion in family.completions: + if _segment_key(completion) in chain_segment_keys: + chain_completion_segments.append(completion) + cross_rank_token_count += _append_chain_segment( + gdn_ranges_by_rank, + rank_loads, + completion, + spec, + attention_layout_index=attention_layout_index, + ) + if not chain_prefix: + if prefix_owner is None: + raise RuntimeError( + "local-prefix/chained-completion planning lost the prefix owner" + ) + parent_state_exchange_families.add(family.family_index) + for dest_rank in range(cp_size): + if dest_rank == prefix_owner: + continue + parent_state_transfer_families.setdefault( + (prefix_owner, dest_rank), set() + ).add(family.family_index) + continue + if co_locate_local_families and not chain_prefix: + if prefix_owner is None: + raise RuntimeError( + "co-located local completion planning lost the prefix owner" + ) + owner = prefix_owner + else: owner = _best_segment_owner( (completion,), rank_loads, segment_attention_counts=segment_attention_counts, planner_config=planner_config, ) + if not chain_prefix: + if prefix_owner is None: + raise RuntimeError( + "local completion planning lost the prefix owner" + ) if owner != prefix_owner: parent_state_exchange_families.add(family.family_index) parent_state_transfer_families.setdefault( (prefix_owner, owner), set() ).add(family.family_index) - local_completion_segments_by_rank[owner].append(completion) - cross_rank_token_count += _append_local_segment( - gdn_ranges_by_rank, - rank_loads, - owner, - completion, - spec, - segment_attention_counts=segment_attention_counts, - ) + local_completion_segments_by_rank[owner].append(completion) + cross_rank_token_count += _append_local_segment( + gdn_ranges_by_rank, + rank_loads, + owner, + completion, + spec, + segment_attention_counts=segment_attention_counts, + ) return GdnCpSegmentSchedule.model_construct( gdn_token_counts_by_rank=tuple(rank_loads), @@ -2112,82 +2721,140 @@ def _build_local_family_rank_execution_plan( target_rank_load = spec.real_token_count / cp_size loads = [0] * cp_size prefix_owner_by_family: list[int] = [] - completion_owner_by_family: list[int] = [] + completion_owners_by_family: list[tuple[int, ...]] = [] for family in spec.families: - if _can_chain_family(family, cp_size=cp_size, planner_config=planner_config): - return None - if ( - family.prefix.length - > planner_config.max_zero_exchange_load_imbalance * target_rank_load + if _has_chainable_segment( + family, cp_size=cp_size, planner_config=planner_config ): return None + prefix_locality_limit = max( + planner_config.max_zero_exchange_load_imbalance * target_rank_load, + min(64.0, float(spec.real_token_count)), + ) + if family.prefix.length > prefix_locality_limit: + return None owner = _least_loaded_rank(loads) prefix_owner_by_family.append(owner) - completion_owner_by_family.append(owner) + completion_owners_by_family.append(tuple(owner for _ in family.completions)) loads[owner] += family.token_count if max(loads, default=0) > ( planner_config.local_completion_rebalance_min_imbalance * target_rank_load ): - completion_owner_by_family = list( - _rebalance_local_completion_bundles( + completion_owners_by_family = list( + _rebalance_local_completion_segments( spec, prefix_owner_by_family=tuple(prefix_owner_by_family), - completion_owner_by_family=tuple(completion_owner_by_family), + completion_owners_by_family=tuple(completion_owners_by_family), initial_loads=tuple(loads), planner_config=planner_config, ) ) - local_tokens, prefix_segments, completion_segments = ( - _materialize_local_family_rank_assignment( - spec, - cp_rank=cp_rank, - prefix_owner_by_family=tuple(prefix_owner_by_family), - completion_owner_by_family=tuple(completion_owner_by_family), - ) + rank_assignments = _materialize_local_family_rank_assignments( + spec, + cp_size=cp_size, + prefix_owner_by_family=tuple(prefix_owner_by_family), + completion_owners_by_family=tuple(completion_owners_by_family), + ) + local_token_count, local_token_ranges, prefix_segments, completion_segments = ( + rank_assignments[cp_rank] ) parent_state_transfer_families: dict[tuple[int, int], set[int]] = {} for family in spec.families: prefix_owner = prefix_owner_by_family[family.family_index] - completion_owner = completion_owner_by_family[family.family_index] - if completion_owner != prefix_owner and family.completions: + completion_owners = completion_owners_by_family[family.family_index] + for completion_owner in sorted(set(completion_owners)): + if completion_owner == prefix_owner: + continue parent_state_transfer_families.setdefault( (prefix_owner, completion_owner), set() ).add(family.family_index) - token_indices_by_rank = tuple( - local_tokens if rank == cp_rank else () for rank in range(cp_size) - ) + from art.megatron.gdn.layout import GdnCpExchangePlan, GdnCpPeerTransfer + + token_counts_by_rank = tuple(assignment[0] for assignment in rank_assignments) identity_exchange = GdnCpExchangePlan.model_construct( cp_size=cp_size, - source_token_counts_by_rank=tuple( - len(tokens) for tokens in token_indices_by_rank - ), - dest_token_counts_by_rank=tuple( - len(tokens) for tokens in token_indices_by_rank - ), + source_token_counts_by_rank=token_counts_by_rank, + dest_token_counts_by_rank=token_counts_by_rank, transfers=tuple( GdnCpPeerTransfer.model_construct( source_rank=rank, dest_rank=rank, - token_count=len(tokens), + token_count=token_count, source_positions_tensor=None, dest_positions_tensor=None, ) - for rank, tokens in enumerate(token_indices_by_rank) - if tokens + for rank, token_count in enumerate(token_counts_by_rank) + if token_count ), ) - local_token_ranges = _local_token_ranges(local_tokens) - prefix_buckets = _batch_segments_by_padded_work( - prefix_segments, - max_padding_ratio=planner_config.max_padding_ratio, - max_segments_per_batch=planner_config.max_segments_per_batch, + parent_state_exchange_family_indices = tuple( + sorted( + family_index + for family_indices in parent_state_transfer_families.values() + for family_index in family_indices + ) + ) + schedule = GdnCpSegmentSchedule.model_construct( + gdn_token_counts_by_rank=token_counts_by_rank, + gdn_token_ranges_by_rank=tuple( + assignment[1] for assignment in rank_assignments + ), + cross_rank_token_count=0, + chain_prefix_buckets=(), + chain_completion_buckets=(), + local_prefix_segments_by_rank=tuple( + assignment[2] for assignment in rank_assignments + ), + local_completion_segments_by_rank=tuple( + assignment[3] for assignment in rank_assignments + ), + parent_state_exchange_family_indices=parent_state_exchange_family_indices, + parent_state_transfers=_build_parent_state_transfer_plans( + parent_state_transfer_families + ), + ) + if parent_state_exchange_family_indices: + ( + remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers, + remote_prefix_tail_families, + ) = _build_remote_prefix_tail_plans( + spec, + schedule, + cp_rank=cp_rank, + device=device, + planner_config=planner_config, + ) + else: + ( + remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers, + remote_prefix_tail_families, + ) = _empty_remote_prefix_tail_plans() + local_prefix_family_indices = {segment.family_index for segment in prefix_segments} + chunk_local_completion_segments = tuple( + segment + for segment in completion_segments + if segment.family_index in local_prefix_family_indices + ) + suffix_only_completion_segments = tuple( + segment + for segment in completion_segments + if segment.family_index not in local_prefix_family_indices + and segment.family_index not in remote_prefix_tail_families ) ready_completion_segments, remote_completion_segments = ( _split_ready_and_remote_completion_segments( - completion_segments, - local_prefix_segments=prefix_segments, + suffix_only_completion_segments, + local_prefix_segments=(), chain_prefix_buckets=(), ) ) @@ -2201,16 +2868,6 @@ def _build_local_family_rank_execution_plan( max_padding_ratio=planner_config.max_padding_ratio, max_segments_per_batch=planner_config.max_segments_per_batch, ) - completion_buckets = ready_completion_buckets + remote_completion_buckets - prefix_family_order = tuple( - segment.family_index for bucket in prefix_buckets for segment in bucket - ) - local_prefix_bucket_plans = _build_position_bucket_plans( - prefix_buckets, - local_token_ranges, - sequence_length=spec.sequence_length, - device=device, - ) ready_completion_bucket_plans = _build_position_bucket_plans( ready_completion_buckets, local_token_ranges, @@ -2229,10 +2886,10 @@ def _build_local_family_rank_execution_plan( ( prefix_boundary_buckets, prefix_tail_buckets, - completion_warmup_buckets, + completion_with_prefix_tail_buckets, ) = _build_chunk_aligned_position_bucket_plans( prefix_segments, - completion_segments, + chunk_local_completion_segments, local_token_ranges, sequence_length=spec.sequence_length, device=device, @@ -2242,129 +2899,230 @@ def _build_local_family_rank_execution_plan( cp_rank=cp_rank, cp_size=cp_size, batch_size=1, - sequence_length=len(local_tokens), + sequence_length=local_token_count, packed_batch_size=spec.batch_size, packed_sequence_length=spec.sequence_length, real_token_mask=torch.ones( - 1, len(local_tokens), device=device, dtype=torch.bool + 1, local_token_count, device=device, dtype=torch.bool ), family_count=spec.family_count, completion_count=spec.completion_count, - prefix_buckets=(), - completion_buckets=(), - local_prefix_buckets=local_prefix_bucket_plans, + local_prefix_buckets=(), local_completion_buckets=local_completion_bucket_plans, ready_local_completion_buckets=ready_completion_bucket_plans, remote_local_completion_buckets=remote_completion_bucket_plans, chain_prefix_buckets=(), chain_completion_buckets=(), prefix_table_is_dense_ordered=( - prefix_family_order == tuple(range(spec.family_count)) + tuple(segment.family_index for segment in prefix_segments) + == tuple(range(spec.family_count)) ), attention_to_gdn=identity_exchange, gdn_to_attention=identity_exchange, attention_token_ranges=local_token_ranges, gdn_token_ranges=local_token_ranges, - attention_token_count=len(local_tokens), - gdn_token_count=len(local_tokens), + attention_token_count=local_token_count, + gdn_token_count=local_token_count, parent_state_exchange_family_indices=tuple( - sorted( - family.family_index - for family in spec.families - if completion_owner_by_family[family.family_index] - != prefix_owner_by_family[family.family_index] - and family.completions - ) + family_index + for family_index in parent_state_exchange_family_indices + if family_index not in remote_prefix_tail_families ), - parent_state_transfers=_transfer_plans_to_device( + parent_state_transfers=_filter_parent_state_transfers( _build_parent_state_transfer_plans(parent_state_transfer_families), + excluded_families=remote_prefix_tail_families, device=device, ), prefix_boundary_buckets=prefix_boundary_buckets, prefix_tail_buckets=prefix_tail_buckets, - completion_warmup_buckets=completion_warmup_buckets, + completion_with_prefix_tail_buckets=completion_with_prefix_tail_buckets, + remote_prefix_tail_buckets=remote_prefix_tail_buckets, + remote_completion_with_prefix_tail_buckets=remote_completion_with_prefix_tail_buckets, + remote_prefix_tail_exchange=remote_prefix_tail_exchange, + remote_prefix_tail_backward_exchange=remote_prefix_tail_backward_exchange, + remote_prefix_tail_state_transfers=remote_prefix_tail_state_transfers, ) -def _rebalance_local_completion_bundles( +def _rebalance_local_completion_segments( spec: GdnPackedExecutionSpec, *, prefix_owner_by_family: tuple[int, ...], - completion_owner_by_family: tuple[int, ...], + completion_owners_by_family: tuple[tuple[int, ...], ...], initial_loads: tuple[int, ...], planner_config: GdnPlannerConfig, -) -> tuple[int, ...]: - owners = list(completion_owner_by_family) +) -> tuple[tuple[int, ...], ...]: + owners = [list(family_owners) for family_owners in completion_owners_by_family] loads = list(initial_loads) + remote_owners_by_family = [ + { + owner + for owner in family_owners + if owner != prefix_owner_by_family[family_index] + } + for family_index, family_owners in enumerate(owners) + ] + transfer_count = sum( + len(remote_owners) for remote_owners in remote_owners_by_family + ) - def score(candidate_loads: list[int], candidate_owners: list[int]) -> float: + def score(candidate_loads: list[int], candidate_transfer_count: int) -> float: max_load = max(candidate_loads, default=0) idle_tokens = sum(max_load - load for load in candidate_loads) - transfer_count = sum( - 1 - for index, owner in enumerate(candidate_owners) - if owner != prefix_owner_by_family[index] - and spec.families[index].completions - ) return ( max_load + planner_config.rank_idle_token_cost * idle_tokens - + planner_config.parent_state_exchange_penalty_tokens * transfer_count + + planner_config.parent_state_exchange_penalty_tokens + * candidate_transfer_count ) - best_score = score(loads, owners) + best_score = score(loads, transfer_count) while True: - best_move: tuple[int, int, list[int], list[int], float] | None = None + best_move: ( + tuple[int, int, int, tuple[int, ...], list[int], int, float] | None + ) = None for family in spec.families: - completion_tokens = sum(segment.length for segment in family.completions) - if completion_tokens <= 0: - continue - source = owners[family.family_index] - for dest in range(len(loads)): - if dest == source: - continue - candidate_loads = list(loads) - candidate_owners = list(owners) - candidate_loads[source] -= completion_tokens - candidate_loads[dest] += completion_tokens - candidate_owners[family.family_index] = dest - candidate_score = score(candidate_loads, candidate_owners) - if candidate_score >= best_score: - continue - if best_move is None or candidate_score < best_move[4]: - best_move = ( - family.family_index, - dest, - candidate_loads, - candidate_owners, - candidate_score, - ) + family_owners = owners[family.family_index] + prefix_owner = prefix_owner_by_family[family.family_index] + original_remote_owners = remote_owners_by_family[family.family_index] + for source in sorted(set(family_owners)): + source_children = [ + child_index + for child_index, owner in enumerate(family_owners) + if owner == source + ] + ordered_children = sorted( + source_children, + key=lambda child_index: family.completions[child_index].length, + reverse=True, + ) + for dest in range(len(loads)): + if dest == source: + continue + moved_tokens = 0 + moved_children = [] + for child_index in ordered_children: + moved_tokens += family.completions[child_index].length + moved_children.append(child_index) + candidate_loads = list(loads) + candidate_loads[source] -= moved_tokens + candidate_loads[dest] += moved_tokens + candidate_remote_owners = set(original_remote_owners) + if source != prefix_owner and len(moved_children) == len( + source_children + ): + candidate_remote_owners.discard(source) + if dest != prefix_owner: + candidate_remote_owners.add(dest) + candidate_transfer_count = ( + transfer_count + - len(original_remote_owners) + + len(candidate_remote_owners) + ) + candidate_score = score( + candidate_loads, candidate_transfer_count + ) + if candidate_score >= best_score: + continue + if best_move is None or candidate_score < best_move[-1]: + best_move = ( + family.family_index, + source, + dest, + tuple(moved_children), + candidate_loads, + candidate_transfer_count, + candidate_score, + ) if best_move is None: - return tuple(owners) - _, _, loads, owners, best_score = best_move - - -def _materialize_local_family_rank_assignment( + return tuple(tuple(item) for item in owners) + ( + family_index, + _source, + dest, + moved_children, + loads, + transfer_count, + best_score, + ) = best_move + for child_index in moved_children: + owners[family_index][child_index] = dest + prefix_owner = prefix_owner_by_family[family_index] + remote_owners_by_family[family_index] = { + owner for owner in set(owners[family_index]) if owner != prefix_owner + } + + +def _materialize_local_family_rank_assignments( spec: GdnPackedExecutionSpec, *, - cp_rank: int, + cp_size: int, prefix_owner_by_family: tuple[int, ...], - completion_owner_by_family: tuple[int, ...], -) -> tuple[tuple[int, ...], tuple[GdnSegmentSpec, ...], tuple[GdnSegmentSpec, ...]]: - token_indices: list[int] = [] - prefix_segments: list[GdnSegmentSpec] = [] - completion_segments: list[GdnSegmentSpec] = [] + completion_owners_by_family: tuple[tuple[int, ...], ...], +) -> tuple[ + tuple[ + int, + tuple[tuple[int, int, int], ...], + tuple[GdnSegmentSpec, ...], + tuple[GdnSegmentSpec, ...], + ], + ..., +]: + token_ranges_by_rank: list[list[tuple[int, int, int]]] = [ + [] for _ in range(cp_size) + ] + token_counts_by_rank = [0] * cp_size + prefix_segments_by_rank: list[list[GdnSegmentSpec]] = [[] for _ in range(cp_size)] + completion_segments_by_rank: list[list[GdnSegmentSpec]] = [ + [] for _ in range(cp_size) + ] + sequence_length = spec.sequence_length for family in spec.families: prefix_owner = prefix_owner_by_family[family.family_index] - completion_owner = completion_owner_by_family[family.family_index] - if prefix_owner == cp_rank: - prefix_segments.append(family.prefix) - token_indices.extend(family.prefix.linear_indices(spec.sequence_length)) - for completion in family.completions: - if completion_owner == cp_rank: - completion_segments.append(completion) - token_indices.extend(completion.linear_indices(spec.sequence_length)) - return tuple(token_indices), tuple(prefix_segments), tuple(completion_segments) + prefix_segments_by_rank[prefix_owner].append(family.prefix) + prefix_token_start = ( + family.prefix.row_index * sequence_length + family.prefix.start + ) + prefix_position_start = token_counts_by_rank[prefix_owner] + token_ranges_by_rank[prefix_owner].append( + ( + prefix_token_start, + prefix_token_start + family.prefix.length, + prefix_position_start, + ) + ) + token_counts_by_rank[prefix_owner] = ( + prefix_position_start + family.prefix.length + ) + for completion, completion_owner in zip( + family.completions, + completion_owners_by_family[family.family_index], + strict=True, + ): + completion_segments_by_rank[completion_owner].append(completion) + completion_token_start = ( + completion.row_index * sequence_length + completion.start + ) + completion_position_start = token_counts_by_rank[completion_owner] + token_ranges_by_rank[completion_owner].append( + ( + completion_token_start, + completion_token_start + completion.length, + completion_position_start, + ) + ) + token_counts_by_rank[completion_owner] = ( + completion_position_start + completion.length + ) + return tuple( + ( + token_counts_by_rank[rank], + tuple(token_ranges_by_rank[rank]), + tuple(prefix_segments_by_rank[rank]), + tuple(completion_segments_by_rank[rank]), + ) + for rank in range(cp_size) + ) def _empty_local_family_rank_execution_plan( @@ -2374,6 +3132,8 @@ def _empty_local_family_rank_execution_plan( cp_rank: int, cp_size: int, ) -> GdnRankExecutionPlan: + from art.megatron.gdn.layout import GdnCpExchangePlan + identity_exchange = GdnCpExchangePlan.model_construct( cp_size=cp_size, source_token_counts_by_rank=tuple(0 for _ in range(cp_size)), @@ -2390,8 +3150,6 @@ def _empty_local_family_rank_execution_plan( real_token_mask=torch.ones(1, 0, device=device, dtype=torch.bool), family_count=spec.family_count, completion_count=spec.completion_count, - prefix_buckets=(), - completion_buckets=(), local_prefix_buckets=(), local_completion_buckets=(), ready_local_completion_buckets=(), @@ -2416,12 +3174,21 @@ def _can_chain_segment( cp_size: int, planner_config: GdnPlannerConfig, ) -> bool: + min_tokens = ( + planner_config.cp_chain_min_prefix_only_tokens + if segment.kind == "prefix" + else planner_config.cp_chain_min_total_tokens + ) + if segment.length < min_tokens: + return False if segment.length < cp_size: return False + if segment.length // FLA_CHUNK_SIZE < cp_size: + return False per_rank = segment.length / cp_size if per_rank < planner_config.cp_chain_min_tokens_per_rank: return False - return segment.length >= planner_config.cp_chain_min_total_tokens + return True def _build_parent_state_transfer_plans( @@ -2475,22 +3242,18 @@ def _transfer_plans_to_device( ) -def _can_chain_family( +def _has_chainable_segment( family: GdnPackedFamilySpec, *, cp_size: int, planner_config: GdnPlannerConfig, ) -> bool: - if not _can_chain_prefix_segment( + return _can_chain_prefix_segment( family.prefix, cp_size=cp_size, planner_config=planner_config - ): - return False - if any( + ) or any( _can_chain_segment(completion, cp_size=cp_size, planner_config=planner_config) for completion in family.completions - ): - return True - return family.prefix.length >= planner_config.cp_chain_min_prefix_only_tokens + ) def _can_chain_prefix_segment( @@ -2499,76 +3262,59 @@ def _can_chain_prefix_segment( cp_size: int, planner_config: GdnPlannerConfig, ) -> bool: - if segment.length < cp_size: - return False - per_rank = segment.length / cp_size - if per_rank < planner_config.cp_chain_min_tokens_per_rank: - return False - return segment.length >= planner_config.cp_chain_min_prefix_only_tokens + return _can_chain_segment(segment, cp_size=cp_size, planner_config=planner_config) -def _candidate_chain_family_sets( - spec: GdnPackedExecutionSpec, +def _score_cp_segment_stats( *, - legal_chain_families: tuple[int, ...], - cp_size: int, -) -> tuple[frozenset[int], ...]: - if not legal_chain_families: - return (frozenset(),) - candidates: set[frozenset[int]] = {frozenset(), frozenset(legal_chain_families)} - if len(legal_chain_families) <= 4: - for mask in range(1, 1 << len(legal_chain_families)): - candidates.add( - frozenset( - family_index - for bit, family_index in enumerate(legal_chain_families) - if mask & (1 << bit) - ) - ) - else: - by_chain_value = sorted( - legal_chain_families, - key=lambda family_index: ( - _family_chain_candidate_tokens(spec.families[family_index]), - spec.families[family_index].prefix.length, - ), - reverse=True, + rank_local_work: tuple[int, ...], + rank_chain_work: tuple[int, ...], + rank_real_tokens: tuple[int, ...], + cross_rank_token_count: int, + parent_state_exchange_family_count: int, + local_bucket_count: int, + local_segment_count: int, + chain_bucket_count: int, + planner_config: GdnPlannerConfig, +) -> float: + empty_rank_count = sum(1 for token_count in rank_real_tokens if token_count == 0) + return ( + _rank_kernel_ms( + rank_local_work, + rank_chain_work, + local_token_ms=planner_config.planner_local_token_ms, + chain_token_ms=planner_config.planner_chain_token_ms, ) - for count in range(1, min(len(by_chain_value), cp_size * 2) + 1): - candidates.add(frozenset(by_chain_value[:count])) - for family_index in by_chain_value[: max(cp_size * 2, 1)]: - candidates.add(frozenset((family_index,))) - return tuple(sorted(candidates, key=lambda item: (len(item), tuple(sorted(item))))) - - -def _family_chain_candidate_tokens(family: GdnPackedFamilySpec) -> int: - return family.prefix.length + sum( - completion.length for completion in family.completions + + planner_config.planner_local_bucket_ms * local_bucket_count + + planner_config.planner_chain_bucket_ms * chain_bucket_count + + planner_config.planner_local_segment_ms * local_segment_count + + planner_config.planner_layout_cross_rank_token_ms * cross_rank_token_count + + ( + planner_config.planner_parent_state_exchange_base_ms + + planner_config.planner_parent_state_exchange_ms + * parent_state_exchange_family_count + if parent_state_exchange_family_count + else 0.0 + ) + + planner_config.planner_empty_rank_ms * empty_rank_count ) -def _score_cp_segment_schedule( - schedule: GdnCpSegmentSchedule, +def _rank_kernel_ms( + rank_local_work: tuple[int, ...], + rank_chain_work: tuple[int, ...], *, - planner_config: GdnPlannerConfig, + local_token_ms: float, + chain_token_ms: float, ) -> float: - rank_loads = list(schedule.gdn_token_counts_by_rank) - max_load = max(rank_loads, default=0) - idle_tokens = sum(max_load - load for load in rank_loads) - empty_rank_count = sum(1 for load in rank_loads if load == 0) - local_launches = sum( - 1 for segments in schedule.local_prefix_segments_by_rank if segments - ) + sum(1 for segments in schedule.local_completion_segments_by_rank if segments) - return ( - max_load - + planner_config.rank_idle_token_cost * idle_tokens - + planner_config.empty_rank_penalty_tokens * empty_rank_count - + planner_config.local_fork_launch_penalty_tokens * local_launches - + planner_config.layout_cross_rank_token_cost * schedule.cross_rank_token_count - + planner_config.parent_state_exchange_penalty_tokens - * len(schedule.parent_state_exchange_family_indices) - + planner_config.cp_collective_latency_tokens - * (len(schedule.chain_prefix_buckets) + len(schedule.chain_completion_buckets)) + return max( + ( + local_work * local_token_ms + chain_work * chain_token_ms + for local_work, chain_work in zip( + rank_local_work, rank_chain_work, strict=True + ) + ), + default=0.0, ) @@ -2579,7 +3325,7 @@ def _best_segment_owner( segment_attention_counts: dict[tuple[int, int, int], tuple[int, ...]], planner_config: GdnPlannerConfig, ) -> int: - del planner_config + segment_length = sum(segment.length for segment in segments) if len(segments) == 1: on_rank_tokens = segment_attention_counts[_segment_key(segments[0])] else: @@ -2590,19 +3336,34 @@ def _best_segment_owner( for rank in range(rank_count): counts_by_rank[rank] += segment_counts[rank] on_rank_tokens = tuple(counts_by_rank) - best_locality = max(on_rank_tokens, default=0) - if best_locality <= 0: - return _least_loaded_rank(rank_loads) - best_rank = 0 - best_load = None + best: tuple[float, int, int, int, int] | None = None for rank, tokens in enumerate(on_rank_tokens): - if tokens != best_locality: - continue - load = rank_loads[rank] - if best_load is None or load < best_load: - best_rank = rank - best_load = load - return best_rank + projected_loads = list(rank_loads) + projected_loads[rank] += segment_length + max_load = max(projected_loads, default=0) + idle_tokens = sum(max_load - load for load in projected_loads) + cross_rank_tokens = segment_length - int(tokens) + empty_rank_count = sum(1 for load in projected_loads if load == 0) + score = ( + max_load * planner_config.planner_local_token_ms + + idle_tokens + * planner_config.rank_idle_token_cost + * planner_config.planner_local_token_ms + + cross_rank_tokens * planner_config.planner_layout_cross_rank_token_ms + + empty_rank_count * planner_config.planner_empty_rank_ms + ) + candidate = ( + score, + max_load, + cross_rank_tokens, + -int(tokens), + rank, + ) + if best is None or candidate < best: + best = candidate + if best is None: + return _least_loaded_rank(rank_loads) + return best[-1] def _build_attention_layout_index_from_token_layout( @@ -2700,16 +3461,31 @@ def _default_attention_layout_ranges( ) -> tuple[tuple[tuple[int, int, int], ...], ...]: ranks: list[list[tuple[int, int, int]]] = [[] for _ in range(cp_size)] loads = [0] * cp_size + target_rank_load = spec.real_token_count / cp_size def append_segment(rank: int, token_start: int, token_count: int) -> None: ranks[rank].append((token_start, token_start + token_count, loads[rank])) loads[rank] += token_count + def should_split_segment(segment: GdnSegmentSpec) -> bool: + if segment.length <= planner_config.max_zero_exchange_load_imbalance * ( + target_rank_load + ): + return False + if segment.kind == "prefix": + return _can_chain_prefix_segment( + segment, cp_size=cp_size, planner_config=planner_config + ) + return _can_chain_segment( + segment, cp_size=cp_size, planner_config=planner_config + ) + for family in spec.families: - chain_family = _can_chain_family( - family, cp_size=cp_size, planner_config=planner_config + has_split_segment = any( + should_split_segment(segment) + for segment in (family.prefix, *family.completions) ) - if not chain_family: + if not has_split_segment: if _should_co_locate_non_chain_family( family, total_real_tokens=spec.real_token_count, @@ -2728,14 +3504,7 @@ def append_segment(rank: int, token_start: int, token_count: int) -> None: continue for segment in (family.prefix, *family.completions): token_start = _segment_token_start(segment, spec.sequence_length) - if ( - segment.kind == "prefix" - and _can_chain_prefix_segment( - segment, cp_size=cp_size, planner_config=planner_config - ) - ) or _can_chain_segment( - segment, cp_size=cp_size, planner_config=planner_config - ): + if should_split_segment(segment): _append_split_default_attention_segment( ranks, loads, token_start, segment.length ) @@ -2795,10 +3564,7 @@ def _append_chain_segment( rank_loads[rank] += len(shard) return 0 cross_rank_tokens = 0 - shard_lengths = tuple( - (segment.length * (rank + 1)) // cp_size - (segment.length * rank) // cp_size - for rank in range(cp_size) - ) + shard_lengths = _fla_aligned_chain_shard_lengths(segment.length, cp_size=cp_size) start = 0 for rank, shard_length in enumerate(shard_lengths): end = start + shard_length @@ -2833,8 +3599,9 @@ def _chain_rank_token_indices( cp_size: int, ) -> range: token_start = _segment_token_start(segment, spec.sequence_length) - start = (segment.length * cp_rank) // cp_size - end = (segment.length * (cp_rank + 1)) // cp_size + lengths = _fla_aligned_chain_shard_lengths(segment.length, cp_size=cp_size) + start = sum(lengths[:cp_rank]) + end = start + lengths[cp_rank] if start >= end: raise ValueError( "CP chain planning requires non-empty shards; " @@ -2844,6 +3611,23 @@ def _chain_rank_token_indices( return range(token_start + start, token_start + end) +def _fla_aligned_chain_shard_lengths(length: int, *, cp_size: int) -> tuple[int, ...]: + full_chunks = int(length) // FLA_CHUNK_SIZE + if full_chunks < int(cp_size): + raise ValueError( + "CP chain planning requires at least one full FLA chunk per rank; " + f"length={length} cp_size={cp_size}" + ) + base_chunks = full_chunks // int(cp_size) + extra_chunks = full_chunks % int(cp_size) + chunk_counts = tuple( + base_chunks + (1 if rank < extra_chunks else 0) for rank in range(int(cp_size)) + ) + lengths = [count * FLA_CHUNK_SIZE for count in chunk_counts] + lengths[-1] += int(length) - full_chunks * FLA_CHUNK_SIZE + return tuple(lengths) + + def _attention_contiguous_chain_shards( token_start: int, token_count: int, @@ -2872,6 +3656,8 @@ def _attention_contiguous_chain_shards( cursor = end if cursor != segment_end: return None + if any(len(shard) % FLA_CHUNK_SIZE != 0 for shard in shards[:-1]): + return None return tuple(shards) @@ -2925,6 +3711,7 @@ def _build_position_bucket_plans( *, sequence_length: int, device: torch.device | str, + token_ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...] | None = None, ) -> tuple[GdnSegmentBucketPlan, ...]: return tuple( _build_position_bucket_plan( @@ -2932,6 +3719,7 @@ def _build_position_bucket_plans( local_token_ranges, sequence_length=sequence_length, device=device, + token_ranges_by_rank=token_ranges_by_rank, ) for bucket in segment_buckets ) @@ -2943,12 +3731,14 @@ def _build_position_bucket_plan( *, sequence_length: int, device: torch.device | str, + token_ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...] | None = None, ) -> GdnSegmentBucketPlan: exact_plan = _build_exact_range_position_bucket_plan( segments, local_token_ranges, sequence_length=sequence_length, device=device, + token_ranges_by_rank=token_ranges_by_rank, ) if exact_plan is not None: return exact_plan @@ -2980,6 +3770,11 @@ def _build_position_bucket_plan( cu_seqlens_cpu = torch.cat( [lengths_cpu.new_zeros(1), torch.cumsum(lengths_cpu, dim=0)] ) + lengths_by_rank_cpu = _bucket_lengths_by_rank_cpu( + segments, + token_ranges_by_rank, + sequence_length=sequence_length, + ) row_indices_cpu = torch.zeros(max_length, len(segments), dtype=torch.long) family_indices_cpu = torch.tensor( [segment.family_index for segment in segments], @@ -2988,8 +3783,11 @@ def _build_position_bucket_plan( return GdnSegmentBucketPlan.model_construct( length=max_length, lengths=_move_planner_tensor(lengths_cpu, device), + lengths_cpu=lengths_cpu, + lengths_by_rank_cpu=lengths_by_rank_cpu, real_mask=_move_planner_tensor(real_mask_cpu, device), cu_seqlens=_move_planner_tensor(cu_seqlens_cpu, device), + cu_seqlens_cpu=cu_seqlens_cpu, row_indices=_move_planner_tensor(row_indices_cpu, device), position_indices=_move_planner_tensor(position_indices_cpu, device), family_indices=_move_planner_tensor(family_indices_cpu, device), @@ -3003,6 +3801,7 @@ def _build_exact_range_position_bucket_plan( *, sequence_length: int, device: torch.device | str, + token_ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...] | None = None, ) -> GdnSegmentBucketPlan | None: range_positions = { (start, end): position for start, end, position in local_token_ranges @@ -3030,6 +3829,11 @@ def _build_exact_range_position_bucket_plan( cu_seqlens_cpu = torch.cat( [lengths_cpu.new_zeros(1), torch.cumsum(lengths_cpu, dim=0)] ) + lengths_by_rank_cpu = _bucket_lengths_by_rank_cpu( + segments, + token_ranges_by_rank, + sequence_length=sequence_length, + ) row_indices_cpu = torch.zeros(max_length, len(segments), dtype=torch.long) family_indices_cpu = torch.tensor( [segment.family_index for segment in segments], @@ -3038,8 +3842,11 @@ def _build_exact_range_position_bucket_plan( return GdnSegmentBucketPlan.model_construct( length=max_length, lengths=_move_planner_tensor(lengths_cpu, device), + lengths_cpu=lengths_cpu, + lengths_by_rank_cpu=lengths_by_rank_cpu, real_mask=_move_planner_tensor(real_mask_cpu, device), cu_seqlens=_move_planner_tensor(cu_seqlens_cpu, device), + cu_seqlens_cpu=cu_seqlens_cpu, row_indices=_move_planner_tensor(row_indices_cpu, device), position_indices=_move_planner_tensor(position_indices_cpu, device), family_indices=_move_planner_tensor(family_indices_cpu, device), @@ -3047,6 +3854,33 @@ def _build_exact_range_position_bucket_plan( ) +def _bucket_lengths_by_rank_cpu( + segments: tuple[GdnSegmentSpec, ...], + token_ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...] | None, + *, + sequence_length: int, +) -> torch.Tensor | None: + if token_ranges_by_rank is None: + return None + lengths_by_rank = [] + for rank_ranges_with_positions in token_ranges_by_rank: + rank_ranges = tuple( + (start, end) for start, end, _position in rank_ranges_with_positions + ) + rank_lengths = [] + for segment in segments: + start = _segment_token_start(segment, sequence_length) + end = start + segment.length + rank_lengths.append( + sum( + max(0, min(end, range_end) - max(start, range_start)) + for range_start, range_end in rank_ranges + ) + ) + lengths_by_rank.append(rank_lengths) + return torch.tensor(lengths_by_rank, dtype=torch.long) + + def _move_planner_tensor( tensor: torch.Tensor, device: torch.device | str ) -> torch.Tensor: @@ -3097,30 +3931,36 @@ def _build_segment_bucket_plan( length: int, segments: tuple[GdnSegmentSpec, ...], *, device: torch.device | str ) -> GdnSegmentBucketPlan: max_length = max(segment.length for segment in segments) - lengths = torch.tensor( - [segment.length for segment in segments], device=device, dtype=torch.long + lengths_cpu = torch.tensor( + [segment.length for segment in segments], dtype=torch.long ) - starts = torch.tensor( - [segment.start for segment in segments], device=device, dtype=torch.long + starts_cpu = torch.tensor([segment.start for segment in segments], dtype=torch.long) + rows_cpu = torch.tensor( + [segment.row_index for segment in segments], dtype=torch.long + ) + offsets_cpu = torch.arange(max_length, dtype=torch.long).unsqueeze(1) + real_mask_cpu = offsets_cpu < lengths_cpu.unsqueeze(0) + positions_cpu = starts_cpu.unsqueeze(0) + offsets_cpu + family_indices_cpu = torch.tensor( + [segment.family_index for segment in segments], + dtype=torch.long, ) - rows = torch.tensor( - [segment.row_index for segment in segments], device=device, dtype=torch.long + cu_seqlens_cpu = torch.cat( + [lengths_cpu.new_zeros(1), torch.cumsum(lengths_cpu, dim=0)] ) - offsets = torch.arange(max_length, device=device, dtype=torch.long).unsqueeze(1) - real_mask = offsets < lengths.unsqueeze(0) - positions = starts.unsqueeze(0) + offsets return GdnSegmentBucketPlan.model_construct( length=max_length, - lengths=lengths, - real_mask=real_mask, - cu_seqlens=torch.cat([lengths.new_zeros(1), torch.cumsum(lengths, dim=0)]), - row_indices=rows.unsqueeze(0).expand(max_length, -1).contiguous(), - position_indices=positions, - family_indices=torch.tensor( - [segment.family_index for segment in segments], - device=device, - dtype=torch.long, + lengths=_move_planner_tensor(lengths_cpu, device), + lengths_cpu=lengths_cpu, + lengths_by_rank_cpu=None, + real_mask=_move_planner_tensor(real_mask_cpu, device), + cu_seqlens=_move_planner_tensor(cu_seqlens_cpu, device), + cu_seqlens_cpu=cu_seqlens_cpu, + row_indices=_move_planner_tensor( + rows_cpu.unsqueeze(0).expand(max_length, -1).contiguous(), device ), + position_indices=_move_planner_tensor(positions_cpu, device), + family_indices=_move_planner_tensor(family_indices_cpu, device), real_token_count_static=sum(segment.length for segment in segments), ) diff --git a/src/art/megatron/gdn/layout.py b/src/art/megatron/gdn/layout.py index 0af2961c5..c3469a451 100644 --- a/src/art/megatron/gdn/layout.py +++ b/src/art/megatron/gdn/layout.py @@ -73,24 +73,17 @@ def cross_rank_token_count(self) -> int: ) -def _layout_cp_size(layout: TokenLayoutIndex) -> int: - return len(layout.token_counts_by_rank) +class GdnSpExchangePlan(BaseModel): + """Sequence-parallel view of an existing CP exchange plan.""" + model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) -def _token_layout_from_rank_ranges( - ranges_by_rank: Sequence[Sequence[tuple[int, int, int]]], -) -> TokenLayoutIndex: - ranges = _normalize_rank_ranges( - "ranges_by_rank", - ranges_by_rank, - cp_size=len(ranges_by_rank), - ) - return TokenLayoutIndex( - ownership_ranges_by_rank=ranges, - token_counts_by_rank=tuple( - _rank_range_count(rank_ranges) for rank_ranges in ranges - ), - ) + plan: GdnCpExchangePlan + rank: int + + +def _layout_cp_size(layout: TokenLayoutIndex) -> int: + return len(layout.token_counts_by_rank) def _normalize_rank_ranges( @@ -122,10 +115,6 @@ def _normalize_rank_ranges( return tuple(normalized) -def _rank_range_count(ranges: Sequence[tuple[int, int, int]]) -> int: - return sum(int(end) - int(start) for start, end, _ in ranges) - - def _intersection_position_tensors( source_ranges: Sequence[tuple[int, int, int]], dest_ranges: Sequence[tuple[int, int, int]], @@ -174,92 +163,6 @@ def _intersection_position_tensors( ) -def _merged_token_ranges( - ranges_by_rank: Sequence[Sequence[tuple[int, int, int]]], -) -> tuple[tuple[int, int], ...]: - ranges = sorted( - (int(start), int(end)) - for rank_ranges in ranges_by_rank - for start, end, _ in rank_ranges - if int(start) < int(end) - ) - if not ranges: - return () - merged = [ranges[0]] - for start, end in ranges[1:]: - prev_start, prev_end = merged[-1] - if start <= prev_end: - merged[-1] = (prev_start, max(prev_end, end)) - else: - merged.append((start, end)) - return tuple(merged) - - -def _range_list_count(ranges: Sequence[tuple[int, int]]) -> int: - return sum(int(end) - int(start) for start, end in ranges) - - -def build_cp_exchange_plan_from_layout_index( - *, - source_layout: TokenLayoutIndex, - dest_layout: TokenLayoutIndex, - device: torch.device | str | None, - validate: bool = True, - local_rank: int | None = None, -) -> GdnCpExchangePlan: - cp_size = _layout_cp_size(source_layout) - if _layout_cp_size(dest_layout) != cp_size: - raise ValueError( - "source and destination cp_size differ: " - f"{cp_size} and {_layout_cp_size(dest_layout)}" - ) - if local_rank is not None and (local_rank < 0 or local_rank >= cp_size): - raise ValueError(f"local_rank must be in [0, {cp_size}), got {local_rank}") - if validate: - _validate_layout_token_sets_match(source_layout, dest_layout) - source_counts = source_layout.token_counts_by_rank - dest_counts = dest_layout.token_counts_by_rank - transfers: list[GdnCpPeerTransfer] = [] - cross_rank_token_count = 0 - for source_rank, source_ranges in enumerate(source_layout.ownership_ranges_by_rank): - for dest_rank, dest_ranges in enumerate(dest_layout.ownership_ranges_by_rank): - source_positions, dest_positions = _intersection_position_tensors( - source_ranges, - dest_ranges, - ) - token_count = int(source_positions.numel()) - if token_count == 0: - continue - if source_rank != dest_rank: - cross_rank_token_count += token_count - if ( - local_rank is not None - and source_rank != local_rank - and dest_rank != local_rank - ): - continue - transfers.append( - _make_peer_transfer( - source_rank=source_rank, - dest_rank=dest_rank, - source_positions=source_positions, - dest_positions=dest_positions, - source_count=source_counts[source_rank], - dest_count=dest_counts[dest_rank], - device=device, - ) - ) - return GdnCpExchangePlan.model_construct( - cp_size=cp_size, - source_token_counts_by_rank=source_counts, - dest_token_counts_by_rank=dest_counts, - transfers=tuple( - sorted(transfers, key=lambda item: (item.source_rank, item.dest_rank)) - ), - cross_rank_token_count_override=cross_rank_token_count, - ) - - def build_local_rank_cp_exchange_plan_from_dest_ranges( *, source_layout: TokenLayoutIndex, @@ -314,22 +217,6 @@ def build_local_rank_cp_exchange_plan_from_dest_ranges( ) -def _validate_layout_token_sets_match( - source_layout: TokenLayoutIndex, - dest_layout: TokenLayoutIndex, -) -> None: - source_ranges = _merged_token_ranges(source_layout.ownership_ranges_by_rank) - dest_ranges = _merged_token_ranges(dest_layout.ownership_ranges_by_rank) - if ( - source_ranges != dest_ranges - or sum(source_layout.token_counts_by_rank) != _range_list_count(source_ranges) - or sum(dest_layout.token_counts_by_rank) != _range_list_count(dest_ranges) - ): - raise ValueError( - "source and destination token layouts must cover the same tokens" - ) - - def _make_peer_transfer( *, source_rank: int, @@ -410,6 +297,187 @@ def _reverse_exchange_plan(plan: GdnCpExchangePlan) -> GdnCpExchangePlan: ) +def _infer_tp_cp_rank_mode( + *, + cp_rank: int, + tp_rank: int, + tp_size: int, + cp_size: int, + tp_cp_rank: int, +) -> str: + cp_major = cp_rank * tp_size + tp_rank + tp_major = tp_rank * cp_size + cp_rank + if tp_cp_rank == cp_major: + return "cp_major" + if tp_cp_rank == tp_major: + return "tp_major" + raise ValueError( + "unsupported TPxCP process-group rank order for GDN SP exchange: " + f"cp_rank={cp_rank}, tp_rank={tp_rank}, tp_size={tp_size}, " + f"cp_size={cp_size}, tp_cp_rank={tp_cp_rank}" + ) + + +def _composite_tp_cp_rank( + cp_rank: int, + tp_rank: int, + *, + tp_size: int, + cp_size: int, + mode: str, +) -> int: + if mode == "cp_major": + return int(cp_rank) * int(tp_size) + int(tp_rank) + if mode == "tp_major": + return int(tp_rank) * int(cp_size) + int(cp_rank) + raise ValueError(f"unsupported TPxCP rank mode {mode!r}") + + +def _ceil_div(value: int, divisor: int) -> int: + return (int(value) + int(divisor) - 1) // int(divisor) + + +def _sp_counts_by_composite_rank( + cp_counts: Sequence[int], + *, + tp_size: int, + mode: str, +) -> tuple[int, ...]: + cp_size = len(cp_counts) + counts = [0] * (cp_size * tp_size) + for cp_rank, count in enumerate(cp_counts): + rows_per_tp = _ceil_div(int(count), tp_size) + for tp_rank in range(tp_size): + counts[ + _composite_tp_cp_rank( + cp_rank, + tp_rank, + tp_size=tp_size, + cp_size=cp_size, + mode=mode, + ) + ] = rows_per_tp + return tuple(counts) + + +def _sp_shard_bounds(count: int, *, tp_rank: int, tp_size: int) -> tuple[int, int, int]: + rows_per_tp = _ceil_div(count, tp_size) + start = int(tp_rank) * rows_per_tp + end = min(start + rows_per_tp, int(count)) + return start, end, rows_per_tp + + +def _shard_implicit_identity_transfer_for_sequence_parallel( + transfer: GdnCpPeerTransfer, + plan: GdnCpExchangePlan, + *, + tp_size: int, + tp_rank: int, + local_rank: int, + rank_mode: str, + source_counts: tuple[int, ...], + dest_counts: tuple[int, ...], + device: torch.device | str | None, +) -> tuple[GdnCpPeerTransfer, ...]: + if transfer.source_rank != transfer.dest_rank: + return () + source_rank = _composite_tp_cp_rank( + transfer.source_rank, + tp_rank, + tp_size=tp_size, + cp_size=plan.cp_size, + mode=rank_mode, + ) + if source_rank != local_rank: + return () + start, end, _ = _sp_shard_bounds( + _source_count_for_rank(plan, transfer.source_rank), + tp_rank=tp_rank, + tp_size=tp_size, + ) + rows = end - start + if rows <= 0: + return () + positions = torch.arange(rows, dtype=torch.long, device=device) + return ( + _make_peer_transfer( + source_rank=source_rank, + dest_rank=source_rank, + source_positions=positions, + dest_positions=positions, + source_count=source_counts[source_rank], + dest_count=dest_counts[source_rank], + device=device, + ), + ) + + +def _shard_indexed_transfer_for_sequence_parallel( + transfer: GdnCpPeerTransfer, + plan: GdnCpExchangePlan, + *, + tp_size: int, + local_rank: int, + rank_mode: str, + source_counts: tuple[int, ...], + dest_counts: tuple[int, ...], + device: torch.device | str | None, +) -> tuple[GdnCpPeerTransfer, ...]: + source_positions = transfer.source_positions_tensor + dest_positions = transfer.dest_positions_tensor + if source_positions is None or dest_positions is None: + raise ValueError("indexed SP exchange requires explicit CP transfer positions") + source_rows_per_tp = _ceil_div( + _source_count_for_rank(plan, transfer.source_rank), tp_size + ) + dest_rows_per_tp = _ceil_div( + _dest_count_for_rank(plan, transfer.dest_rank), tp_size + ) + if source_rows_per_tp <= 0 or dest_rows_per_tp <= 0: + return () + source_tp = torch.div(source_positions, source_rows_per_tp, rounding_mode="floor") + dest_tp = torch.div(dest_positions, dest_rows_per_tp, rounding_mode="floor") + source_rank = ( + transfer.source_rank * tp_size + source_tp + if rank_mode == "cp_major" + else source_tp * plan.cp_size + transfer.source_rank + ) + dest_rank = ( + transfer.dest_rank * tp_size + dest_tp + if rank_mode == "cp_major" + else dest_tp * plan.cp_size + transfer.dest_rank + ) + keep = (source_rank == local_rank) | (dest_rank == local_rank) + if not bool(torch.any(keep).item()): + return () + source_rank = source_rank[keep] + dest_rank = dest_rank[keep] + source_local_positions = ( + source_positions[keep] - source_tp[keep] * source_rows_per_tp + ) + dest_local_positions = dest_positions[keep] - dest_tp[keep] * dest_rows_per_tp + world_size = plan.cp_size * tp_size + keys = source_rank * world_size + dest_rank + transfers = [] + for key in torch.unique(keys, sorted=True).detach().cpu().tolist(): + key = int(key) + peer_source_rank = key // world_size + peer_dest_rank = key % world_size + peer_mask = keys == key + transfers.append( + _make_peer_transfer( + source_rank=peer_source_rank, + dest_rank=peer_dest_rank, + source_positions=source_local_positions[peer_mask], + dest_positions=dest_local_positions[peer_mask], + source_count=source_counts[peer_source_rank], + dest_count=dest_counts[peer_dest_rank], + device=device, + ) + ) + return tuple(transfers) + + def move_cp_exchange_plan_to_device( plan: GdnCpExchangePlan | None, device: torch.device | str, @@ -426,10 +494,10 @@ def move_cp_exchange_plan_to_device( source_rank=transfer.source_rank, dest_rank=transfer.dest_rank, token_count=transfer.token_count, - source_positions_tensor=_move_index_tensor_if_present( + source_positions_tensor=_move_optional_index_tensor( transfer.source_positions_tensor, target ), - dest_positions_tensor=_move_index_tensor_if_present( + dest_positions_tensor=_move_optional_index_tensor( transfer.dest_positions_tensor, target ), ) @@ -439,7 +507,7 @@ def move_cp_exchange_plan_to_device( ) -def _move_index_tensor_if_present( +def _move_optional_index_tensor( tensor: Tensor | None, device: torch.device ) -> Tensor | None: if tensor is None or tensor.device == device: @@ -447,12 +515,104 @@ def _move_index_tensor_if_present( return tensor.to(device=device) -def send_split_sizes_for_rank(plan: GdnCpExchangePlan, rank: int) -> tuple[int, ...]: - _check_rank(plan, rank) - return tuple( - _transfer_token_count(_transfer(plan, source_rank=rank, dest_rank=dest_rank)) - for dest_rank in range(plan.cp_size) +def shard_cp_exchange_plan_for_sequence_parallel( + plan: GdnCpExchangePlan, + *, + cp_rank: int, + tp_rank: int, + tp_size: int, + tp_cp_rank: int, + device: torch.device | str | None, +) -> GdnSpExchangePlan: + """Split one CP exchange plan into the local TPxCP sequence-parallel view. + + The GDN planner stays CP-only. This adapter preserves the planner's existing + source/destination position tensors and only remaps them into local SP shards + for the actual boundary all-to-all. + """ + + if tp_size <= 1: + return GdnSpExchangePlan.model_construct(plan=plan, rank=cp_rank) + _check_rank(plan, cp_rank) + if tp_rank < 0 or tp_rank >= tp_size: + raise ValueError(f"tp_rank must be in [0, {tp_size}), got {tp_rank}") + world_size = plan.cp_size * tp_size + rank_mode = _infer_tp_cp_rank_mode( + cp_rank=cp_rank, + tp_rank=tp_rank, + tp_size=tp_size, + cp_size=plan.cp_size, + tp_cp_rank=tp_cp_rank, + ) + composite_rank = _composite_tp_cp_rank( + cp_rank, tp_rank, tp_size=tp_size, cp_size=plan.cp_size, mode=rank_mode ) + if composite_rank != tp_cp_rank: + raise ValueError( + "TPxCP rank mapping mismatch: inferred " + f"{composite_rank}, process group reports {tp_cp_rank}" + ) + + source_counts = _sp_counts_by_composite_rank( + plan.source_token_counts_by_rank, + tp_size=tp_size, + mode=rank_mode, + ) + dest_counts = _sp_counts_by_composite_rank( + plan.dest_token_counts_by_rank, + tp_size=tp_size, + mode=rank_mode, + ) + transfers: list[GdnCpPeerTransfer] = [] + for transfer in plan.transfers: + if not _transfer_token_count(transfer): + continue + if _is_implicit_full_identity_transfer( + transfer, + source_count=_source_count_for_rank(plan, transfer.source_rank), + dest_count=_dest_count_for_rank(plan, transfer.dest_rank), + ): + transfers.extend( + _shard_implicit_identity_transfer_for_sequence_parallel( + transfer, + plan, + tp_size=tp_size, + tp_rank=tp_rank, + local_rank=composite_rank, + rank_mode=rank_mode, + source_counts=source_counts, + dest_counts=dest_counts, + device=device, + ) + ) + continue + transfers.extend( + _shard_indexed_transfer_for_sequence_parallel( + transfer, + plan, + tp_size=tp_size, + local_rank=composite_rank, + rank_mode=rank_mode, + source_counts=source_counts, + dest_counts=dest_counts, + device=device, + ) + ) + + # Force all sequence-parallel layout conversions through the same collective. + # A CP-local reorder can still move rows between TP ranks, and local CP plans do + # not contain enough global TP information for every rank to independently + # prove that no peer exchange is needed. + sp_plan = GdnCpExchangePlan.model_construct( + cp_size=world_size, + source_token_counts_by_rank=source_counts, + dest_token_counts_by_rank=dest_counts, + transfers=tuple( + sorted(transfers, key=lambda item: (item.source_rank, item.dest_rank)) + ), + cross_rank_token_count_override=1, + ) + return GdnSpExchangePlan.model_construct(plan=sp_plan, rank=composite_rank) def recv_split_sizes_for_rank(plan: GdnCpExchangePlan, rank: int) -> tuple[int, ...]: @@ -686,9 +846,17 @@ def _exchange_rank_tensor_all_to_all_forward( ) -> Tensor: if plan.cross_rank_token_count == 0: return _exchange_rank_tensor_local(local_tensor, plan, rank=rank) - accumulate = _rank_recv_requires_accumulation(plan, rank) + write_positions = _rank_recv_write_positions(plan, rank) + accumulate = len(write_positions) != len(set(write_positions)) + zero_init = accumulate or len(set(write_positions)) != _dest_count_for_rank( + plan, rank + ) output = _init_rank_exchange_output( - local_tensor, plan, rank=rank, accumulate=accumulate + local_tensor, + plan, + rank=rank, + accumulate=accumulate, + zero_init=zero_init, ) send_buffer = _pack_rank_cross_send_tensor(local_tensor, plan, source_rank=rank) send_buffer = send_buffer.contiguous() @@ -727,18 +895,30 @@ def _exchange_rank_tensor_local( ) +def _copy_rank_self_transfers( + local_tensor: Tensor, + plan: GdnCpExchangePlan, + *, + rank: int, +) -> Tensor: + return _init_rank_exchange_output( + local_tensor, plan, rank=rank, accumulate=False, zero_init=False + ) + + def _init_rank_exchange_output( local_tensor: Tensor, plan: GdnCpExchangePlan, *, rank: int, accumulate: bool, + zero_init: bool, ) -> Tensor: dest_rows = _dest_count_for_rank(plan, rank) output_shape = (dest_rows, *local_tensor.shape[1:]) output = ( local_tensor.new_zeros(output_shape) - if accumulate + if zero_init else local_tensor.new_empty(output_shape) ) transfer = _transfer(plan, source_rank=rank, dest_rank=rank) @@ -826,14 +1006,14 @@ def _unpack_rank_cross_recv_tensor_into( output.index_copy_(0, dest_index, peer_rows) -def _rank_recv_requires_accumulation(plan: GdnCpExchangePlan, rank: int) -> bool: +def _rank_recv_write_positions(plan: GdnCpExchangePlan, rank: int) -> list[int]: positions: list[int] = [] for source_rank in range(plan.cp_size): transfer = _transfer(plan, source_rank=source_rank, dest_rank=rank) if not _transfer_token_count(transfer): continue positions.extend(_transfer_dest_positions_for_duplicate_check(plan, transfer)) - return len(positions) != len(set(positions)) + return positions def _transfer_dest_positions_for_duplicate_check( diff --git a/src/art/megatron/gdn/operator.py b/src/art/megatron/gdn/operator.py index 034065cdb..e8a122f5c 100644 --- a/src/art/megatron/gdn/operator.py +++ b/src/art/megatron/gdn/operator.py @@ -1,22 +1,15 @@ from __future__ import annotations -from contextlib import contextmanager -from contextvars import ContextVar -import importlib from types import MethodType -from typing import Any, Callable, Iterator, Literal, Sequence, cast - -from causal_conv1d import causal_conv1d_fn -from fla.modules.l2norm import l2norm -from fla.ops.gated_delta_rule import chunk_gated_delta_rule -from megatron.core.ssm.gated_delta_net import GatedDeltaNet -from megatron.core.transformer.transformer_layer import TransformerLayer -from pydantic import BaseModel, ConfigDict +from typing import Any, Callable, Literal, NamedTuple, Sequence, cast + import torch from torch import Tensor +import torch.distributed as dist import torch.nn.functional as F -from .conv_gelu import gdn_varlen_causal_conv_gelu, packed_varlen_causal_conv +from .conv_gelu import packed_varlen_causal_conv +from .fla_cp import chunk_gated_delta_rule_native_cp from .gdn_shared_prefix import ( GdnPackedExecutionSpec, GdnParentStateTransferPlan, @@ -35,27 +28,33 @@ scatter_bucket_output_compact as _scatter_bucket_output_fused, ) -_NVTX_ENABLED: ContextVar[bool] = ContextVar("art_gdn_nvtx_enabled", default=False) +_GDN_ATTENTION_ORIGINAL_SHAPE_ATTR = "_art_gdn_attention_original_shape" +_GDN_TRACE_TOKEN_UID_HOOKS: Any | None = None + +class _GdnIslandBoundary(NamedTuple): + is_gdn: bool + island_id: int | None + input_layout: Literal["attention", "gdn"] + output_layout: Literal["attention", "gdn"] -class _BucketFlatLayout(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) - padded_indices: Tensor - padded_mask: Tensor - real_indices: Tensor - output_indices: Tensor - output_selector: Tensor | None +def set_gdn_trace_token_uid_hooks(hooks: Any | None) -> Any | None: + global _GDN_TRACE_TOKEN_UID_HOOKS + previous = _GDN_TRACE_TOKEN_UID_HOOKS + _GDN_TRACE_TOKEN_UID_HOOKS = hooks + return previous def install_shared_prefix_gdn_hooks(model_chunks: Sequence[Any]) -> None: """Patch Megatron GatedDeltaNet modules to honor ART shared-prefix packing.""" + gated_delta_net_type = _optional_gated_delta_net_type() + if gated_delta_net_type is None: + return for chunk in model_chunks: - if not hasattr(chunk, "modules"): - continue for module in chunk.modules(): - if not isinstance(module, GatedDeltaNet): + if not isinstance(module, gated_delta_net_type): continue if getattr(module, "_art_shared_prefix_gdn_hooked", False): continue @@ -68,26 +67,27 @@ def install_shared_prefix_gdn_hooks(model_chunks: Sequence[Any]) -> None: def install_gdn_island_hooks(model_chunks: Sequence[Any]) -> None: """Hoist CP layout conversion across consecutive Transformer GDN layers.""" + gated_delta_net_type = _optional_gated_delta_net_type() + transformer_layer_type = _optional_transformer_layer_type() + if gated_delta_net_type is None or transformer_layer_type is None: + return + + next_island_id = 0 for chunk in model_chunks: - if not hasattr(chunk, "modules"): - continue _install_empty_safe_norm_hooks(chunk) layers = [ module for module in chunk.modules() - if isinstance(module, TransformerLayer) + if isinstance(module, transformer_layer_type) and hasattr(module, "self_attention") ] - layer_is_gdn = [ - isinstance(layer.self_attention, GatedDeltaNet) for layer in layers - ] - for index, layer in enumerate(layers): - is_gdn = layer_is_gdn[index] - layer._art_gdn_island_is_gdn = is_gdn - layer._art_gdn_island_prev_is_gdn = index > 0 and layer_is_gdn[index - 1] - layer._art_gdn_island_next_is_gdn = ( - index + 1 < len(layers) and layer_is_gdn[index + 1] - ) + boundaries, next_island_id = _build_gdn_island_boundaries( + layers, + gated_delta_net_type, + next_island_id=next_island_id, + ) + for layer, boundary in zip(layers, boundaries, strict=True): + layer._art_gdn_island_boundary = boundary if getattr(layer, "_art_gdn_island_hooked", False): continue layer._art_gdn_island_physical_forward = layer.forward @@ -95,6 +95,54 @@ def install_gdn_island_hooks(model_chunks: Sequence[Any]) -> None: layer._art_gdn_island_hooked = True +def _build_gdn_island_boundaries( + layers: Sequence[Any], + gated_delta_net_type: type[Any], + *, + next_island_id: int, +) -> tuple[list[_GdnIslandBoundary], int]: + layer_is_gdn = [ + isinstance(layer.self_attention, gated_delta_net_type) for layer in layers + ] + boundaries: list[_GdnIslandBoundary] = [] + active_island_id: int | None = None + for index, is_gdn in enumerate(layer_is_gdn): + prev_is_gdn = index > 0 and layer_is_gdn[index - 1] + next_is_gdn = index + 1 < len(layer_is_gdn) and layer_is_gdn[index + 1] + if is_gdn: + if not prev_is_gdn: + active_island_id = next_island_id + next_island_id += 1 + boundaries.append( + _GdnIslandBoundary( + True, + active_island_id, + "gdn" if prev_is_gdn else "attention", + "gdn" if next_is_gdn else "attention", + ) + ) + else: + active_island_id = None + boundaries.append(_GdnIslandBoundary(False, None, "attention", "attention")) + return boundaries, next_island_id + + +def _optional_gated_delta_net_type() -> type[Any] | None: + try: + from megatron.core.ssm.gated_delta_net import GatedDeltaNet + except ImportError: + return None + return GatedDeltaNet + + +def _optional_transformer_layer_type() -> type[Any] | None: + try: + from megatron.core.transformer.transformer_layer import TransformerLayer + except ImportError: + return None + return TransformerLayer + + def _gdn_island_layer_forward(self: Any, *args: Any, **kwargs: Any) -> Any: attention_bias = kwargs.get("attention_bias") plan = getattr(attention_bias, "gdn_execution_plan", None) @@ -106,33 +154,72 @@ def _gdn_island_layer_forward(self: Any, *args: Any, **kwargs: Any) -> Any: if hidden_states is None: return original_forward(*args, **kwargs) - is_gdn = bool(getattr(self, "_art_gdn_island_is_gdn", False)) - if not is_gdn: - if getattr(attention_bias, "gdn_hidden_layout", "attention") == "gdn": - _mark_attention_layout_active(attention_bias) + boundary = cast(_GdnIslandBoundary, self._art_gdn_island_boundary) + if not boundary.is_gdn: + if getattr(attention_bias, "gdn_hidden_layout", "attention") != "attention": + _mark_attention_layout_active(attention_bias, hidden_states) return original_forward(*args, **kwargs) - prev_is_gdn = bool(getattr(self, "_art_gdn_island_prev_is_gdn", False)) - next_is_gdn = bool(getattr(self, "_art_gdn_island_next_is_gdn", False)) - if prev_is_gdn: - _mark_gdn_layout_active(attention_bias, hidden_states) + if boundary.input_layout == "gdn": + original_shape = _gdn_attention_original_shape_from_tensor( + hidden_states + ) or _gdn_attention_original_shape_from_state( + attention_bias, + gdn=self.self_attention, + island_id=boundary.island_id, + ) + if original_shape is not None: + _store_gdn_attention_original_shape( + attention_bias, + original_shape, + gdn=self.self_attention, + island_id=boundary.island_id, + ) + _mark_gdn_layout_active( + attention_bias, + hidden_states, + gdn=self.self_attention, + island_id=boundary.island_id, + ) else: hidden_states = _enter_gdn_island_layout( - hidden_states, attention_bias, force=True + hidden_states, + attention_bias, + gdn=self.self_attention, + island_id=boundary.island_id, + force=True, ) args, kwargs = _replace_layer_hidden_states(args, kwargs, hidden_states) + previous_input_layout = getattr(attention_bias, "gdn_input_layout", None) + previous_output_layout = getattr(attention_bias, "gdn_output_layout", None) + setattr(attention_bias, "gdn_input_layout", "gdn") + setattr(attention_bias, "gdn_output_layout", "gdn") - output = ( - _empty_gdn_island_layer_forward(self, hidden_states, kwargs) - if int(hidden_states.shape[0]) == 0 - else original_forward(*args, **kwargs) - ) - if next_is_gdn: - _mark_gdn_layout_active(attention_bias, _layer_output_hidden_states(output)) - return output - + try: + output = original_forward(*args, **kwargs) + finally: + setattr(attention_bias, "gdn_input_layout", previous_input_layout) + setattr(attention_bias, "gdn_output_layout", previous_output_layout) + if boundary.output_layout == "gdn": + original_shape = _gdn_attention_original_shape_from_state( + attention_bias, gdn=self.self_attention, island_id=boundary.island_id + ) + hidden_out = _attach_gdn_attention_original_shape( + _layer_output_hidden_states(output), + original_shape, + ) + _mark_gdn_layout_active( + attention_bias, + hidden_out, + gdn=self.self_attention, + island_id=boundary.island_id, + ) + return _replace_layer_output_hidden_states(output, hidden_out) hidden_out = _leave_gdn_island_layout( - _layer_output_hidden_states(output), attention_bias + _layer_output_hidden_states(output), + attention_bias, + gdn=self.self_attention, + island_id=boundary.island_id, ) return _replace_layer_output_hidden_states(output, hidden_out) @@ -214,33 +301,6 @@ def _empty_safe_norm_forward( return original_forward(input_, *args, **kwargs) -def _empty_gdn_island_layer_forward( - layer: Any, hidden_states: Tensor, kwargs: dict[str, Any] -) -> tuple[Tensor, Tensor | None]: - with _nvtx_range("art_gdn_empty_island_layer", hidden_states): - attention_output = layer.self_attention( - hidden_states, - attention_mask=kwargs.get("attention_mask"), - inference_context=kwargs.get( - "inference_context", kwargs.get("inference_params") - ), - rotary_pos_emb=kwargs.get("rotary_pos_emb"), - rotary_pos_cos=kwargs.get("rotary_pos_cos"), - rotary_pos_sin=kwargs.get("rotary_pos_sin"), - rotary_pos_cos_sin=kwargs.get("rotary_pos_cos_sin"), - attention_bias=kwargs.get("attention_bias"), - packed_seq_params=kwargs.get("packed_seq_params"), - sequence_len_offset=kwargs.get("sequence_len_offset"), - ) - context = kwargs.get("context") - if isinstance(attention_output, dict) and "context" in attention_output: - context = attention_output["context"] - attention_hidden = ( - attention_output[0] if isinstance(attention_output, tuple) else attention_output - ) - return hidden_states + cast(Tensor, attention_hidden), context - - def _shared_prefix_forward( self: Any, hidden_states: Tensor, @@ -281,25 +341,39 @@ def _shared_prefix_forward( raise NotImplementedError( "PackedSeqParams is not used in ART shared-prefix GDN." ) - return gdn_shared_prefix_forward( + current_layout = _normalize_cp_layout( + getattr(attention_bias, "gdn_hidden_layout", "attention") + ) + input_layout = _normalize_cp_layout( + getattr(attention_bias, "gdn_input_layout", None) or current_layout + ) + output_layout = _normalize_cp_layout( + getattr(attention_bias, "gdn_output_layout", None) or current_layout + ) + mark_layout = execution_plan is not None and int(execution_plan.cp_size) > 1 + if mark_layout: + _mark_cp_layout_active( + attention_bias, hidden_states, gdn=self, layout=input_layout + ) + output = gdn_shared_prefix_forward( self, hidden_states, group_ids=cast(Tensor, group_ids), parent_ids=cast(Tensor, parent_ids), execution_spec=cast(GdnPackedExecutionSpec | None, execution_spec), execution_plan=cast(GdnRankExecutionPlan | None, execution_plan), - input_layout=( - "gdn" - if getattr(attention_bias, "gdn_hidden_layout", "attention") == "gdn" - else "attention" - ), - output_layout=( - "gdn" - if getattr(attention_bias, "gdn_hidden_layout", "attention") == "gdn" - else "attention" - ), - require_prebuilt_plan=False, + input_layout=input_layout, + output_layout=output_layout, + require_prebuilt_plan=True, ) + if mark_layout: + _mark_cp_layout_active( + attention_bias, + _layer_output_hidden_states(output), + gdn=self, + layout=output_layout, + ) + return output @torch.compiler.disable @@ -355,7 +429,9 @@ def run_gdn_layer( ) seq_len, batch_size, _ = hidden_states.shape requested_cp_size = ( - execution_plan.cp_size if execution_plan is not None else _default_cp_size() + execution_plan.cp_size + if execution_plan is not None + else int(getattr(gdn, "sp_size", 1)) ) cp_rank = ( execution_plan.cp_rank @@ -373,7 +449,8 @@ def run_gdn_layer( raise ValueError( "shared-prefix GDN group_ids shape must match the logical sequence " "processed by Megatron GDN after sequence-parallel input gather, got " - f"hidden={tuple(hidden_states.shape)} group_ids={tuple(group_ids.shape)} " + f"hidden={tuple(hidden_states.shape)} " + f"group_ids={tuple(group_ids.shape)} " f"expected_group_shape={(batch_size, expected_group_seq_len)}" ) @@ -386,10 +463,9 @@ def run_gdn_layer( ) if execution_spec is None and execution_plan is None: - with _nvtx_range("art_gdn_parse_shared_prefix_layout", hidden_states): - execution_spec = parse_gdn_shared_prefix_segments( - group_ids, parent_ids, min_completions_per_family=0 - ) + execution_spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) if ( execution_spec is not None and requested_cp_size == 1 @@ -407,20 +483,20 @@ def run_gdn_layer( if execution_plan is None: if execution_spec is None: raise ValueError("GDN execution spec is required to build a missing plan") - with _nvtx_range("art_gdn_plan_shared_prefix_layout", hidden_states): - execution_plan = build_gdn_rank_execution_plan( - execution_spec, - device=hidden_states.device, - cp_rank=cp_rank, - cp_size=requested_cp_size, - ) + execution_plan = build_gdn_rank_execution_plan( + execution_spec, + device=hidden_states.device, + cp_rank=cp_rank, + cp_size=requested_cp_size, + ) elif execution_plan.cp_size == 1 and ( execution_plan.batch_size != batch_size - or execution_plan.sequence_length != seq_len + or execution_plan.sequence_length != expected_group_seq_len ): raise ValueError( "GDN execution plan shape must match hidden_states, got " f"plan={(execution_plan.batch_size, execution_plan.sequence_length)} " + f"expected={(batch_size, expected_group_seq_len)} " f"hidden={(batch_size, seq_len)}" ) if execution_plan.cp_size != 1: @@ -454,7 +530,7 @@ def _has_chunk_aligned_local_plan(plan: GdnRankExecutionPlan) -> bool: return bool( plan.prefix_boundary_buckets or plan.prefix_tail_buckets - or plan.completion_warmup_buckets + or plan.completion_with_prefix_tail_buckets ) @@ -463,8 +539,7 @@ def _run_chunk_aligned_prefixes_and_completions( hidden_states: Tensor, plan: GdnRankExecutionPlan, ) -> tuple[Tensor, Tensor | None]: - with _nvtx_range("art_gdn_in_proj", hidden_states): - qkv, gate, beta, recurrent_g = _project_gdn_inputs(gdn, hidden_states) + qkv, gate, beta, recurrent_g = _project_gdn_inputs(gdn, hidden_states) gate = gate.clone() recurrent_output = torch.zeros_like(gate) boundary_family_chunks: list[Tensor] = [] @@ -472,24 +547,22 @@ def _run_chunk_aligned_prefixes_and_completions( boundary_rec_chunks: list[Tensor] = [] for bucket in plan.prefix_boundary_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - prefix_qkv, prefix_beta, prefix_g = _gather_compact_bucket_streams( - qkv, beta, recurrent_g, bucket - ) + prefix_qkv, prefix_beta, prefix_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) zero_conv = _zero_conv_state( gdn, hidden_states, batch_size=bucket.segment_count ) zero_rec = _zero_recurrent_state( gdn, hidden_states, batch_size=bucket.segment_count ) - with _nvtx_range("art_gdn_prefix_boundary_segment", prefix_qkv): - prefix_out, prefix_conv, prefix_rec = run_gdn_bucket( - bucket, - (prefix_qkv, prefix_beta, prefix_g), - (zero_conv, zero_rec), - gdn=gdn, - output_final_state=True, - ) + prefix_out, prefix_conv, prefix_rec = run_gdn_bucket( + bucket, + (prefix_qkv, prefix_beta, prefix_g), + (zero_conv, zero_rec), + gdn=gdn, + output_final_state=True, + ) if prefix_conv is None or prefix_rec is None: raise RuntimeError("prefix boundary GDN execution must return final states") recurrent_output = _scatter_bucket_recurrent_output( @@ -518,21 +591,18 @@ def _run_chunk_aligned_prefixes_and_completions( tail_conv_chunks: list[Tensor] = [] tail_rec_chunks: list[Tensor] = [] for bucket in plan.prefix_tail_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - tail_qkv, tail_beta, tail_g = _gather_compact_bucket_streams( - qkv, beta, recurrent_g, bucket - ) - with _nvtx_range("art_gdn_state_fanout", tail_qkv): - tail_conv = boundary_conv_table.index_select(0, bucket.family_indices) - tail_rec = boundary_rec_table.index_select(0, bucket.family_indices) - with _nvtx_range("art_gdn_prefix_tail_segment", tail_qkv): - tail_out, tail_conv, tail_rec = run_gdn_bucket( - bucket, - (tail_qkv, tail_beta, tail_g), - (tail_conv, tail_rec), - gdn=gdn, - output_final_state=True, - ) + tail_qkv, tail_beta, tail_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) + tail_conv = boundary_conv_table.index_select(0, bucket.family_indices) + tail_rec = boundary_rec_table.index_select(0, bucket.family_indices) + tail_out, tail_conv, tail_rec = run_gdn_bucket( + bucket, + (tail_qkv, tail_beta, tail_g), + (tail_conv, tail_rec), + gdn=gdn, + output_final_state=True, + ) if tail_conv is None or tail_rec is None: raise RuntimeError("prefix tail GDN execution must return final states") recurrent_output = _scatter_bucket_recurrent_output( @@ -553,75 +623,25 @@ def _run_chunk_aligned_prefixes_and_completions( state_chunks=tail_rec_chunks, ) - for bucket in plan.completion_warmup_buckets: - with _nvtx_range("art_gdn_state_fanout", hidden_states): - completion_conv = prefix_conv_table.index_select(0, bucket.family_indices) - completion_rec = prefix_rec_table.index_select(0, bucket.family_indices) - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - completion_qkv, completion_beta, completion_g = ( - _gather_compact_bucket_streams(qkv, beta, recurrent_g, bucket) - ) - with _nvtx_range("art_gdn_completion_warmup_segment", completion_qkv): - completion_out, _, _ = run_gdn_bucket( - bucket, - (completion_qkv, completion_beta, completion_g), - (completion_conv, completion_rec), - gdn=gdn, - output_final_state=False, - ) + for bucket in plan.completion_with_prefix_tail_buckets: + completion_conv = prefix_conv_table.index_select(0, bucket.family_indices) + completion_rec = prefix_rec_table.index_select(0, bucket.family_indices) + completion_qkv, completion_beta, completion_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) + completion_out, _, _ = run_gdn_bucket( + bucket, + (completion_qkv, completion_beta, completion_g), + (completion_conv, completion_rec), + gdn=gdn, + output_final_state=False, + ) recurrent_output = _scatter_bucket_recurrent_output( recurrent_output, bucket, completion_out ) - return _project_gdn_output(gdn, recurrent_output, gate, plan) -def _iter_prepared_bucket_columns( - bucket: GdnSegmentBucketPlan, - qkv: Tensor, - beta: Tensor, - recurrent_g: Tensor, - conv_initial: Tensor, - recurrent_initial: Tensor, -) -> Iterator[tuple[GdnSegmentBucketPlan, Tensor, Tensor, Tensor, Tensor, Tensor]]: - for column in range(int(bucket.lengths.numel())): - length = int(bucket.lengths[column].item()) - if length == 0: - continue - column_bucket = _slice_bucket_column(bucket, column=column, length=length) - yield ( - column_bucket, - qkv[column : column + 1, :, :length], - beta[column : column + 1, :length], - recurrent_g[column : column + 1, :length], - conv_initial[column : column + 1], - recurrent_initial[column : column + 1], - ) - - -def _slice_bucket_column( - bucket: GdnSegmentBucketPlan, *, column: int, length: int -) -> GdnSegmentBucketPlan: - lengths = bucket.lengths[column : column + 1] - cu_seqlens = torch.stack((lengths.new_zeros(()), lengths[0])) - output_mask = ( - None - if bucket.output_mask is None - else bucket.output_mask[:length, column : column + 1] - ) - return GdnSegmentBucketPlan.model_construct( - length=length, - lengths=lengths, - real_mask=bucket.real_mask[:length, column : column + 1], - cu_seqlens=cu_seqlens, - row_indices=bucket.row_indices[:length, column : column + 1], - position_indices=bucket.position_indices[:length, column : column + 1], - family_indices=bucket.family_indices[column : column + 1], - real_token_count_static=length, - output_mask=output_mask, - ) - - def _run_cp_planned_prefixes_and_completions( gdn: Any, hidden_states: Tensor, @@ -640,48 +660,65 @@ def _run_cp_planned_prefixes_and_completions( raise ValueError( f"unsupported GDN CP layouts: {input_layout=} {output_layout=}" ) - run_gdn_prepared_varlen_native_fla_cp = importlib.import_module( - "art.megatron.gdn.cp_runtime" - ).run_gdn_prepared_varlen_native_fla_cp - if input_layout == "attention": - gdn_hidden, original_shape = gdn_cp_attention_to_gdn_layout( - hidden_states, plan, group + gdn_hidden, _original_shape = gdn_cp_attention_to_gdn_layout( + hidden_states, + plan, + group, + gdn=gdn, ) else: - gdn_hidden = _validate_gdn_hidden_for_cp_plan(hidden_states, plan) - original_shape = _attention_original_shape_from_plan(hidden_states, plan) - with _nvtx_range("art_gdn_in_proj", gdn_hidden): + gdn_hidden = _validate_gdn_hidden_for_cp_plan(hidden_states, plan, gdn=gdn) + empty_gdn_rank = plan.gdn_token_count == 0 + if empty_gdn_rank: + qkv, gate, beta, recurrent_g = _project_empty_gdn_inputs(gdn, gdn_hidden) + else: qkv, gate, beta, recurrent_g = _project_gdn_inputs(gdn, gdn_hidden) + cp_dependency = ( + _make_zero_autograd_dependency(gdn_hidden) + if empty_gdn_rank + else _empty_autograd_dependency(qkv) + ) + qkv_with_remote_tail = qkv + beta_with_remote_tail = beta + recurrent_g_with_remote_tail = recurrent_g + if plan.remote_prefix_tail_exchange is not None: + remote_qkv, remote_beta, remote_g = _exchange_remote_prefix_tail_streams( + qkv, + beta, + recurrent_g, + plan=plan, + group=group, + ) + qkv_with_remote_tail = torch.cat([qkv, remote_qkv.unsqueeze(0)], dim=1) + beta_with_remote_tail = torch.cat([beta, remote_beta.unsqueeze(0)], dim=1) + recurrent_g_with_remote_tail = torch.cat( + [recurrent_g, remote_g.unsqueeze(0)], dim=1 + ) + cp_dependency = cp_dependency + _make_zero_autograd_dependency( + remote_qkv, remote_beta, remote_g + ) gate = gate.clone() recurrent_output = torch.zeros_like(gate) prefix_family_chunks: list[Tensor] = [] prefix_conv_chunks: list[Tensor] = [] prefix_rec_chunks: list[Tensor] = [] - cp_dependency = _empty_autograd_dependency(qkv) for bucket in plan.chain_prefix_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - prefix_qkv, prefix_beta, prefix_g = _gather_bucket_streams( - qkv, beta, recurrent_g, bucket - ) - zero_conv = _zero_conv_state(gdn, gdn_hidden, batch_size=prefix_qkv.shape[0]) - zero_rec = _zero_recurrent_state( - gdn, gdn_hidden, batch_size=prefix_qkv.shape[0] - ) - with _nvtx_range("art_gdn_cp_prefix_segment", prefix_qkv): - prefix_out, prefix_conv, prefix_rec = run_gdn_prepared_varlen_native_fla_cp( - gdn, - prefix_qkv, - beta=prefix_beta, - recurrent_g=prefix_g, - lengths=bucket.lengths, - cu_seqlens=bucket.cu_seqlens, - conv_initial=zero_conv, - recurrent_initial=zero_rec, - group=group, - output_final_state=True, - ) + prefix_qkv, prefix_beta, prefix_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) + zero_conv = _zero_conv_state(gdn, qkv, batch_size=bucket.segment_count) + zero_rec = _zero_recurrent_state(gdn, qkv, batch_size=bucket.segment_count) + prefix_out, prefix_conv, prefix_rec = run_gdn_bucket( + bucket, + (prefix_qkv, prefix_beta, prefix_g), + (zero_conv, zero_rec), + gdn=gdn, + group=group, + recurrent_cp=True, + output_final_state=True, + ) if prefix_conv is None or prefix_rec is None: raise RuntimeError("CP prefix GDN execution must return final states") prefix_out = _add_autograd_dependency(prefix_out, cp_dependency) @@ -699,25 +736,18 @@ def _run_cp_planned_prefixes_and_completions( boundary_conv_chunks: list[Tensor] = [] boundary_rec_chunks: list[Tensor] = [] for bucket in plan.prefix_boundary_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - prefix_qkv, prefix_beta, prefix_g = _gather_bucket_streams( - qkv, beta, recurrent_g, bucket - ) - zero_conv = _zero_conv_state(gdn, gdn_hidden, batch_size=prefix_qkv.shape[0]) - zero_rec = _zero_recurrent_state( - gdn, gdn_hidden, batch_size=prefix_qkv.shape[0] - ) - with _nvtx_range("art_gdn_local_prefix_segment", prefix_qkv): - prefix_out, prefix_conv, prefix_rec = _run_gdn_prepared_varlen_batch( - gdn, - prefix_qkv, - beta=prefix_beta, - recurrent_g=prefix_g, - bucket=bucket, - conv_initial=zero_conv, - recurrent_initial=zero_rec, - output_final_state=True, - ) + prefix_qkv, prefix_beta, prefix_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) + zero_conv = _zero_conv_state(gdn, qkv, batch_size=bucket.segment_count) + zero_rec = _zero_recurrent_state(gdn, qkv, batch_size=bucket.segment_count) + prefix_out, prefix_conv, prefix_rec = run_gdn_bucket( + bucket, + (prefix_qkv, prefix_beta, prefix_g), + (zero_conv, zero_rec), + gdn=gdn, + output_final_state=True, + ) if prefix_conv is None or prefix_rec is None: raise RuntimeError("local prefix GDN execution must return final states") prefix_out = _add_autograd_dependency(prefix_out, cp_dependency) @@ -733,42 +763,55 @@ def _run_cp_planned_prefixes_and_completions( prefix_conv_chunks.append(prefix_conv) prefix_rec_chunks.append(prefix_rec) - if plan.prefix_tail_buckets or plan.completion_warmup_buckets: + if ( + plan.prefix_tail_buckets + or plan.remote_prefix_tail_buckets + or plan.completion_with_prefix_tail_buckets + or plan.remote_completion_with_prefix_tail_buckets + or plan.remote_prefix_tail_state_transfers + ): boundary_conv_table = _materialize_indexed_family_state_table( plan=plan, family_chunks=boundary_family_chunks, state_chunks=boundary_conv_chunks, - zero_state=_zero_conv_state(gdn, gdn_hidden, batch_size=plan.family_count), + zero_state=_zero_conv_state(gdn, qkv, batch_size=plan.family_count), ) boundary_rec_table = _materialize_indexed_family_state_table( plan=plan, family_chunks=boundary_family_chunks, state_chunks=boundary_rec_chunks, - zero_state=_zero_recurrent_state( - gdn, gdn_hidden, batch_size=plan.family_count - ), - ) + zero_state=_zero_recurrent_state(gdn, qkv, batch_size=plan.family_count), + ) + remote_boundary_conv_table = boundary_conv_table + remote_boundary_rec_table = boundary_rec_table + if plan.remote_prefix_tail_state_transfers: + ( + remote_boundary_conv_table, + remote_boundary_rec_table, + remote_boundary_dependency, + ) = _exchange_parent_state_rows( + boundary_conv_table, + boundary_rec_table, + transfers=plan.remote_prefix_tail_state_transfers, + group=group, + ) + cp_dependency = cp_dependency + remote_boundary_dependency tail_family_chunks: list[Tensor] = [] tail_conv_chunks: list[Tensor] = [] tail_rec_chunks: list[Tensor] = [] for bucket in plan.prefix_tail_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - tail_qkv, tail_beta, tail_g = _gather_bucket_streams( - qkv, beta, recurrent_g, bucket - ) + tail_qkv, tail_beta, tail_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) tail_conv = boundary_conv_table.index_select(0, bucket.family_indices) tail_rec = boundary_rec_table.index_select(0, bucket.family_indices) - with _nvtx_range("art_gdn_local_prefix_segment", tail_qkv): - tail_out, tail_conv, tail_rec = _run_gdn_prepared_varlen_batch( - gdn, - tail_qkv, - beta=tail_beta, - recurrent_g=tail_g, - bucket=bucket, - conv_initial=tail_conv, - recurrent_initial=tail_rec, - output_final_state=True, - ) + tail_out, tail_conv, tail_rec = run_gdn_bucket( + bucket, + (tail_qkv, tail_beta, tail_g), + (tail_conv, tail_rec), + gdn=gdn, + output_final_state=True, + ) if tail_conv is None or tail_rec is None: raise RuntimeError("local prefix tail GDN execution must return states") tail_out = _add_autograd_dependency(tail_out, cp_dependency) @@ -783,6 +826,37 @@ def _run_cp_planned_prefixes_and_completions( prefix_family_chunks.append(bucket.family_indices) prefix_conv_chunks.append(tail_conv) prefix_rec_chunks.append(tail_rec) + for bucket in plan.remote_prefix_tail_buckets: + tail_qkv, tail_beta, tail_g = _gather_bucket_streams( + qkv_with_remote_tail, + beta_with_remote_tail, + recurrent_g_with_remote_tail, + bucket, + ) + tail_conv = remote_boundary_conv_table.index_select( + 0, bucket.family_indices + ) + tail_rec = remote_boundary_rec_table.index_select(0, bucket.family_indices) + tail_out, tail_conv, tail_rec = run_gdn_bucket( + bucket, + (tail_qkv, tail_beta, tail_g), + (tail_conv, tail_rec), + gdn=gdn, + output_final_state=True, + ) + if tail_conv is None or tail_rec is None: + raise RuntimeError( + "remote prefix tail GDN execution must return states" + ) + tail_out = _add_autograd_dependency(tail_out, cp_dependency) + tail_conv = _add_autograd_dependency(tail_conv, cp_dependency) + tail_rec = _add_autograd_dependency(tail_rec, cp_dependency) + tail_family_chunks.append(bucket.family_indices) + tail_conv_chunks.append(tail_conv) + tail_rec_chunks.append(tail_rec) + prefix_family_chunks.append(bucket.family_indices) + prefix_conv_chunks.append(tail_conv) + prefix_rec_chunks.append(tail_rec) prefix_conv_table = _replace_indexed_family_states( boundary_conv_table, family_chunks=tail_family_chunks, @@ -793,67 +867,63 @@ def _run_cp_planned_prefixes_and_completions( family_chunks=tail_family_chunks, state_chunks=tail_rec_chunks, ) - for bucket in plan.completion_warmup_buckets: + for bucket in plan.completion_with_prefix_tail_buckets: completion_conv = prefix_conv_table.index_select(0, bucket.family_indices) completion_rec = prefix_rec_table.index_select(0, bucket.family_indices) completion_conv, completion_rec = _couple_parent_states( completion_conv, completion_rec ) - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - completion_qkv, completion_beta, completion_g = _gather_bucket_streams( - qkv, beta, recurrent_g, bucket - ) - for ( - column_bucket, - qkv_col, - beta_col, - g_col, - conv_col, - rec_col, - ) in _iter_prepared_bucket_columns( - bucket, - completion_qkv, - completion_beta, - completion_g, - completion_conv, - completion_rec, - ): - with _nvtx_range("art_gdn_local_completion_segment", qkv_col): - completion_out, _, _ = _run_gdn_prepared_varlen_batch( - gdn, - qkv_col, - beta=beta_col, - recurrent_g=g_col, - bucket=column_bucket, - conv_initial=conv_col, - recurrent_initial=rec_col, - output_final_state=False, - ) - completion_out = _add_autograd_dependency(completion_out, cp_dependency) - recurrent_output = _scatter_bucket_recurrent_output( - recurrent_output, column_bucket, completion_out - ) - - for bucket in plan.local_prefix_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - prefix_qkv, prefix_beta, prefix_g = _gather_bucket_streams( + completion_qkv, completion_beta, completion_g = _gather_bucket_streams( qkv, beta, recurrent_g, bucket ) - zero_conv = _zero_conv_state(gdn, gdn_hidden, batch_size=prefix_qkv.shape[0]) - zero_rec = _zero_recurrent_state( - gdn, gdn_hidden, batch_size=prefix_qkv.shape[0] - ) - with _nvtx_range("art_gdn_local_prefix_segment", prefix_qkv): - prefix_out, prefix_conv, prefix_rec = _run_gdn_prepared_varlen_batch( - gdn, - prefix_qkv, - beta=prefix_beta, - recurrent_g=prefix_g, - bucket=bucket, - conv_initial=zero_conv, - recurrent_initial=zero_rec, - output_final_state=True, + completion_out, _, _ = run_gdn_bucket( + bucket, + (completion_qkv, completion_beta, completion_g), + (completion_conv, completion_rec), + gdn=gdn, + output_final_state=False, + ) + completion_out = _add_autograd_dependency(completion_out, cp_dependency) + recurrent_output = _scatter_bucket_recurrent_output( + recurrent_output, bucket, completion_out + ) + for bucket in plan.remote_completion_with_prefix_tail_buckets: + completion_conv = prefix_conv_table.index_select(0, bucket.family_indices) + completion_rec = prefix_rec_table.index_select(0, bucket.family_indices) + completion_conv, completion_rec = _couple_parent_states( + completion_conv, completion_rec + ) + completion_qkv, completion_beta, completion_g = _gather_bucket_streams( + qkv, + beta, + recurrent_g, + bucket, + ) + completion_out, _, _ = run_gdn_bucket( + bucket, + (completion_qkv, completion_beta, completion_g), + (completion_conv, completion_rec), + gdn=gdn, + output_final_state=False, + ) + completion_out = _add_autograd_dependency(completion_out, cp_dependency) + recurrent_output = _scatter_bucket_recurrent_output( + recurrent_output, bucket, completion_out ) + + for bucket in plan.local_prefix_buckets: + prefix_qkv, prefix_beta, prefix_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) + zero_conv = _zero_conv_state(gdn, qkv, batch_size=bucket.segment_count) + zero_rec = _zero_recurrent_state(gdn, qkv, batch_size=bucket.segment_count) + prefix_out, prefix_conv, prefix_rec = run_gdn_bucket( + bucket, + (prefix_qkv, prefix_beta, prefix_g), + (zero_conv, zero_rec), + gdn=gdn, + output_final_state=True, + ) if prefix_conv is None or prefix_rec is None: raise RuntimeError("local prefix GDN execution must return final states") prefix_out = _add_autograd_dependency(prefix_out, cp_dependency) @@ -867,26 +937,45 @@ def _run_cp_planned_prefixes_and_completions( prefix_rec_chunks.append(prefix_rec) if not prefix_conv_chunks and not plan.parent_state_exchange_family_indices: - projected, out_bias = _project_gdn_output(gdn, recurrent_output, gate, plan) - if output_layout == "gdn": - return projected, out_bias - return _cp_output_to_attention(projected, plan, original_shape, group), out_bias + projected, out_bias = _project_cp_gdn_output( + gdn, + recurrent_output, + gate, + plan, + group=group, + output_layout=output_layout, + ) + projected = _add_autograd_dependency(projected, cp_dependency) + return projected, out_bias prefix_conv_table = _materialize_ordered_family_state_table( family_chunks=prefix_family_chunks, state_chunks=prefix_conv_chunks, - zero_state=_zero_conv_state(gdn, gdn_hidden, batch_size=plan.family_count), + zero_state=_zero_conv_state(gdn, qkv, batch_size=plan.family_count), ) prefix_rec_table = _materialize_ordered_family_state_table( family_chunks=prefix_family_chunks, state_chunks=prefix_rec_chunks, - zero_state=_zero_recurrent_state(gdn, gdn_hidden, batch_size=plan.family_count), + zero_state=_zero_recurrent_state(gdn, qkv, batch_size=plan.family_count), ) - for bucket in plan.chain_completion_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - completion_qkv, completion_beta, completion_g = _gather_bucket_streams( - qkv, beta, recurrent_g, bucket + parent_state_exchanged = False + if plan.chain_completion_buckets and plan.parent_state_exchange_family_indices: + if not plan.parent_state_transfers: + raise ValueError("CP parent-state exchange requires planned transfers") + prefix_conv_table, prefix_rec_table, exchange_dependency = ( + _exchange_parent_state_rows( + prefix_conv_table, + prefix_rec_table, + transfers=plan.parent_state_transfers, + group=group, ) + ) + cp_dependency = cp_dependency + exchange_dependency + parent_state_exchanged = True + for bucket in plan.chain_completion_buckets: + completion_qkv, completion_beta, completion_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) completion_conv = prefix_conv_table.index_select(0, bucket.family_indices) completion_rec = prefix_rec_table.index_select(0, bucket.family_indices) completion_conv, completion_rec = _couple_parent_states( @@ -894,19 +983,15 @@ def _run_cp_planned_prefixes_and_completions( ) completion_conv = _scale_state_gradient(completion_conv, 1.0 / plan.cp_size) completion_rec = _scale_state_gradient(completion_rec, 1.0 / plan.cp_size) - with _nvtx_range("art_gdn_cp_completion_segment", completion_qkv): - completion_out, _, _ = run_gdn_prepared_varlen_native_fla_cp( - gdn, - completion_qkv, - beta=completion_beta, - recurrent_g=completion_g, - lengths=bucket.lengths, - cu_seqlens=bucket.cu_seqlens, - conv_initial=completion_conv, - recurrent_initial=completion_rec, - group=group, - output_final_state=False, - ) + completion_out, _, _ = run_gdn_bucket( + bucket, + (completion_qkv, completion_beta, completion_g), + (completion_conv, completion_rec), + gdn=gdn, + group=group, + recurrent_cp=True, + output_final_state=False, + ) completion_out = _add_autograd_dependency(completion_out, cp_dependency) cp_dependency = _make_autograd_dependency(completion_out) recurrent_output = _scatter_bucket_recurrent_output( @@ -919,76 +1004,70 @@ def _run_cp_planned_prefixes_and_completions( else plan.local_completion_buckets ) for bucket in ready_completion_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - completion_qkv, completion_beta, completion_g = _gather_bucket_streams( - qkv, beta, recurrent_g, bucket - ) + completion_qkv, completion_beta, completion_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) completion_conv = prefix_conv_table.index_select(0, bucket.family_indices) completion_rec = prefix_rec_table.index_select(0, bucket.family_indices) completion_conv, completion_rec = _couple_parent_states( completion_conv, completion_rec ) - with _nvtx_range("art_gdn_local_completion_segment", completion_qkv): - completion_out, _, _ = _run_gdn_prepared_varlen_batch( - gdn, - completion_qkv, - beta=completion_beta, - recurrent_g=completion_g, - bucket=bucket, - conv_initial=completion_conv, - recurrent_initial=completion_rec, - output_final_state=False, - ) + completion_out, _, _ = run_gdn_bucket( + bucket, + (completion_qkv, completion_beta, completion_g), + (completion_conv, completion_rec), + gdn=gdn, + output_final_state=False, + ) completion_out = _add_autograd_dependency(completion_out, cp_dependency) recurrent_output = _scatter_bucket_recurrent_output( recurrent_output, bucket, completion_out ) - if plan.parent_state_exchange_family_indices: + if plan.parent_state_exchange_family_indices and not parent_state_exchanged: if not plan.parent_state_transfers: raise ValueError("CP parent-state exchange requires planned transfers") - with _nvtx_range("art_gdn_cp_parent_state_exchange", prefix_conv_table): - prefix_conv_table, prefix_rec_table, exchange_dependency = ( - _exchange_parent_state_rows( - prefix_conv_table, - prefix_rec_table, - transfers=plan.parent_state_transfers, - group=group, - ) + prefix_conv_table, prefix_rec_table, exchange_dependency = ( + _exchange_parent_state_rows( + prefix_conv_table, + prefix_rec_table, + transfers=plan.parent_state_transfers, + group=group, ) + ) cp_dependency = cp_dependency + exchange_dependency for bucket in plan.remote_local_completion_buckets: - with _nvtx_range("art_gdn_input_layout_gather_reorder", qkv): - completion_qkv, completion_beta, completion_g = _gather_bucket_streams( - qkv, beta, recurrent_g, bucket - ) + completion_qkv, completion_beta, completion_g = _gather_bucket_streams( + qkv, beta, recurrent_g, bucket + ) completion_conv = prefix_conv_table.index_select(0, bucket.family_indices) completion_rec = prefix_rec_table.index_select(0, bucket.family_indices) completion_conv, completion_rec = _couple_parent_states( completion_conv, completion_rec ) - with _nvtx_range("art_gdn_local_completion_segment", completion_qkv): - completion_out, _, _ = _run_gdn_prepared_varlen_batch( - gdn, - completion_qkv, - beta=completion_beta, - recurrent_g=completion_g, - bucket=bucket, - conv_initial=completion_conv, - recurrent_initial=completion_rec, - output_final_state=False, - ) + completion_out, _, _ = run_gdn_bucket( + bucket, + (completion_qkv, completion_beta, completion_g), + (completion_conv, completion_rec), + gdn=gdn, + output_final_state=False, + ) completion_out = _add_autograd_dependency(completion_out, cp_dependency) recurrent_output = _scatter_bucket_recurrent_output( recurrent_output, bucket, completion_out ) - projected, out_bias = _project_gdn_output(gdn, recurrent_output, gate, plan) + projected, out_bias = _project_cp_gdn_output( + gdn, + recurrent_output, + gate, + plan, + group=group, + output_layout=output_layout, + ) projected = _add_autograd_dependency(projected, cp_dependency) - if output_layout == "gdn": - return projected, out_bias - return _cp_output_to_attention(projected, plan, original_shape, group), out_bias + return projected, out_bias @torch.compiler.disable @@ -996,20 +1075,29 @@ def gdn_cp_attention_to_gdn_layout( hidden_states: Tensor, plan: GdnRankExecutionPlan, group: Any, + gdn: Any | None = None, ) -> tuple[Tensor, tuple[int, int, int]]: from .layout import exchange_rank_tensor_all_to_all if plan.attention_to_gdn is None or plan.gdn_to_attention is None: raise ValueError("CP GDN layout conversion requires prebuilt exchange plans") - attention_flat, original_shape = _flatten_hidden_for_cp_plan(hidden_states, plan) - with _nvtx_range("art_gdn_cp_attention_to_gdn_exchange", attention_flat): - gdn_flat = exchange_rank_tensor_all_to_all( - attention_flat, - plan.attention_to_gdn, - rank=plan.cp_rank, - group=group, - backward_plan=plan.gdn_to_attention, - ) + exchange_plan, backward_plan, rank, group = _hidden_layout_exchange_context( + plan, + gdn=gdn, + group=group, + forward_plan=plan.attention_to_gdn, + backward_plan=plan.gdn_to_attention, + ) + attention_flat, original_shape = _flatten_hidden_for_exchange_plan( + hidden_states, exchange_plan, rank=rank + ) + gdn_flat = exchange_rank_tensor_all_to_all( + attention_flat, + exchange_plan, + rank=rank, + group=group, + backward_plan=backward_plan, + ) return gdn_flat.unsqueeze(1).contiguous(), original_shape @@ -1019,55 +1107,156 @@ def gdn_cp_gdn_to_attention_layout( plan: GdnRankExecutionPlan, original_shape: tuple[int, int, int] | None, group: Any, + gdn: Any | None = None, ) -> Tensor: - original_shape = original_shape or _attention_original_shape_from_plan( - gdn_hidden, plan - ) - return _cp_output_to_attention(gdn_hidden, plan, original_shape, group) + if original_shape is None: + raise RuntimeError("GDN CP output layout conversion requires original_shape") + return _cp_output_to_attention(gdn_hidden, plan, original_shape, group, gdn=gdn) + + +def _normalize_cp_layout(value: Any) -> Literal["attention", "gdn"]: + if value in ("attention", "gdn"): + return cast(Literal["attention", "gdn"], value) + raise ValueError(f"unsupported GDN CP layout {value!r}") def _enter_gdn_island_layout( - hidden_states: Tensor, attention_bias: Any, *, force: bool = False + hidden_states: Tensor, + attention_bias: Any, + *, + gdn: Any | None = None, + island_id: int | None = None, + force: bool = False, ) -> Tensor: plan = _require_gdn_cp_plan(attention_bias) if not force and getattr(attention_bias, "gdn_hidden_layout", "attention") == "gdn": - return _validate_gdn_hidden_for_cp_plan(hidden_states, plan) + return _validate_gdn_hidden_for_cp_plan(hidden_states, plan, gdn=gdn) gdn_hidden, original_shape = gdn_cp_attention_to_gdn_layout( hidden_states, plan, _default_cp_group(plan.cp_size), + gdn=gdn, ) attention_bias.gdn_hidden_layout = "gdn" - attention_bias.gdn_attention_original_shape = original_shape - return gdn_hidden - + _store_gdn_attention_original_shape( + attention_bias, original_shape, gdn=gdn, island_id=island_id + ) + if gdn is not None: + attention_bias.gdn_active_module = gdn + token_uids = ( + _local_layout_token_uids(plan, "gdn", hidden_states=gdn_hidden, gdn=gdn) + if _layout_token_uids_enabled() + else None + ) + _set_active_routing_replay_layout("gdn") + return _attach_gdn_attention_original_shape( + _attach_trace_token_uids(gdn_hidden, token_uids), + original_shape, + ) + + +def _mark_cp_layout_active( + attention_bias: Any, + hidden_states: Tensor | None, + *, + gdn: Any | None, + island_id: int | None = None, + layout: Literal["attention", "gdn"], +) -> None: + if layout == "gdn": + _mark_gdn_layout_active( + attention_bias, hidden_states, gdn=gdn, island_id=island_id + ) + else: + _mark_attention_layout_active(attention_bias, hidden_states, gdn=gdn) + -def _mark_attention_layout_active(attention_bias: Any) -> None: +def _mark_attention_layout_active( + attention_bias: Any, + hidden_states: Tensor | None = None, + *, + gdn: Any | None = None, +) -> None: attention_bias.gdn_hidden_layout = "attention" attention_bias.gdn_attention_original_shape = None + attention_bias.gdn_attention_token_uids = None + attention_bias.gdn_active_module = None + if hidden_states is None: + return + plan = _require_gdn_cp_plan(attention_bias) + token_uids = ( + _local_layout_token_uids( + plan, "attention", hidden_states=hidden_states, gdn=gdn + ) + if _layout_token_uids_enabled() + else None + ) + _set_active_routing_replay_layout("attention") + _attach_trace_token_uids(hidden_states, token_uids) + + +def _mark_gdn_layout_active( + attention_bias: Any, + hidden_states: Tensor | None, + *, + gdn: Any | None = None, + island_id: int | None = None, +) -> None: + plan = _require_gdn_cp_plan(attention_bias) + attention_bias.gdn_hidden_layout = "gdn" + if gdn is not None: + attention_bias.gdn_active_module = gdn + if hidden_states is None: + return + original_shape = _gdn_attention_original_shape_from_tensor(hidden_states) + if original_shape is not None: + _store_gdn_attention_original_shape( + attention_bias, original_shape, gdn=gdn, island_id=island_id + ) + gdn_token_uids = ( + _local_layout_token_uids(plan, "gdn", hidden_states=hidden_states, gdn=gdn) + if _layout_token_uids_enabled() + else None + ) + _set_active_routing_replay_layout("gdn") + _attach_trace_token_uids(hidden_states, gdn_token_uids) -def _leave_gdn_island_layout(hidden_states: Tensor, attention_bias: Any) -> Tensor: +def _leave_gdn_island_layout( + hidden_states: Tensor, + attention_bias: Any, + *, + gdn: Any | None = None, + island_id: int | None = None, +) -> Tensor: plan = _require_gdn_cp_plan(attention_bias) - gdn_hidden = _validate_gdn_hidden_for_cp_plan(hidden_states, plan) + gdn_hidden = _validate_gdn_hidden_for_cp_plan(hidden_states, plan, gdn=gdn) + original_shape = _gdn_attention_original_shape_from_state( + attention_bias, gdn=gdn, island_id=island_id + ) + if original_shape is None: + original_shape = _gdn_attention_original_shape_from_tensor(hidden_states) + if original_shape is not None: + _store_gdn_attention_original_shape( + attention_bias, original_shape, gdn=gdn, island_id=island_id + ) attention_hidden = gdn_cp_gdn_to_attention_layout( gdn_hidden, plan, - getattr(attention_bias, "gdn_attention_original_shape", None), + original_shape, _default_cp_group(plan.cp_size), + gdn=gdn, ) _mark_attention_layout_active(attention_bias) - return attention_hidden - - -def _mark_gdn_layout_active(attention_bias: Any, hidden_states: Tensor) -> None: - plan = _require_gdn_cp_plan(attention_bias) - _validate_gdn_hidden_for_cp_plan(hidden_states, plan) - attention_bias.gdn_hidden_layout = "gdn" - if getattr(attention_bias, "gdn_attention_original_shape", None) is None: - attention_bias.gdn_attention_original_shape = ( - _attention_original_shape_from_plan(hidden_states, plan) + token_uids = ( + _local_layout_token_uids( + plan, "attention", hidden_states=attention_hidden, gdn=gdn ) + if _layout_token_uids_enabled() + else None + ) + _set_active_routing_replay_layout("attention") + return _attach_trace_token_uids(attention_hidden, token_uids) def _require_gdn_cp_plan(attention_bias: Any) -> GdnRankExecutionPlan: @@ -1082,41 +1271,362 @@ def _cp_output_to_attention( plan: GdnRankExecutionPlan, original_shape: tuple[int, int, int], group: Any, + *, + gdn: Any | None = None, ) -> Tensor: from .layout import exchange_rank_tensor_all_to_all if plan.gdn_to_attention is None: raise ValueError("CP GDN execution requires a GDN-to-attention exchange plan") - gdn_flat = gdn_output.squeeze(1).contiguous() - with _nvtx_range("art_gdn_cp_gdn_to_attention_exchange", gdn_flat): - attention_flat = exchange_rank_tensor_all_to_all( - gdn_flat, - plan.gdn_to_attention, - rank=plan.cp_rank, - group=group, - backward_plan=plan.attention_to_gdn, - ) + if plan.attention_to_gdn is None: + raise ValueError("CP GDN execution requires an attention-to-GDN backward plan") + exchange_plan, backward_plan, rank, group = _hidden_layout_exchange_context( + plan, + gdn=gdn, + group=group, + forward_plan=plan.gdn_to_attention, + backward_plan=plan.attention_to_gdn, + ) + gdn_flat, _ = _flatten_hidden_for_exchange_plan( + gdn_output, exchange_plan, rank=rank + ) + attention_flat = exchange_rank_tensor_all_to_all( + gdn_flat, + exchange_plan, + rank=rank, + group=group, + backward_plan=backward_plan, + ) return _restore_hidden_from_cp_flat(attention_flat, original_shape) -def _flatten_hidden_for_cp_plan( - hidden_states: Tensor, plan: GdnRankExecutionPlan +def _hidden_layout_exchange_context( + plan: GdnRankExecutionPlan, + *, + gdn: Any | None, + group: Any, + forward_plan: Any, + backward_plan: Any, +) -> tuple[Any, Any, int, Any]: + projection = _gdn_output_projection(gdn) or _gdn_input_projection(gdn) + if projection is None or not _uses_sequence_parallel(projection): + return forward_plan, backward_plan, int(plan.cp_rank), group + from .layout import shard_cp_exchange_plan_for_sequence_parallel + + tp_size = _tp_world_size(projection) + tp_rank = _tp_rank(projection) + tp_cp_group = _default_tp_cp_group(plan.cp_size, tp_size) + tp_cp_rank = _group_rank(tp_cp_group) + sharded_forward = shard_cp_exchange_plan_for_sequence_parallel( + forward_plan, + cp_rank=int(plan.cp_rank), + tp_rank=tp_rank, + tp_size=tp_size, + tp_cp_rank=tp_cp_rank, + device=_exchange_plan_device(forward_plan), + ) + sharded_backward = shard_cp_exchange_plan_for_sequence_parallel( + backward_plan, + cp_rank=int(plan.cp_rank), + tp_rank=tp_rank, + tp_size=tp_size, + tp_cp_rank=tp_cp_rank, + device=_exchange_plan_device(backward_plan), + ) + return ( + sharded_forward.plan, + sharded_backward.plan, + sharded_forward.rank, + tp_cp_group, + ) + + +def _flatten_hidden_for_exchange_plan( + hidden_states: Tensor, plan: Any, *, rank: int ) -> tuple[Tensor, tuple[int, int, int]]: seq_len, batch_size, hidden_size = hidden_states.shape flat = hidden_states.transpose(0, 1).reshape(seq_len * batch_size, hidden_size) - expected = int(plan.attention_token_count) - if int(flat.shape[0]) != expected: + expected = int(plan.source_token_counts_by_rank[rank]) + if int(flat.shape[0]) < expected: raise ValueError( - "CP GDN hidden token count must match the rank-local attention plan, " + "CP GDN hidden token count must match the exchange source layout, " f"got {int(flat.shape[0])} tokens and expected {expected}" ) - return flat.contiguous(), (seq_len, batch_size, hidden_size) + return flat[:expected].contiguous(), (seq_len, batch_size, hidden_size) + + +def _exchange_plan_device(plan: Any) -> torch.device | str | None: + for transfer in getattr(plan, "transfers", ()): + for tensor in ( + getattr(transfer, "source_positions_tensor", None), + getattr(transfer, "dest_positions_tensor", None), + ): + if isinstance(tensor, Tensor): + return tensor.device + return None + + +def _hidden_token_count(hidden_states: Tensor) -> int: + if hidden_states.ndim < 2: + return 0 + return int(hidden_states.shape[0]) * int(hidden_states.shape[1]) + + +def _layout_token_uids( + plan: GdnRankExecutionPlan, layout: Literal["attention", "gdn"] +) -> Tensor: + indices = ( + plan.gdn_token_indices if layout == "gdn" else plan.attention_token_indices + ) + return torch.tensor(indices, dtype=torch.int64) + + +def _trace_token_uids_enabled() -> bool: + return _GDN_TRACE_TOKEN_UID_HOOKS is not None + + +def _local_layout_token_uids( + plan: GdnRankExecutionPlan, + layout: Literal["attention", "gdn"], + *, + hidden_states: Tensor, + gdn: Any | None, +) -> Tensor: + token_uids = _layout_token_uids(plan, layout) + token_count = _hidden_token_count(hidden_states) + if token_count == int(token_uids.numel()): + return token_uids + if token_count <= 0: + return token_uids.new_empty((0,)) + projection = _gdn_output_projection(gdn) + tp_rank = _tp_rank(projection) if projection is not None else 0 + start = tp_rank * token_count + end = min(start + token_count, int(token_uids.numel())) + local_uids = token_uids.new_full((token_count,), -1) + if start >= int(token_uids.numel()): + return local_uids + real_uids = token_uids[start:end] + local_uids[: int(real_uids.numel())] = real_uids + return local_uids + + +def _replicated_layout_token_uids( + plan: GdnRankExecutionPlan, + layout: Literal["attention", "gdn"], + *, + hidden_states: Tensor, +) -> Tensor: + token_uids = _layout_token_uids(plan, layout) + token_count = _hidden_token_count(hidden_states) + if token_count == int(token_uids.numel()): + return token_uids + if token_count <= 0: + return token_uids.new_empty((0,)) + local_uids = token_uids.new_full((token_count,), -1) + real_uids = token_uids[: min(token_count, int(token_uids.numel()))] + local_uids[: int(real_uids.numel())] = real_uids + return local_uids + + +def _attach_trace_token_uids(tensor: Tensor, token_uids: Tensor | None) -> Tensor: + hooks = _GDN_TRACE_TOKEN_UID_HOOKS + if hooks is None or token_uids is None: + return tensor + attach = getattr(hooks, "attach_token_uids", None) + return tensor if attach is None else cast(Tensor, attach(tensor, token_uids)) + + +def _prepare_in_proj_trace_token_uids(gdn: Any, hidden_states: Tensor) -> None: + hooks = _GDN_TRACE_TOKEN_UID_HOOKS + if hooks is None: + return + prepare = getattr(hooks, "prepare_in_proj_token_uids", None) + if prepare is not None: + prepare(gdn, hidden_states) + + +def _set_out_proj_lora_trace_token_uids(gdn: Any, hidden_states: Tensor) -> None: + hooks = _GDN_TRACE_TOKEN_UID_HOOKS + if hooks is None: + return + setter = getattr(hooks, "set_out_proj_lora_token_uids", None) + if setter is not None: + setter(gdn, hidden_states) + + +def _set_out_norm_trace_token_uids(gdn: Any, token_uids: Tensor | None) -> None: + hooks = _GDN_TRACE_TOKEN_UID_HOOKS + if hooks is None or token_uids is None: + return + setter = getattr(hooks, "set_out_norm_token_uids", None) + if setter is not None: + setter(gdn, token_uids) + + +def _set_out_proj_trace_token_uids( + gdn: Any, + hidden_states: Tensor, + *, + sequence_parallel_output: bool, +) -> None: + hooks = _GDN_TRACE_TOKEN_UID_HOOKS + if hooks is None: + return + setter = getattr(hooks, "set_out_proj_token_uids", None) + if setter is not None: + setter( + gdn, + hidden_states, + sequence_parallel_output=sequence_parallel_output, + ) + + +def _pad_trace_token_uids_for_stream(token_uids: Tensor, stream: Tensor) -> Tensor: + hooks = _GDN_TRACE_TOKEN_UID_HOOKS + pad = None if hooks is None else getattr(hooks, "pad_token_uids_for_stream", None) + if pad is not None: + return cast(Tensor, pad(token_uids, stream)) + return token_uids + + +def _attach_gdn_attention_original_shape( + tensor: Tensor, original_shape: tuple[int, int, int] | None +) -> Tensor: + if original_shape is not None: + setattr( + tensor, + _GDN_ATTENTION_ORIGINAL_SHAPE_ATTR, + tuple(int(dim) for dim in original_shape), + ) + return tensor + + +def _store_gdn_attention_original_shape( + attention_bias: Any, + original_shape: tuple[int, int, int], + *, + gdn: Any | None, + island_id: int | None = None, +) -> tuple[int, int, int]: + normalized = ( + int(original_shape[0]), + int(original_shape[1]), + int(original_shape[2]), + ) + attention_bias.gdn_attention_original_shape = normalized + cache = _gdn_attention_original_shape_cache(attention_bias) + cache[_gdn_attention_original_shape_cache_key(gdn)] = normalized + if island_id is not None: + cache[_gdn_attention_original_shape_cache_key(None, island_id)] = normalized + return normalized + + +def _gdn_attention_original_shape_from_state( + attention_bias: Any, + *, + gdn: Any | None, + island_id: int | None = None, +) -> tuple[int, int, int] | None: + cache = getattr(attention_bias, "gdn_attention_original_shapes", None) + if isinstance(cache, dict): + if island_id is not None: + original_shape = _normalize_gdn_attention_original_shape( + cache.get(_gdn_attention_original_shape_cache_key(None, island_id)) + ) + if original_shape is not None: + return original_shape + if gdn is not None: + original_shape = _normalize_gdn_attention_original_shape( + cache.get(_gdn_attention_original_shape_cache_key(gdn)) + ) + if original_shape is not None: + return original_shape + active_gdn = getattr(attention_bias, "gdn_active_module", None) + if active_gdn is not None: + original_shape = _normalize_gdn_attention_original_shape( + cache.get(_gdn_attention_original_shape_cache_key(active_gdn)) + ) + if original_shape is not None: + return original_shape + if gdn is None: + original_shape = _normalize_gdn_attention_original_shape( + cache.get(_gdn_attention_original_shape_cache_key(None)) + ) + if original_shape is not None: + return original_shape + original_shape = _normalize_gdn_attention_original_shape( + getattr(attention_bias, "gdn_attention_original_shape", None) + ) + active_gdn = getattr(attention_bias, "gdn_active_module", None) + if original_shape is None or ( + gdn is not None and active_gdn is not None and active_gdn is not gdn + ): + return None + return original_shape + + +def _gdn_attention_original_shape_cache( + attention_bias: Any, +) -> dict[int, tuple[int, int, int]]: + cache = getattr(attention_bias, "gdn_attention_original_shapes", None) + if not isinstance(cache, dict): + cache = {} + setattr(attention_bias, "gdn_attention_original_shapes", cache) + return cast(dict[int, tuple[int, int, int]], cache) + + +def _gdn_attention_original_shape_cache_key( + gdn: Any | None, island_id: int | None = None +) -> int: + if island_id is not None: + return -int(island_id) - 1 + return 0 if gdn is None else id(gdn) + + +def _normalize_gdn_attention_original_shape( + original_shape: Any, +) -> tuple[int, int, int] | None: + if not isinstance(original_shape, tuple) or len(original_shape) != 3: + return None + return (int(original_shape[0]), int(original_shape[1]), int(original_shape[2])) + + +def _gdn_attention_original_shape_from_tensor( + tensor: Tensor, +) -> tuple[int, int, int] | None: + original_shape = getattr(tensor, _GDN_ATTENTION_ORIGINAL_SHAPE_ATTR, None) + return _normalize_gdn_attention_original_shape(original_shape) + + +def _active_routing_replay_controller() -> Any | None: + try: + from art.megatron.routing_replay import _active_routing_replay_controller + except ImportError: + return None + return _active_routing_replay_controller() + + +def _layout_token_uids_enabled() -> bool: + return ( + _trace_token_uids_enabled() or _active_routing_replay_controller() is not None + ) + + +def _set_active_routing_replay_layout( + layout: Literal["attention", "gdn"], +) -> None: + controller = _active_routing_replay_controller() + if controller is None: + return + controller.set_active_token_uid_key(layout) def _validate_gdn_hidden_for_cp_plan( - hidden_states: Tensor, plan: GdnRankExecutionPlan + hidden_states: Tensor, plan: GdnRankExecutionPlan, *, gdn: Any | None = None ) -> Tensor: - expected = int(plan.gdn_token_count) + expected = _local_layout_token_count_for_hidden( + plan, "gdn", hidden_states=hidden_states, gdn=gdn + ) if hidden_states.ndim != 3 or int(hidden_states.shape[0]) != expected: raise ValueError( "CP GDN-layout hidden_states must be [rank_gdn_tokens, 1, D], " @@ -1130,6 +1640,25 @@ def _validate_gdn_hidden_for_cp_plan( return hidden_states.contiguous() +def _local_layout_token_count_for_hidden( + plan: GdnRankExecutionPlan, + layout: Literal["attention", "gdn"], + *, + hidden_states: Tensor, + gdn: Any | None, +) -> int: + del hidden_states + real_count = ( + int(plan.gdn_token_count) + if layout == "gdn" + else int(plan.attention_token_count) + ) + projection = _gdn_output_projection(gdn) or _gdn_input_projection(gdn) + if projection is None or not _uses_sequence_parallel(projection): + return real_count + return (real_count + _tp_world_size(projection) - 1) // _tp_world_size(projection) + + def _attention_original_shape_from_plan( hidden_states: Tensor, plan: GdnRankExecutionPlan ) -> tuple[int, int, int]: @@ -1140,11 +1669,17 @@ def _restore_hidden_from_cp_flat( flat: Tensor, original_shape: tuple[int, int, int] ) -> Tensor: seq_len, batch_size, hidden_size = original_shape - if int(flat.shape[0]) != seq_len * batch_size: + token_count = seq_len * batch_size + if int(flat.shape[0]) > token_count: raise ValueError( "CP GDN output token count changed across layout exchange, got " f"{int(flat.shape[0])} for original shape {original_shape}" ) + if int(flat.shape[0]) < token_count: + padded = flat.new_zeros((token_count, hidden_size)) + if int(flat.shape[0]) > 0: + padded[: int(flat.shape[0])] = flat + flat = padded return flat.reshape(batch_size, seq_len, hidden_size).transpose(0, 1).contiguous() @@ -1164,6 +1699,15 @@ def _make_autograd_dependency(*tensors: Tensor | None) -> Tensor: return dependency +def _make_zero_autograd_dependency(*tensors: Tensor) -> Tensor: + if not tensors: + raise ValueError("at least one tensor is required") + dependency = tensors[0].sum() * 0 + for tensor in tensors[1:]: + dependency = dependency + tensor.sum() * 0 + return dependency + + def _add_autograd_dependency(tensor: Tensor, dependency: Tensor) -> Tensor: return tensor + dependency.to(dtype=tensor.dtype) @@ -1209,198 +1753,26 @@ def backward(ctx: Any, *grad_outputs: Tensor | None) -> tuple[Tensor | None, Non return grad_output * ctx.scale, None -def _gather_flat_bucket_streams( - qkv_flat: Tensor, - beta_flat: Tensor, - recurrent_g_flat: Tensor, - *, - layout: _BucketFlatLayout, - length: int, - segment_count: int, -) -> tuple[Tensor, Tensor, Tensor]: - return _FlatBucketStreamGather.apply( - qkv_flat, - beta_flat, - recurrent_g_flat, - layout.padded_indices, - layout.padded_mask, - length, - segment_count, - ) - - -def _gather_compact_bucket_streams( - qkv: Tensor, - beta: Tensor, - recurrent_g: Tensor, - bucket: GdnSegmentBucketPlan, -) -> tuple[Tensor, Tensor, Tensor]: - return _gather_bucket_streams_compact_fused( - qkv.reshape(-1, int(qkv.shape[-1])), - beta.reshape(-1, int(beta.shape[-1])), - recurrent_g.reshape(-1, int(recurrent_g.shape[-1])), - bucket.row_indices, - bucket.position_indices, - bucket.cu_seqlens, - token_count=int(bucket.real_token_count), - segment_count=int(bucket.segment_count), - sequence_length=int(qkv.shape[1]), - ) - - -class _FlatBucketStreamGather(torch.autograd.Function): - @staticmethod - def forward( - ctx: Any, - qkv_flat: Tensor, - beta_flat: Tensor, - recurrent_g_flat: Tensor, - padded_indices: Tensor, - padded_mask: Tensor, - length: int, - segment_count: int, - ) -> tuple[Tensor, Tensor, Tensor]: - flat_indices = padded_indices.reshape(-1) - flat_mask = padded_mask.reshape(-1) - safe_indices = torch.where( - flat_mask, - flat_indices, - torch.zeros((), device=flat_indices.device, dtype=flat_indices.dtype), - ) - qkv = qkv_flat.index_select(0, safe_indices).reshape( - length, segment_count, int(qkv_flat.shape[-1]) - ) - beta = beta_flat.index_select(0, safe_indices).reshape( - length, segment_count, int(beta_flat.shape[-1]) - ) - recurrent_g = recurrent_g_flat.index_select(0, safe_indices).reshape( - length, segment_count, int(recurrent_g_flat.shape[-1]) - ) - qkv = qkv.masked_fill(~padded_mask.unsqueeze(-1), 0) - beta = beta.masked_fill(~padded_mask.unsqueeze(-1), 0) - recurrent_g = recurrent_g.masked_fill(~padded_mask.unsqueeze(-1), 0) - ctx.save_for_backward(safe_indices, flat_mask) - ctx.qkv_flat_count = int(qkv_flat.shape[0]) - ctx.beta_flat_count = int(beta_flat.shape[0]) - ctx.recurrent_g_flat_count = int(recurrent_g_flat.shape[0]) - return ( - qkv.permute(1, 2, 0).contiguous(), - beta.transpose(0, 1).contiguous(), - recurrent_g.transpose(0, 1).contiguous(), - ) - - @staticmethod - def backward( - ctx: Any, *grad_outputs: Tensor | None - ) -> tuple[Tensor | None, Tensor | None, Tensor | None, None, None, None, None]: - grad_qkv_bucket, grad_beta_bucket, grad_g_bucket = grad_outputs - safe_indices, flat_mask = ctx.saved_tensors - grad_qkv = ( - _bucket_stream_grad_to_flat( - grad_qkv_bucket.permute(2, 0, 1).contiguous() - if grad_qkv_bucket is not None - else None, - safe_indices, - flat_mask, - ctx.qkv_flat_count, - ) - if ctx.needs_input_grad[0] - else None - ) - grad_beta = ( - _bucket_stream_grad_to_flat( - grad_beta_bucket.transpose(0, 1).contiguous() - if grad_beta_bucket is not None - else None, - safe_indices, - flat_mask, - ctx.beta_flat_count, - ) - if ctx.needs_input_grad[1] - else None - ) - grad_g = ( - _bucket_stream_grad_to_flat( - grad_g_bucket.transpose(0, 1).contiguous() - if grad_g_bucket is not None - else None, - safe_indices, - flat_mask, - ctx.recurrent_g_flat_count, - ) - if ctx.needs_input_grad[2] - else None - ) - return grad_qkv, grad_beta, grad_g, None, None, None, None - - -def _bucket_stream_grad_to_flat( - grad: Tensor | None, - safe_indices: Tensor, - flat_mask: Tensor, - flat_count: int, -) -> Tensor | None: - if grad is None: - return None - grad_flat_values = grad.reshape(int(safe_indices.numel()), int(grad.shape[-1])) - grad_flat_values = grad_flat_values.masked_fill(~flat_mask.unsqueeze(-1), 0) - grad_flat = grad.new_zeros(flat_count, int(grad.shape[-1])) - return grad_flat.index_add(0, safe_indices, grad_flat_values) - - -def _scatter_compact_hidden( - compact: Tensor, - indices: Tensor, - *, - batch_size: int, - sequence_length: int, -) -> Tensor: - return _CompactHiddenScatter.apply(compact, indices, batch_size, sequence_length) - - -class _CompactHiddenScatter(torch.autograd.Function): - @staticmethod - def forward( - ctx: Any, - compact: Tensor, - indices: Tensor, - batch_size: int, - sequence_length: int, - ) -> Tensor: - hidden_size = int(compact.shape[-1]) - flat = compact.new_zeros(batch_size * sequence_length, hidden_size) - if int(indices.numel()): - flat = flat.index_copy(0, indices, compact.reshape(-1, hidden_size)) - ctx.save_for_backward(indices) - ctx.batch_size = batch_size - ctx.sequence_length = sequence_length - return ( - flat.reshape(batch_size, sequence_length, hidden_size) - .transpose(0, 1) - .contiguous() - ) - - @staticmethod - def backward( - ctx: Any, *grad_outputs: Any - ) -> tuple[Tensor | None, None, None, None]: - (grad_output,) = grad_outputs - if grad_output is None: - return None, None, None, None - (indices,) = ctx.saved_tensors - flat_grad = grad_output.transpose(0, 1).reshape( - ctx.batch_size * ctx.sequence_length, int(grad_output.shape[-1]) - ) - return flat_grad.index_select(0, indices), None, None, None - - def _project_gdn_inputs( - gdn: Any, hidden_states: Tensor + gdn: Any, + hidden_states: Tensor, + *, + sequence_parallel_input: bool = True, ) -> tuple[Tensor, Tensor, Tensor, Tensor]: seq_len, batch_size, _ = hidden_states.shape - seq_len *= int(getattr(gdn, "sp_size", 1)) - qkvzba, _ = _in_proj(gdn, hidden_states) + if sequence_parallel_input: + seq_len *= int(getattr(gdn, "sp_size", 1)) + qkvzba, _ = _in_proj( + gdn, + hidden_states, + sequence_parallel_input=sequence_parallel_input, + ) qkvzba = qkvzba.transpose(0, 1) + if int(qkvzba.shape[0]) != batch_size: + raise ValueError( + "GDN input projection changed the packed batch dimension, " + f"got {int(qkvzba.shape[0])} and expected {batch_size}" + ) qkv, gate, beta, alpha = torch.split( qkvzba, [ @@ -1423,7 +1795,43 @@ def _project_gdn_inputs( return qkv.contiguous(), gate, beta, recurrent_g -def _in_proj(gdn: Any, hidden_states: Tensor) -> tuple[Tensor, Tensor | None]: +def _project_empty_gdn_inputs( + gdn: Any, + hidden_states: Tensor, + *, + sequence_parallel_input: bool = True, +) -> tuple[Tensor, Tensor, Tensor, Tensor]: + seq_len, batch_size, _ = hidden_states.shape + if sequence_parallel_input: + seq_len *= int(getattr(gdn, "sp_size", 1)) + value_heads = _local_value_heads(gdn) + qkv_width = (gdn.qk_dim * 2 + gdn.v_dim) // gdn.tp_size + dependency = hidden_states.sum() * 0 + qkv = hidden_states.new_zeros((batch_size, seq_len, qkv_width)) + dependency + gate = ( + hidden_states.new_zeros((batch_size, seq_len, value_heads, gdn.value_head_dim)) + + dependency + ) + beta = hidden_states.new_zeros((batch_size, seq_len, value_heads)) + dependency + recurrent_g = ( + hidden_states.new_zeros((batch_size, seq_len, value_heads)) + dependency + ) + return ( + qkv.contiguous(), + gate.contiguous(), + beta.contiguous(), + recurrent_g.contiguous(), + ) + + +def _in_proj( + gdn: Any, + hidden_states: Tensor, + *, + sequence_parallel_input: bool = True, +) -> tuple[Tensor, Tensor | None]: + del sequence_parallel_input + _prepare_in_proj_trace_token_uids(gdn, hidden_states) return gdn.in_proj(hidden_states) @@ -1433,40 +1841,16 @@ def _gather_bucket_streams( recurrent_g: Tensor, bucket: GdnSegmentBucketPlan, ) -> tuple[Tensor, Tensor, Tensor]: - layout = _bucket_flat_layout( - bucket, - sequence_length=int(qkv.shape[1]), - ) - return _gather_flat_bucket_streams( + return _gather_bucket_streams_compact_fused( qkv.reshape(-1, int(qkv.shape[-1])), beta.reshape(-1, int(beta.shape[-1])), recurrent_g.reshape(-1, int(recurrent_g.shape[-1])), - layout=layout, - length=int(bucket.length), + bucket.row_indices, + bucket.position_indices, + bucket.cu_seqlens, + token_count=int(bucket.real_token_count), segment_count=int(bucket.segment_count), - ) - - -def _bucket_flat_layout( - bucket: GdnSegmentBucketPlan, *, sequence_length: int -) -> _BucketFlatLayout: - positions = bucket.position_indices.clamp_max(sequence_length - 1) - padded_indices = (bucket.row_indices * sequence_length + positions).contiguous() - padded_mask = bucket.real_mask.contiguous() - segment_major_indices = padded_indices.transpose(0, 1).contiguous() - segment_major_mask = padded_mask.transpose(0, 1).contiguous() - real_indices = segment_major_indices[segment_major_mask].contiguous() - output_mask = _bucket_output_mask(bucket).transpose(0, 1).contiguous() - output_indices = segment_major_indices[output_mask].contiguous() - output_selector = None - if bucket.output_mask is not None: - output_selector = output_mask[segment_major_mask].contiguous() - return _BucketFlatLayout( - padded_indices=padded_indices, - padded_mask=padded_mask, - real_indices=real_indices, - output_indices=output_indices, - output_selector=output_selector, + sequence_length=int(qkv.shape[1]), ) @@ -1475,17 +1859,29 @@ def _project_gdn_output( recurrent_output: Tensor, gate: Tensor, plan: GdnRankExecutionPlan, + *, + sequence_parallel_output: bool = True, + reduce_tensor_parallel_output: bool = True, ) -> tuple[Tensor, Tensor | None]: batch_size, seq_len, _, _ = recurrent_output.shape - with _nvtx_range("art_gdn_output_norm_gate", recurrent_output): - norm_out = _apply_gated_rms_norm(gdn, recurrent_output, gate) - norm_out = norm_out.reshape(batch_size, seq_len, _local_value_dim(gdn)) - norm_out = norm_out.transpose(0, 1).contiguous() - with _nvtx_range("art_gdn_out_proj", norm_out): - if plan.cp_size > 1: - out, out_bias = _out_proj_cp_full_shape(gdn, norm_out, plan) - else: - out, out_bias = _out_proj(gdn, norm_out) + token_uids = ( + _replicated_layout_token_uids(plan, "gdn", hidden_states=recurrent_output) + if _trace_token_uids_enabled() + else None + ) + _set_out_norm_trace_token_uids(gdn, token_uids) + norm_out = _apply_gated_rms_norm(gdn, recurrent_output, gate) + norm_out = norm_out.reshape(batch_size, seq_len, _local_value_dim(gdn)) + norm_out = norm_out.transpose(0, 1).contiguous() + if token_uids is not None: + token_uids = _replicated_layout_token_uids(plan, "gdn", hidden_states=norm_out) + _attach_trace_token_uids(norm_out, token_uids) + out, out_bias = _out_proj( + gdn, + norm_out, + sequence_parallel_output=sequence_parallel_output, + reduce_tensor_parallel_output=reduce_tensor_parallel_output, + ) return _mask_gdn_output(gdn, out, plan), out_bias @@ -1505,7 +1901,8 @@ def _mask_gdn_output(gdn: Any, out: Tensor, plan: GdnRankExecutionPlan) -> Tenso full_mask = full_flat.reshape(full_batch, full_seq).transpose(0, 1).unsqueeze(-1) if tuple(full_mask.shape[:2]) == tuple(out.shape[:2]): return out.masked_fill(~full_mask, 0) - rank = _tp_rank(getattr(gdn.out_proj, "linear_proj", gdn.out_proj)) + projection = _gdn_output_projection(gdn) + rank = _tp_rank(projection) if projection is not None else 0 start = rank * int(out.shape[0]) end = start + int(out.shape[0]) if end <= int(full_mask.shape[0]) and int(full_mask.shape[1]) == int(out.shape[1]): @@ -1517,60 +1914,213 @@ def _mask_gdn_output(gdn: Any, out: Tensor, plan: GdnRankExecutionPlan) -> Tenso ) -def _out_proj_cp_full_shape( - gdn: Any, hidden_states: Tensor, plan: GdnRankExecutionPlan -) -> tuple[Tensor, Tensor | None]: - full_batch = int(plan.packed_batch_size or plan.batch_size) - full_seq = int(plan.packed_sequence_length or plan.sequence_length) - full_count = full_batch * full_seq - if full_count == int(hidden_states.shape[0]): - return _out_proj(gdn, hidden_states) - if int(hidden_states.shape[1]) != 1: - raise ValueError( - "CP GDN full-shape output projection expects flattened local batch, got " - f"{tuple(hidden_states.shape)}" - ) - local_indices = torch.tensor( - plan.gdn_token_indices, device=hidden_states.device, dtype=torch.long +def _project_cp_gdn_output( + gdn: Any, + recurrent_output: Tensor, + gate: Tensor, + plan: GdnRankExecutionPlan, + *, + group: Any, + output_layout: Literal["attention", "gdn"], +) -> tuple[Tensor, Tensor | None]: + batch_size, seq_len, _, _ = recurrent_output.shape + token_uids = ( + _replicated_layout_token_uids(plan, "gdn", hidden_states=recurrent_output) + if _trace_token_uids_enabled() + else None ) - if int(local_indices.numel()) != int(hidden_states.shape[0]): - raise ValueError( - "CP GDN token index count must match local projection input, got " - f"{int(local_indices.numel())} indices for {tuple(hidden_states.shape)}" + _set_out_norm_trace_token_uids(gdn, token_uids) + norm_out = _apply_gated_rms_norm(gdn, recurrent_output, gate) + norm_out = norm_out.reshape(batch_size, seq_len, _local_value_dim(gdn)) + norm_out = norm_out.transpose(0, 1).contiguous() + if token_uids is not None: + token_uids = _replicated_layout_token_uids(plan, "gdn", hidden_states=norm_out) + _attach_trace_token_uids(norm_out, token_uids) + if output_layout == "attention": + norm_out = _exchange_cp_sequence_stream( + norm_out, + plan=plan, + group=group, + source_layout="gdn", + dest_layout="attention", ) - if int(local_indices.numel()) and int(local_indices.max().item()) >= full_count: - raise ValueError( - "CP GDN token index exceeds packed output shape, got " - f"max_index={int(local_indices.max().item())} full_count={full_count}" + if token_uids is not None: + token_uids = _replicated_layout_token_uids( + plan, "attention", hidden_states=norm_out + ) + _attach_trace_token_uids(norm_out, token_uids) + norm_out = _pad_sequence_parallel_output_stream(gdn, norm_out) + if token_uids is not None: + token_uids = _pad_trace_token_uids_for_stream(token_uids, norm_out) + _attach_trace_token_uids(norm_out, token_uids) + return _out_proj(gdn, norm_out) + + +def _pad_sequence_parallel_output_stream(gdn: Any, stream: Tensor) -> Tensor: + projection = _gdn_output_projection(gdn) + if projection is None or not _uses_sequence_parallel(projection): + return stream + tp_size = _tp_world_size(projection) + remainder = int(stream.shape[0]) % tp_size + if remainder == 0: + return stream + padding = stream.new_zeros((tp_size - remainder, *stream.shape[1:])) + return torch.cat((stream, padding), dim=0).contiguous() + + +def _exchange_cp_sequence_stream( + stream: Tensor, + *, + plan: GdnRankExecutionPlan, + group: Any, + source_layout: Literal["attention", "gdn"], + dest_layout: Literal["attention", "gdn"], +) -> Tensor: + return ( + _exchange_cp_batch_stream( + stream.transpose(0, 1).contiguous(), + plan=plan, + group=group, + source_layout=source_layout, + dest_layout=dest_layout, ) - full_flat = hidden_states.new_zeros(full_count, int(hidden_states.shape[-1])) - if int(local_indices.numel()): - full_flat = full_flat.index_copy(0, local_indices, hidden_states.squeeze(1)) - full_hidden = ( - full_flat.reshape(full_batch, full_seq, int(hidden_states.shape[-1])) .transpose(0, 1) .contiguous() ) - full_out, out_bias = _out_proj(gdn, full_hidden) - local_out = ( - full_out.transpose(0, 1) - .reshape(full_count, int(full_out.shape[-1])) - .index_select(0, local_indices) - .unsqueeze(1) - .contiguous() + + +def _exchange_cp_batch_stream( + stream: Tensor, + *, + plan: GdnRankExecutionPlan, + group: Any, + source_layout: Literal["attention", "gdn"], + dest_layout: Literal["attention", "gdn"], +) -> Tensor: + from .layout import exchange_rank_tensor_all_to_all + + if source_layout == dest_layout: + return stream + exchange_plan = ( + plan.attention_to_gdn if source_layout == "attention" else plan.gdn_to_attention + ) + backward_plan = ( + plan.gdn_to_attention if source_layout == "attention" else plan.attention_to_gdn ) - return local_out, out_bias + if exchange_plan is None or backward_plan is None: + raise ValueError("CP GDN stream exchange requires prebuilt exchange plans") + source_tokens = ( + int(plan.attention_token_count) + if source_layout == "attention" + else int(plan.gdn_token_count) + ) + dest_tokens = ( + int(plan.attention_token_count) + if dest_layout == "attention" + else int(plan.gdn_token_count) + ) + feature_shape = tuple(stream.shape[2:]) + flat = stream.reshape(-1, *feature_shape) + if int(flat.shape[0]) < source_tokens: + raise ValueError( + "CP GDN stream token count is smaller than the exchange source layout, " + f"got {int(flat.shape[0])} and expected at least {source_tokens}" + ) + exchanged = exchange_rank_tensor_all_to_all( + flat[:source_tokens].contiguous(), + exchange_plan, + rank=plan.cp_rank, + group=group, + backward_plan=backward_plan, + ) + return exchanged.reshape(1, dest_tokens, *feature_shape).contiguous() def _apply_gated_rms_norm(gdn: Any, x: Tensor, gate: Tensor) -> Tensor: + if x.dtype != torch.float32 and int(x.numel()) != 0: + return gdn._apply_gated_norm(x, gate) x_dtype = x.dtype - hidden = gdn.out_norm(x.reshape(-1, int(x.shape[-1]))) + hidden = _apply_explicit_norm( + gdn.out_norm, + x.reshape(-1, int(x.shape[-1])), + config=getattr(gdn, "config", None), + weight_name="weight", + bias_name="bias", + ) gate = gate.reshape(-1, int(gate.shape[-1])) return (hidden * gdn.act_fn(gate.float())).to(x_dtype) -def _out_proj(gdn: Any, hidden_states: Tensor) -> tuple[Tensor, Tensor | None]: - return gdn.out_proj(hidden_states) +def _out_proj( + gdn: Any, + hidden_states: Tensor, + *, + force_explicit: bool = False, + sequence_parallel_output: bool = True, + reduce_tensor_parallel_output: bool = True, +) -> tuple[Tensor, Tensor | None]: + projection = gdn.out_proj + _set_out_proj_trace_token_uids( + gdn, + hidden_states, + sequence_parallel_output=sequence_parallel_output, + ) + if ( + int(hidden_states.numel()) != 0 + and not force_explicit + and reduce_tensor_parallel_output + and hidden_states.dtype != torch.float32 + ): + return projection(hidden_states) + return _explicit_out_proj( + gdn, + hidden_states, + sequence_parallel_output=sequence_parallel_output, + reduce_tensor_parallel_output=reduce_tensor_parallel_output, + ) + + +def _explicit_out_proj( + gdn: Any, + hidden_states: Tensor, + *, + sequence_parallel_output: bool = True, + reduce_tensor_parallel_output: bool = True, +) -> tuple[Tensor, Tensor | None]: + projection = gdn.out_proj + base_projection = getattr(projection, "linear_proj", projection) + bias = _linear_bias(base_projection) + out = _stable_fp32_linear(hidden_states, base_projection.weight, None) + if reduce_tensor_parallel_output: + out = _row_parallel_output( + out, base_projection, sequence_parallel_output=sequence_parallel_output + ) + if bias is not None and not _returns_bias(base_projection): + out = out + bias + if hasattr(projection, "lora"): + _set_out_proj_lora_trace_token_uids(gdn, hidden_states) + lora_output = projection.lora(hidden_states) + if reduce_tensor_parallel_output and bool( + getattr(projection, "reduce_output", True) + ): + lora_output = _row_parallel_output( + lora_output, + base_projection, + sequence_parallel_output=sequence_parallel_output, + ) + out = out + lora_output + return out, bias if _returns_bias(base_projection) else None + + +def _stable_fp32_linear(x: Tensor, weight: Tensor, bias: Tensor | None) -> Tensor: + if x.dtype != torch.float32: + return F.linear(x, weight, bias) + out = F.linear( + x.to(dtype=torch.float64), + weight.to(dtype=torch.float64), + None if bias is None else bias.to(dtype=torch.float64), + ) + return out.to(dtype=torch.float32) def _apply_explicit_norm( @@ -1581,57 +2131,132 @@ def _apply_explicit_norm( weight_name: str, bias_name: str, ) -> Tensor: - del config + weight = getattr(module, weight_name, None) + if not isinstance(weight, Tensor): + return x x_dtype = x.dtype x_float = x.float() - normalization = str(module.normalization) + eps = float(getattr(module, "eps", getattr(config, "layernorm_epsilon", 1e-5))) + normalization = getattr(module, "normalization", None) + if normalization is None and config is not None: + normalization = getattr(config, "normalization", None) + if normalization is None: + module_name = type(module).__name__ + normalization = "LayerNorm" if module_name == "LayerNorm" else "RMSNorm" + normalization = str(normalization) if normalization == "RMSNorm": normed = x_float * torch.rsqrt( - x_float.square().mean(dim=-1, keepdim=True) + float(module.eps) + x_float.square().mean(dim=-1, keepdim=True) + eps ) - bias = None elif normalization == "LayerNorm": centered = x_float - x_float.mean(dim=-1, keepdim=True) normed = centered * torch.rsqrt( - centered.square().mean(dim=-1, keepdim=True) + float(module.eps) + centered.square().mean(dim=-1, keepdim=True) + eps ) - bias = getattr(module, bias_name) else: raise ValueError(f"unsupported GDN normalization '{normalization}'") - - scale = getattr(module, weight_name).float() - if bool(module.zero_centered_gamma): + scale = weight.float() + if bool(getattr(module, "zero_centered_gamma", False)): scale = scale + 1.0 normed = normed * scale + bias = getattr(module, bias_name, None) if isinstance(bias, Tensor): normed = normed + bias.float() return normed.to(dtype=x_dtype) +def _gdn_uses_sequence_parallel(gdn: Any | None) -> bool: + return any( + projection is not None and _uses_sequence_parallel(projection) + for projection in (_gdn_input_projection(gdn), _gdn_output_projection(gdn)) + ) + + +def _gdn_input_projection(gdn: Any | None) -> Any | None: + if gdn is None: + return None + projection = getattr(gdn, "in_proj", None) + if projection is None: + return None + return getattr(projection, "in_proj", projection) + + +def _gdn_output_projection(gdn: Any | None) -> Any | None: + if gdn is None: + return None + projection = getattr(gdn, "out_proj", None) + if projection is None: + return None + return getattr(projection, "linear_proj", projection) + + +def _column_parallel_input(x: Tensor, projection: Any) -> Tensor: + if not _uses_sequence_parallel(projection): + return x + from megatron.core.tensor_parallel.mappings import ( + gather_from_sequence_parallel_region, + ) + + return gather_from_sequence_parallel_region(x, group=_tp_group(projection)) + + +def _row_parallel_output( + x: Tensor, projection: Any, *, sequence_parallel_output: bool = True +) -> Tensor: + if _tp_world_size(projection) <= 1: + return x + if _uses_sequence_parallel(projection) and sequence_parallel_output: + from megatron.core.tensor_parallel.mappings import ( + reduce_scatter_to_sequence_parallel_region, + ) + + return reduce_scatter_to_sequence_parallel_region( + x, group=_tp_group(projection) + ) + from megatron.core.tensor_parallel.mappings import ( + reduce_from_tensor_model_parallel_region, + ) + + return reduce_from_tensor_model_parallel_region(x, group=_tp_group(projection)) + + def _uses_sequence_parallel(projection: Any) -> bool: return bool(getattr(projection, "sequence_parallel", False)) and ( _tp_world_size(projection) > 1 ) -def _gdn_uses_sequence_parallel(gdn: Any) -> bool: - projection = getattr(gdn, "in_proj", None) - base_projection = getattr(projection, "in_proj", projection) - return _uses_sequence_parallel(base_projection) +def _tp_world_size(projection: Any) -> int: + group = _tp_group(projection) + if group is not None and dist.is_initialized(): # ty: ignore[possibly-missing-attribute] + return int(dist.get_world_size(group)) # ty: ignore[possibly-missing-attribute] + return int(getattr(projection, "tp_size", 1)) -def _tp_world_size(projection: Any) -> int: - del projection - from megatron.core import parallel_state as ps +def _tp_rank(projection: Any) -> int: + group = _tp_group(projection) + if group is not None and dist.is_initialized(): # ty: ignore[possibly-missing-attribute] + return int(dist.get_rank(group)) # ty: ignore[possibly-missing-attribute] + for name in ("tp_rank", "tensor_model_parallel_rank"): + value = getattr(projection, name, None) + if isinstance(value, int): + return value + return 0 - return int(ps.get_tensor_model_parallel_world_size()) +def _tp_group(projection: Any) -> Any | None: + return getattr(projection, "_tp_group", getattr(projection, "tp_group", None)) -def _tp_rank(projection: Any) -> int: - del projection - from megatron.core import parallel_state as ps - return int(ps.get_tensor_model_parallel_rank()) +def _linear_bias(projection: Any) -> Tensor | None: + bias = getattr(projection, "bias", None) + if not isinstance(bias, Tensor) or int(bias.numel()) == 0: + return None + return bias + + +def _returns_bias(projection: Any) -> bool: + return bool(getattr(projection, "te_return_bias", False)) def _local_key_heads(gdn: Any) -> int: @@ -1723,6 +2348,40 @@ def _exchange_parent_state_rows( return conv_table, rec_table, _make_autograd_dependency(conv_table, rec_table) +def _exchange_remote_prefix_tail_streams( + qkv: Tensor, + beta: Tensor, + recurrent_g: Tensor, + *, + plan: GdnRankExecutionPlan, + group: Any, +) -> tuple[Tensor, Tensor, Tensor]: + from .layout import exchange_rank_tensor_all_to_all + + if plan.remote_prefix_tail_exchange is None: + return ( + qkv.new_empty((0, int(qkv.shape[-1]))), + beta.new_empty((0, int(beta.shape[-1]))), + recurrent_g.new_empty((0, int(recurrent_g.shape[-1]))), + ) + if plan.remote_prefix_tail_backward_exchange is None: + raise ValueError("remote prefix-tail exchange requires a backward plan") + qkv_flat = qkv.reshape(-1, int(qkv.shape[-1])) + beta_flat = beta.reshape(-1, int(beta.shape[-1])) + g_flat = recurrent_g.reshape(-1, int(recurrent_g.shape[-1])) + kwargs = { + "plan": plan.remote_prefix_tail_exchange, + "rank": plan.cp_rank, + "group": group, + "backward_plan": plan.remote_prefix_tail_backward_exchange, + } + return ( + exchange_rank_tensor_all_to_all(qkv_flat, **kwargs), + exchange_rank_tensor_all_to_all(beta_flat, **kwargs), + exchange_rank_tensor_all_to_all(g_flat, **kwargs), + ) + + class _ParentStateExchange(torch.autograd.Function): @staticmethod def forward( @@ -1893,129 +2552,37 @@ def _parent_state_index_tensor( return torch.tensor(transfer.family_indices, device=device, dtype=torch.long) -def _run_gdn_segment( - gdn: Any, - hidden_states: Tensor, +def run_gdn_bucket( + bucket: GdnSegmentBucketPlan, + projected_streams: tuple[Tensor, Tensor, Tensor], + parent_states: tuple[Tensor, Tensor], *, - conv_initial: Tensor, - recurrent_initial: Tensor, + gdn: Any, + group: Any | None = None, + recurrent_cp: bool = False, output_final_state: bool = True, -) -> tuple[Tensor, Tensor | None, Tensor | None, Tensor | None]: +) -> tuple[Tensor, Tensor | None, Tensor | None]: _disable_reentrant_te_linear_transpose_cache(gdn) - seq_len, batch_size, _ = hidden_states.shape - if int(conv_initial.shape[0]) != batch_size: + qkv, beta, recurrent_g = projected_streams + conv_initial, recurrent_initial = parent_states + token_count = int(qkv.shape[0]) if qkv.ndim == 2 else -1 + batch_size = int(bucket.segment_count) + if qkv.ndim != 2: raise ValueError( - "conv_initial batch must match hidden_states batch, got " - f"{tuple(conv_initial.shape)} for hidden {tuple(hidden_states.shape)}" + "GDN bucket execution requires compact projected streams; " + f"got qkv shape {tuple(qkv.shape)}" ) - if int(recurrent_initial.shape[0]) != batch_size: + if token_count != int(bucket.real_token_count): raise ValueError( - "recurrent_initial batch must match hidden_states batch, got " - f"{tuple(recurrent_initial.shape)} for hidden {tuple(hidden_states.shape)}" - ) - - with _nvtx_range("art_gdn_in_proj", hidden_states): - qkvzba, _ = _in_proj(gdn, hidden_states) - qkvzba = qkvzba.transpose(0, 1) - - with _nvtx_range("art_gdn_qkv_gate_beta_alpha_split_reshape", qkvzba): - qkv, gate, beta, alpha = torch.split( - qkvzba, - [ - (gdn.qk_dim * 2 + gdn.v_dim) // gdn.tp_size, - gdn.v_dim // gdn.tp_size, - gdn.num_value_heads // gdn.tp_size, - gdn.num_value_heads // gdn.tp_size, - ], - dim=-1, - ) - key_heads = _local_key_heads(gdn) - value_heads = _local_value_heads(gdn) - gate = gate.reshape(batch_size, seq_len, value_heads, gdn.value_head_dim) - beta = beta.reshape(batch_size, seq_len, value_heads) - alpha = alpha.reshape(batch_size, seq_len, value_heads) - - with _nvtx_range("art_gdn_causal_conv_forward", qkv): - qkv = qkv.transpose(1, 2) - qkv, conv_final = _causal_conv1d_with_state( - gdn, - qkv, - conv_initial, - output_final_state=output_final_state, - ) - qkv = qkv.transpose(1, 2) - - with _nvtx_range("art_gdn_qkv_head_prepare", qkv): - query, key, value = torch.split( - qkv, - [ - gdn.qk_dim // gdn.tp_size, - gdn.qk_dim // gdn.tp_size, - gdn.v_dim // gdn.tp_size, - ], - dim=-1, - ) - query = query.reshape(batch_size, seq_len, key_heads, gdn.key_head_dim) - key = key.reshape(batch_size, seq_len, key_heads, gdn.key_head_dim) - value = value.reshape(batch_size, seq_len, value_heads, gdn.value_head_dim) - if gdn.use_qk_l2norm: - query = _l2norm(query.contiguous()) - key = _l2norm(key.contiguous()) - if gdn.num_value_heads // gdn.num_key_heads > 1: - repeat = gdn.num_value_heads // gdn.num_key_heads - query = query.repeat_interleave(repeat, dim=2) - key = key.repeat_interleave(repeat, dim=2) - - query = query.contiguous() - key = key.contiguous() - value = value.contiguous() - gate = gate.contiguous() - beta = beta.contiguous() - alpha = alpha.contiguous() - - with _nvtx_range("art_gdn_recurrent_gate_prepare", alpha): - g = -gdn.A_log.exp() * F.softplus(alpha.float() + gdn.dt_bias) - beta = beta.sigmoid() - - with _nvtx_range("art_gdn_recurrent_forward", query): - recurrent_out, recurrent_final = _chunk_gated_delta_rule( - query, - key, - value, - g=g, - beta=beta, - initial_state=recurrent_initial, - output_final_state=output_final_state, - use_qk_l2norm_in_kernel=False, + "GDN packed varlen token count mismatch, got " + f"qkv={tuple(qkv.shape)} and bucket tokens={bucket.real_token_count}" ) - - with _nvtx_range("art_gdn_output_norm_gate", recurrent_out): - norm_out = _apply_gated_rms_norm(gdn, recurrent_out, gate) - norm_out = norm_out.reshape(batch_size, seq_len, _local_value_dim(gdn)) - norm_out = norm_out.transpose(0, 1).contiguous() - with _nvtx_range("art_gdn_out_proj", norm_out): - out, out_bias = _out_proj(gdn, norm_out) - return out, out_bias, conv_final, recurrent_final - - -def _run_gdn_prepared_varlen_batch( - gdn: Any, - qkv: Tensor, - *, - beta: Tensor, - recurrent_g: Tensor, - bucket: GdnSegmentBucketPlan, - conv_initial: Tensor, - recurrent_initial: Tensor, - output_final_state: bool = True, -) -> tuple[Tensor, Tensor | None, Tensor | None]: - _disable_reentrant_te_linear_transpose_cache(gdn) - batch_size, _, max_len = qkv.shape - if int(bucket.length) != max_len or int(bucket.segment_count) != batch_size: + if tuple(beta.shape[:1]) != (token_count,) or tuple(recurrent_g.shape) != tuple( + beta.shape + ): raise ValueError( - "GDN prepared varlen bucket shape mismatch, got " - f"qkv={tuple(qkv.shape)} bucket_len={bucket.length} " - f"segments={bucket.segment_count}" + "packed beta/recurrent_g must be [tokens, heads], got " + f"{tuple(beta.shape)} and {tuple(recurrent_g.shape)}" ) if int(conv_initial.shape[0]) != batch_size: raise ValueError( @@ -2028,241 +2595,207 @@ def _run_gdn_prepared_varlen_batch( f"{tuple(recurrent_initial.shape)} for {batch_size} segments" ) - with _nvtx_range("art_gdn_causal_conv_forward", qkv): - qkv, conv_final = _causal_conv1d_varlen_with_state( - gdn, + conv_output_final_state = output_final_state + chain_conv_final: Tensor | None = None + if recurrent_cp: + conv_initial, chain_conv_final = _chain_conv_initial_and_final( qkv, + bucket.cu_seqlens_cpu, + bucket.lengths_by_rank_cpu, conv_initial, - bucket.lengths, + group=group, output_final_state=output_final_state, ) - qkv = qkv.transpose(1, 2) + conv_output_final_state = False - with _nvtx_range("art_gdn_qkv_head_prepare", qkv): - query, key, value = torch.split( - qkv, - [ - gdn.qk_dim // gdn.tp_size, - gdn.qk_dim // gdn.tp_size, - gdn.v_dim // gdn.tp_size, - ], - dim=-1, - ) - key_heads = _local_key_heads(gdn) - value_heads = _local_value_heads(gdn) - query = query.reshape(batch_size, max_len, key_heads, gdn.key_head_dim) - key = key.reshape(batch_size, max_len, key_heads, gdn.key_head_dim) - value = value.reshape(batch_size, max_len, value_heads, gdn.value_head_dim) - if gdn.use_qk_l2norm: - query = _l2norm(query.contiguous()) - key = _l2norm(key.contiguous()) - if gdn.num_value_heads // gdn.num_key_heads > 1: - repeat = gdn.num_value_heads // gdn.num_key_heads - query = query.repeat_interleave(repeat, dim=2) - key = key.repeat_interleave(repeat, dim=2) - - real_mask = bucket.real_mask.transpose(0, 1) - query = query[real_mask].unsqueeze(0).contiguous() - key = key[real_mask].unsqueeze(0).contiguous() - value = value[real_mask].unsqueeze(0).contiguous() - beta = beta[real_mask].unsqueeze(0).contiguous() - recurrent_g = recurrent_g[real_mask].unsqueeze(0).contiguous() - - with _nvtx_range("art_gdn_recurrent_forward", query): - recurrent_out, recurrent_final = _chunk_gated_delta_rule( + qkv, conv_final = _causal_conv1d_packed_varlen_with_state( + gdn, + qkv, + conv_initial, + bucket.cu_seqlens, + output_final_state=conv_output_final_state, + ) + if recurrent_cp: + conv_final = chain_conv_final + + query, key, value, beta, recurrent_g = _prepare_packed_recurrent_inputs_fused( + qkv, + beta, + recurrent_g, + key_heads=_local_key_heads(gdn), + value_heads=_local_value_heads(gdn), + key_dim=int(gdn.key_head_dim), + value_dim=int(gdn.value_head_dim), + ) + if gdn.use_qk_l2norm: + query = _l2norm(query.contiguous()) + key = _l2norm(key.contiguous()) + + if recurrent_cp: + if group is None: + raise ValueError("CP recurrent GDN bucket requires a process group") + recurrent_out, recurrent_final = chunk_gated_delta_rule_native_cp( query, key, value, g=recurrent_g, beta=beta, initial_state=recurrent_initial, + group=group, output_final_state=output_final_state, - use_qk_l2norm_in_kernel=False, cu_seqlens=bucket.cu_seqlens, + cu_seqlens_cpu=bucket.cu_seqlens_cpu, + lengths_by_rank_cpu=bucket.lengths_by_rank_cpu, ) - return recurrent_out, conv_final, recurrent_final - - -def _run_gdn_varlen_batch( - gdn: Any, - hidden_states: Tensor, - *, - bucket: GdnSegmentBucketPlan, - conv_initial: Tensor, - recurrent_initial: Tensor, - output_final_state: bool = True, -) -> tuple[Tensor, Tensor | None, Tensor | None, Tensor | None]: - _disable_reentrant_te_linear_transpose_cache(gdn) - max_len, batch_size, _ = hidden_states.shape - if int(bucket.length) != max_len or int(bucket.segment_count) != batch_size: - raise ValueError( - "GDN varlen bucket shape mismatch, got " - f"hidden={tuple(hidden_states.shape)} bucket_len={bucket.length} " - f"segments={bucket.segment_count}" - ) - if int(conv_initial.shape[0]) != batch_size: - raise ValueError( - "conv_initial batch must match bucket segment count, got " - f"{tuple(conv_initial.shape)} for {batch_size} segments" - ) - if int(recurrent_initial.shape[0]) != batch_size: - raise ValueError( - "recurrent_initial batch must match bucket segment count, got " - f"{tuple(recurrent_initial.shape)} for {batch_size} segments" - ) - - with _nvtx_range("art_gdn_in_proj", hidden_states): - qkvzba, _ = _in_proj(gdn, hidden_states) - qkvzba = qkvzba.transpose(0, 1) - - with _nvtx_range("art_gdn_qkv_gate_beta_alpha_split_reshape", qkvzba): - qkv, gate, beta, alpha = torch.split( - qkvzba, - [ - (gdn.qk_dim * 2 + gdn.v_dim) // gdn.tp_size, - gdn.v_dim // gdn.tp_size, - gdn.num_value_heads // gdn.tp_size, - gdn.num_value_heads // gdn.tp_size, - ], - dim=-1, - ) - key_heads = _local_key_heads(gdn) - value_heads = _local_value_heads(gdn) - gate = gate.reshape(batch_size, max_len, value_heads, gdn.value_head_dim) - beta = beta.reshape(batch_size, max_len, value_heads) - alpha = alpha.reshape(batch_size, max_len, value_heads) - - with _nvtx_range("art_gdn_causal_conv_forward", qkv): - qkv = qkv.transpose(1, 2).contiguous() - qkv, conv_final = _causal_conv1d_varlen_with_state( - gdn, - qkv, - conv_initial, - bucket.lengths, - output_final_state=output_final_state, - ) - qkv = qkv.transpose(1, 2) - - with _nvtx_range("art_gdn_qkv_head_prepare", qkv): - query, key, value = torch.split( - qkv, - [ - gdn.qk_dim // gdn.tp_size, - gdn.qk_dim // gdn.tp_size, - gdn.v_dim // gdn.tp_size, - ], - dim=-1, - ) - query = query.reshape(batch_size, max_len, key_heads, gdn.key_head_dim) - key = key.reshape(batch_size, max_len, key_heads, gdn.key_head_dim) - value = value.reshape(batch_size, max_len, value_heads, gdn.value_head_dim) - if gdn.use_qk_l2norm: - query = _l2norm(query.contiguous()) - key = _l2norm(key.contiguous()) - if gdn.num_value_heads // gdn.num_key_heads > 1: - repeat = gdn.num_value_heads // gdn.num_key_heads - query = query.repeat_interleave(repeat, dim=2) - key = key.repeat_interleave(repeat, dim=2) - - with _nvtx_range("art_gdn_recurrent_gate_prepare", alpha): - g = -gdn.A_log.exp() * F.softplus(alpha.float() + gdn.dt_bias) - beta = beta.sigmoid() - - real_mask = bucket.real_mask.transpose(0, 1) - query = query[real_mask].unsqueeze(0).contiguous() - key = key[real_mask].unsqueeze(0).contiguous() - value = value[real_mask].unsqueeze(0).contiguous() - gate = gate[real_mask].unsqueeze(0).contiguous() - beta = beta[real_mask].unsqueeze(0).contiguous() - g = g[real_mask].unsqueeze(0).contiguous() - - with _nvtx_range("art_gdn_recurrent_forward", query): + else: recurrent_out, recurrent_final = _chunk_gated_delta_rule( query, key, value, - g=g, + g=recurrent_g, beta=beta, initial_state=recurrent_initial, output_final_state=output_final_state, use_qk_l2norm_in_kernel=False, cu_seqlens=bucket.cu_seqlens, ) - - with _nvtx_range("art_gdn_output_norm_gate", recurrent_out): - norm_out = _apply_gated_rms_norm(gdn, recurrent_out, gate) - if norm_out.ndim == 4: - norm_out = norm_out.flatten(2).transpose(0, 1).contiguous() - elif norm_out.ndim == 3: - norm_out = ( - norm_out.transpose(0, 1).contiguous() - if int(norm_out.shape[0]) == 1 - else norm_out.reshape( - norm_out.shape[0], 1, _local_value_dim(gdn) - ).contiguous() - ) - elif norm_out.ndim == 2: - norm_out = norm_out.reshape( - 1, recurrent_out.shape[1], _local_value_dim(gdn) - ) - norm_out = norm_out.transpose(0, 1).contiguous() - else: - raise RuntimeError( - f"unexpected GDN norm output shape {tuple(norm_out.shape)}" - ) - with _nvtx_range("art_gdn_out_proj", norm_out): - out, out_bias = _out_proj(gdn, norm_out) - return out, out_bias, conv_final, recurrent_final + return recurrent_out, conv_final, recurrent_final -def _conv_final_from_varlen_qkv( - qkv: Tensor, conv_initial: Tensor, lengths: Tensor -) -> Tensor: - tail_width = int(conv_initial.shape[-1]) - if tail_width == 0: - return conv_initial - batch_size, channel_count, max_len = qkv.shape - arange = torch.arange(batch_size, device=qkv.device) - pieces = [] - for tail_offset in range(tail_width): - source = lengths - tail_width + tail_offset - from_qkv = source >= 0 - qkv_index = source.clamp(min=0, max=max_len - 1) - init_index = (source + tail_width).clamp(min=0, max=tail_width - 1) - qkv_piece = qkv[arange, :, qkv_index] - init_piece = conv_initial[arange, :, init_index] - pieces.append(torch.where(from_qkv.unsqueeze(1), qkv_piece, init_piece)) - return torch.stack(pieces, dim=-1).reshape(batch_size, channel_count, tail_width) - - -def _causal_conv1d_varlen_with_state( - gdn: Any, +def _chain_conv_initial_and_final( qkv: Tensor, - conv_initial: Tensor, - lengths: Tensor, + cu_seqlens_cpu: Tensor, + lengths_by_rank_cpu: Tensor | None, + parent_initial: Tensor, *, + group: Any, output_final_state: bool, ) -> tuple[Tensor, Tensor | None]: - if str(getattr(gdn, "activation", "")) == "gelu": - return gdn_varlen_causal_conv_gelu( - gdn, - qkv, - conv_initial, - lengths, - output_final_state=output_final_state, - ) + if group is None: + raise ValueError("CP chain conv state requires a process group") + if not dist.is_available() or not dist.is_initialized(): # ty: ignore[possibly-missing-attribute] + raise RuntimeError("torch.distributed must be initialized for CP chain conv") + parent_initial = _AllReduceGradient.apply(parent_initial, group) + tail_width = int(parent_initial.shape[-1]) + if tail_width <= 0: + return parent_initial, parent_initial if output_final_state else None + if lengths_by_rank_cpu is None: + raise ValueError("CP chain conv requires static all-rank bucket lengths") + if cu_seqlens_cpu.device.type != "cpu" or lengths_by_rank_cpu.device.type != "cpu": + raise ValueError("CP chain conv metadata must stay on CPU") + local_tail = _local_packed_conv_tail(qkv, cu_seqlens_cpu, tail_width) + gathered_tails = _AllGatherReplicated.apply(local_tail, group) + rank = dist.get_rank(group) # ty: ignore[possibly-missing-attribute] + conv_initial = _scan_conv_tail_batch( + parent_initial, + gathered_tails, + lengths_by_rank_cpu.clamp(max=tail_width), + stop_rank=rank, + ) + conv_initial = _add_autograd_dependency( + conv_initial, gathered_tails.reshape(-1)[:1].sum() * 0 + ) conv_final = ( - _conv_final_from_varlen_qkv(qkv, conv_initial, lengths) + _scan_conv_tail_batch( + parent_initial, + gathered_tails, + lengths_by_rank_cpu.clamp(max=tail_width), + stop_rank=dist.get_world_size(group), # ty: ignore[possibly-missing-attribute] + ) if output_final_state else None ) - out, _ = _causal_conv1d_with_state( - gdn, - qkv, - conv_initial, - output_final_state=False, - ) - return out, conv_final + return conv_initial, conv_final + + +def _local_packed_conv_tail( + qkv: Tensor, cu_seqlens_cpu: Tensor, tail_width: int +) -> Tensor: + segment_count = int(cu_seqlens_cpu.numel()) - 1 + channels = int(qkv.shape[1]) + tails = qkv.new_zeros(segment_count, channels, tail_width) + lengths = cu_seqlens_cpu[1:] - cu_seqlens_cpu[:-1] + valid_lengths = torch.clamp(lengths, max=tail_width).tolist() + ends = cu_seqlens_cpu[1:].tolist() + for segment, valid in enumerate(valid_lengths): + valid = int(valid) + if valid <= 0: + continue + end = int(ends[segment]) + tails[segment, :, :valid] = qkv[end - valid : end].transpose(0, 1) + return tails + + +def _scan_conv_tail_batch( + parent_initial: Tensor, + tails_by_rank: Tensor, + lengths_by_rank_cpu: Tensor, + *, + stop_rank: int, +) -> Tensor: + states = [] + tail_width = int(parent_initial.shape[-1]) + host_lengths = lengths_by_rank_cpu.tolist() + for segment in range(int(parent_initial.shape[0])): + state = parent_initial[segment] + for peer in range(int(stop_rank)): + valid = int(host_lengths[peer][segment]) + if valid <= 0: + continue + state = torch.cat([state, tails_by_rank[peer, segment, :, :valid]], dim=-1)[ + :, -tail_width: + ] + states.append(state) + return torch.stack(states, dim=0) + + +class _AllGatherReplicated(torch.autograd.Function): + @staticmethod + def forward(ctx: Any, local_tensor: Tensor, group: Any) -> Tensor: + ctx.group = group + ctx.rank = dist.get_rank(group) # ty: ignore[possibly-missing-attribute] + gathered = torch.empty( + dist.get_world_size(group), # ty: ignore[possibly-missing-attribute] + *local_tensor.shape, + device=local_tensor.device, + dtype=local_tensor.dtype, + ) + dist.all_gather_into_tensor( # ty: ignore[possibly-missing-attribute] + gathered, + local_tensor.contiguous(), + group=group, + ) + return gathered + + @staticmethod + def backward(ctx: Any, *grad_outputs: Tensor) -> tuple[Tensor, None]: + (grad_output,) = grad_outputs + grad_input = torch.empty_like(grad_output[ctx.rank]) + dist.reduce_scatter_tensor( # ty: ignore[possibly-missing-attribute] + grad_input, + grad_output.contiguous(), + op=dist.ReduceOp.SUM, # ty: ignore[possibly-missing-attribute] + group=ctx.group, + ) + return grad_input, None + + +class _AllReduceGradient(torch.autograd.Function): + @staticmethod + def forward(ctx: Any, tensor: Tensor, group: Any) -> Tensor: + ctx.group = group + return tensor + + @staticmethod + def backward(ctx: Any, *grad_outputs: Tensor) -> tuple[Tensor, None]: + (grad_output,) = grad_outputs + grad_input = grad_output.contiguous() + dist.all_reduce( # ty: ignore[possibly-missing-attribute] + grad_input, + op=dist.ReduceOp.SUM, # ty: ignore[possibly-missing-attribute] + group=ctx.group, + ) + return grad_input, None def _causal_conv1d_packed_varlen_with_state( @@ -2273,112 +2806,19 @@ def _causal_conv1d_packed_varlen_with_state( *, output_final_state: bool, ) -> tuple[Tensor, Tensor | None]: + weight = gdn.conv1d.weight.squeeze(1) + bias = gdn.conv1d.bias return packed_varlen_causal_conv( qkv, cu_seqlens, conv_initial, - gdn.conv1d.weight.squeeze(1), - gdn.conv1d.bias, + weight, + bias, activation=str(getattr(gdn, "activation", "gelu")), output_final_state=output_final_state, ) -def _causal_conv1d_with_state( - gdn: Any, - qkv: Tensor, - conv_initial: Tensor, - *, - output_final_state: bool, -) -> tuple[Tensor, Tensor | None]: - weight = gdn.conv1d.weight.squeeze(1) - bias = gdn.conv1d.bias - if not bool( - getattr(gdn.config, "deterministic_mode", False) - ) and gdn.activation in ("silu", "swish"): - qkv_fast = _channel_last_conv1d_layout(qkv) - conv_initial_fast = _channel_last_conv1d_layout(conv_initial) - if qkv_fast is not None and conv_initial_fast is not None: - conv_result = causal_conv1d_fn( - x=qkv_fast, - weight=weight, - bias=bias, - initial_states=conv_initial_fast, - return_final_states=output_final_state, - activation=gdn.activation, - ) - if output_final_state: - out, final = conv_result - else: - out, final = conv_result, None - return out, final - - qkv_dtype = qkv.dtype - if not bool(getattr(gdn.config, "deterministic_mode", False)): - final = ( - _conv_final_from_dense_qkv(qkv, conv_initial, weight.shape[1]) - if output_final_state - else None - ) - qkv_fast = _channel_last_conv1d_layout(qkv) - conv_initial_fast = _channel_last_conv1d_layout(conv_initial) - if qkv_fast is not None and conv_initial_fast is not None: - out = causal_conv1d_fn( - x=qkv_fast, - weight=weight, - bias=bias, - initial_states=conv_initial_fast, - return_final_states=False, - activation=None, - ) - out = gdn.act_fn(out).to(dtype=qkv_dtype) - return out, final - - extended = torch.cat([conv_initial, qkv], dim=-1) - out = F.conv1d( - extended, weight.unsqueeze(1), bias, padding=0, groups=extended.shape[1] - ) - out = out[..., : qkv.shape[-1]] - out = gdn.act_fn(out).to(dtype=qkv_dtype) - final = ( - extended[..., -(weight.shape[1] - 1) :].to(dtype=qkv_dtype) - if output_final_state - else None - ) - return out, final - - -def _conv_final_from_dense_qkv( - qkv: Tensor, conv_initial: Tensor, kernel_width: int -) -> Tensor: - tail_width = int(kernel_width) - 1 - if tail_width <= 0: - return conv_initial[..., :0].to(dtype=qkv.dtype) - if int(qkv.shape[-1]) >= tail_width: - return qkv[..., -tail_width:].to(dtype=qkv.dtype) - initial_width = tail_width - int(qkv.shape[-1]) - return torch.cat([conv_initial[..., -initial_width:], qkv], dim=-1).to( - dtype=qkv.dtype - ) - - -def _channel_last_conv1d_layout(tensor: Tensor) -> Tensor | None: - if _causal_conv1d_layout_supported(tensor): - return tensor - channel_last = tensor.transpose(1, 2).contiguous().transpose(1, 2) - if _causal_conv1d_layout_supported(channel_last): - return channel_last - return None - - -def _causal_conv1d_layout_supported(tensor: Tensor) -> bool: - return ( - int(tensor.shape[-1]) >= 8 - and int(tensor.stride(1)) == 1 - and all(int(tensor.stride(dim)) % 8 == 0 for dim in (0, 2)) - ) - - def _disable_reentrant_te_linear_transpose_cache(gdn: Any) -> None: if getattr(gdn, "_art_reentrant_te_linear_transpose_cache_disabled", False): return @@ -2393,85 +2833,6 @@ def _disable_reentrant_te_linear_transpose_cache(gdn: Any) -> None: gdn._art_reentrant_te_linear_transpose_cache_disabled = True -def run_gdn_bucket( - bucket: GdnSegmentBucketPlan, - projected_streams: tuple[Tensor, Tensor, Tensor], - parent_states: tuple[Tensor, Tensor], - *, - gdn: Any, - output_final_state: bool = True, -) -> tuple[Tensor, Tensor | None, Tensor | None]: - _disable_reentrant_te_linear_transpose_cache(gdn) - qkv, beta, recurrent_g = projected_streams - conv_initial, recurrent_initial = parent_states - token_count = int(qkv.shape[0]) if qkv.ndim == 2 else -1 - segment_count = int(bucket.segment_count) - if qkv.ndim != 2: - raise ValueError( - "GDN bucket execution requires compact projected streams; " - f"got qkv shape {tuple(qkv.shape)}" - ) - if token_count != int(bucket.real_token_count): - raise ValueError( - "GDN packed varlen token count mismatch, got " - f"qkv={tuple(qkv.shape)} and bucket tokens={bucket.real_token_count}" - ) - if tuple(beta.shape[:1]) != (token_count,) or tuple(recurrent_g.shape) != tuple( - beta.shape - ): - raise ValueError( - "packed beta/recurrent_g must be [tokens, heads], got " - f"{tuple(beta.shape)} and {tuple(recurrent_g.shape)}" - ) - if int(conv_initial.shape[0]) != segment_count: - raise ValueError( - "conv_initial batch must match bucket segment count, got " - f"{tuple(conv_initial.shape)} for {segment_count} segments" - ) - if int(recurrent_initial.shape[0]) != segment_count: - raise ValueError( - "recurrent_initial batch must match bucket segment count, got " - f"{tuple(recurrent_initial.shape)} for {segment_count} segments" - ) - - with _nvtx_range("art_gdn_causal_conv_forward", qkv): - qkv, conv_final = _causal_conv1d_packed_varlen_with_state( - gdn, - qkv, - conv_initial, - bucket.cu_seqlens, - output_final_state=output_final_state, - ) - - with _nvtx_range("art_gdn_qkv_head_prepare", qkv): - query, key, value, beta, recurrent_g = _prepare_packed_recurrent_inputs_fused( - qkv, - beta, - recurrent_g, - key_heads=_local_key_heads(gdn), - value_heads=_local_value_heads(gdn), - key_dim=int(gdn.key_head_dim), - value_dim=int(gdn.value_head_dim), - ) - if gdn.use_qk_l2norm: - query = l2norm(query.contiguous()) - key = l2norm(key.contiguous()) - - with _nvtx_range("art_gdn_recurrent_forward", query): - recurrent_out, recurrent_final = _chunk_gated_delta_rule( - query, - key, - value, - g=recurrent_g, - beta=beta, - initial_state=recurrent_initial, - output_final_state=output_final_state, - use_qk_l2norm_in_kernel=False, - cu_seqlens=bucket.cu_seqlens, - ) - return recurrent_out, conv_final, recurrent_final - - def _zero_conv_state( gdn: Any, hidden_states: Tensor, @@ -2505,49 +2866,73 @@ def _zero_recurrent_state( def _default_cp_rank(cp_size: int) -> int: - del cp_size - from megatron.core import parallel_state as ps + if cp_size == 1: + return 0 + try: + from megatron.core import parallel_state as ps - return int(ps.get_context_parallel_rank()) + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + return int(ps.get_context_parallel_rank()) + except Exception: + pass + if torch.distributed.is_available() and torch.distributed.is_initialized(): # ty: ignore[possibly-missing-attribute] + return int(torch.distributed.get_rank()) # ty: ignore[possibly-missing-attribute] + return 0 -def _default_cp_size() -> int: - from megatron.core import parallel_state as ps +def _default_cp_group(cp_size: int) -> Any: + if cp_size == 1: + return None + try: + from megatron.core import parallel_state as ps - return max(1, int(ps.get_context_parallel_world_size())) + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + return ps.get_context_parallel_group() + except Exception: + pass + if torch.distributed.is_available() and torch.distributed.is_initialized(): # ty: ignore[possibly-missing-attribute] + return torch.distributed.group.WORLD # ty: ignore[possibly-missing-attribute] + raise RuntimeError("CP GDN execution requires torch.distributed initialization") -def _default_cp_group(cp_size: int) -> Any: - del cp_size - from megatron.core import parallel_state as ps +def _default_tp_cp_group(cp_size: int, tp_size: int) -> Any: + if cp_size == 1 and tp_size == 1: + return None + try: + from megatron.core import parallel_state as ps + + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + return ps.get_tensor_and_context_parallel_group() + except Exception: + pass + if torch.distributed.is_available() and torch.distributed.is_initialized(): # ty: ignore[possibly-missing-attribute] + return torch.distributed.group.WORLD # ty: ignore[possibly-missing-attribute] + raise RuntimeError( + "CP GDN layout exchange requires torch.distributed initialization" + ) + - return ps.get_context_parallel_group() +def _group_rank(group: Any | None) -> int: + if group is None: + return 0 + if torch.distributed.is_available() and torch.distributed.is_initialized(): # ty: ignore[possibly-missing-attribute] + return int(torch.distributed.get_rank(group)) # ty: ignore[possibly-missing-attribute] + return 0 def _l2norm(x: Tensor) -> Tensor: + try: + from fla.modules.l2norm import l2norm + except ImportError: + return F.normalize(x, p=2, dim=-1) return l2norm(x) def _chunk_gated_delta_rule(*args: Any, **kwargs: Any) -> tuple[Tensor, Tensor | None]: - return chunk_gated_delta_rule(*args, **kwargs) - - -@contextmanager -def _nvtx_range(label: str, tensor: Tensor | None = None) -> Iterator[None]: - if _NVTX_ENABLED.get() and tensor is not None and tensor.is_cuda: - torch.cuda.nvtx.range_push(label) - try: - yield - finally: - torch.cuda.nvtx.range_pop() - return - yield - - -@contextmanager -def gdn_nvtx_ranges(enabled: bool = True) -> Iterator[None]: - token = _NVTX_ENABLED.set(bool(enabled)) try: - yield - finally: - _NVTX_ENABLED.reset(token) + from fla.ops.gated_delta_rule import chunk_gated_delta_rule + except ImportError as exc: + raise ImportError( + "FLA is required for ART shared-prefix GDN execution." + ) from exc + return chunk_gated_delta_rule(*args, **kwargs) diff --git a/src/art/megatron/gdn/segment_layout.py b/src/art/megatron/gdn/segment_layout.py index 0dc4bdfdf..9bbb3517a 100644 --- a/src/art/megatron/gdn/segment_layout.py +++ b/src/art/megatron/gdn/segment_layout.py @@ -679,10 +679,10 @@ def forward( ) ctx.input_shape = tuple(qkv.shape) ctx.beta_shape = tuple(beta.shape) + ctx.device = qkv.device ctx.input_dtype = qkv.dtype ctx.beta_dtype = beta.dtype ctx.g_dtype = recurrent_g.dtype - ctx.device = qkv.device ctx.key_heads = key_heads ctx.value_heads = value_heads ctx.key_dim = key_dim @@ -693,7 +693,7 @@ def forward( @staticmethod def backward( ctx: Any, - *grad_outputs: Any, + *grad_outputs: Tensor | None, ) -> tuple[ Tensor | None, Tensor | None, @@ -703,6 +703,8 @@ def backward( None, None, ]: + if len(grad_outputs) != 5: + raise RuntimeError("expected five packed QKV output gradients") grad_query, grad_key, grad_value, grad_beta_out, grad_g_out = grad_outputs token_count, channels = ctx.input_shape grad_qkv = None @@ -837,9 +839,11 @@ def forward( @staticmethod def backward( - ctx: Any, *grad_outputs: Any + ctx: Any, *grad_outputs: Tensor | None ) -> tuple[Tensor, Tensor, None, None, None, None]: - (grad_out,) = grad_outputs + if len(grad_outputs) != 1 or grad_outputs[0] is None: + raise RuntimeError("expected compact scatter output gradient") + grad_out = grad_outputs[0] row_indices, position_indices, output_mask, cu_seqlens = ctx.saved_tensors _, output_sequence_length, heads, dim = ctx.output_shape grad_out = grad_out.contiguous() diff --git a/src/art/megatron/lora.py b/src/art/megatron/lora.py index d7a8b5ade..5617ac63f 100644 --- a/src/art/megatron/lora.py +++ b/src/art/megatron/lora.py @@ -1,6 +1,7 @@ -from collections.abc import Mapping, Sequence +from collections.abc import Sequence import math -from typing import Any, Literal, cast +import re +from typing import Any, Literal, NamedTuple, cast from megatron.bridge.models.gpt_provider import GPTModelProvider from megatron.core import parallel_state as ps @@ -18,7 +19,6 @@ reduce_scatter_to_sequence_parallel_region, ) from megatron.core.transformer.attention import SelfAttention -from megatron.core.transformer.module import MegatronModule from megatron.core.transformer.moe.experts import TEGroupedMLP from megatron.core.transformer.moe.shared_experts import SharedExpertMLP from megatron.core.transformer.transformer_layer import TransformerLayer @@ -26,18 +26,14 @@ import torch from .kernels.cute_grouped_lora_quack import ( - quack_grouped_lora as _quack_grouped_lora, + quack_grouped_lora, + quack_grouped_lora_dual, ) -from .kernels.cute_grouped_lora_quack import ( - quack_grouped_lora_dual as _quack_grouped_lora_dual, -) - -quack_grouped_lora = cast(Any, _quack_grouped_lora) -quack_grouped_lora_dual = cast(Any, _quack_grouped_lora_dual) MOE_LORA_RANK = 1 DENSE_LORA_RANK = 8 LORA_ALPHA = 32 +_LAYER_BLOCK_RE = re.compile(r"^(?P.*\.layers\.\d+)\.") ShardDomain = Literal["tp", "expert_tp"] GradSyncDomain = Literal["tp_default", "expert_tp"] @@ -62,6 +58,36 @@ class LoRAParallelSpec(BaseModel): grad_sync_op: GradSyncOp = GRAD_SYNC_OP_NONE +class LoraShardMeta(NamedTuple): + key: str + owner_rank: int + shape: tuple[int, ...] + dtype_name: str + manifest: dict[str, Any] + block: str + + @property + def numel(self) -> int: + total = 1 + for dim in self.shape: + total *= dim + return total + + +class _LoraPublishTemplate(NamedTuple): + adapter_model_prefix: str + suffix: str + shape: tuple[int, ...] + dtype_name: str + num_local_experts: int + shard_domain: ShardDomain + sharded: bool + shard_world_size: int + export_shard_dim: int + export_shard_strategy: str | None + component_sizes: tuple[int, ...] + + def _distributed_initialized() -> bool: is_initialized = getattr(torch.distributed, "is_initialized", None) return ( @@ -79,7 +105,6 @@ def _get_shard_world_size(domain: ShardDomain) -> int: group = ps.get_expert_tensor_parallel_group(check_initialized=False) if group is None: return 1 - assert isinstance(group, torch.distributed.ProcessGroup) return group.size() @@ -91,7 +116,6 @@ def _get_shard_rank(domain: ShardDomain) -> int: group = ps.get_expert_tensor_parallel_group(check_initialized=False) if group is None: return 0 - assert isinstance(group, torch.distributed.ProcessGroup) return group.rank() @@ -103,6 +127,30 @@ def _get_shard_group(domain: ShardDomain) -> Any | None: return ps.get_expert_tensor_parallel_group(check_initialized=False) +def _dtype_name(dtype: torch.dtype) -> str: + return str(dtype).removeprefix("torch.") + + +def _block_for_key(key: str) -> str: + match = _LAYER_BLOCK_RE.match(key) + if match is not None: + return match.group("block") + return "__global__" + + +def _process_group_ranks(group: Any | None) -> tuple[int, ...]: + if group is None or not _distributed_initialized(): + return (0,) + get_process_group_ranks = getattr( + torch.distributed, + "get_process_group_ranks", + None, + ) + if not callable(get_process_group_ranks): + raise RuntimeError("torch.distributed.get_process_group_ranks is unavailable") + return tuple(int(rank) for rank in get_process_group_ranks(group)) + + def _normalize_axis(axis: int, ndim: int) -> int: if axis < 0: axis += ndim @@ -146,12 +194,6 @@ def default_lora_rank_for_handler(handler: Any) -> int: return MOE_LORA_RANK if bool(getattr(handler, "is_moe", False)) else DENSE_LORA_RANK -def _forward_backward_dw(linear: Any) -> None: - backward_dw = getattr(linear, "backward_dw", None) - if callable(backward_dw): - backward_dw() - - def _column_parallel_lora_input(x: torch.Tensor, linear: Any) -> torch.Tensor: if _linear_disables_tensor_parallel_comm(linear): return x @@ -159,9 +201,7 @@ def _column_parallel_lora_input(x: torch.Tensor, linear: Any) -> torch.Tensor: bool(getattr(linear, "sequence_parallel", False)) and int(getattr(linear, "tp_size", 1)) > 1 ): - gathered_x = gather_from_sequence_parallel_region(x) - assert isinstance(gathered_x, torch.Tensor) - return gathered_x + return gather_from_sequence_parallel_region(x) return x @@ -228,8 +268,7 @@ def _set_lora_shard_strategy_metadata( def _exported_shard_dim(param: torch.nn.Parameter) -> int: - assert hasattr(param, "lora_tp_shard_dim") - axis = _normalize_axis(getattr(param, "lora_tp_shard_dim"), param.ndim) # ty: ignore[unresolved-attribute] + axis = _normalize_axis(param.lora_tp_shard_dim, param.ndim) # ty: ignore[unresolved-attribute] # LoRA exports always serialize a 2D tensor: # - non-expert params export `param.T` # - expert params export `param[expert].T` @@ -293,9 +332,9 @@ def num_local_experts(self) -> int: return self.A_T.shape[0] if self.A_T.ndim == 3 else 1 def _broadcast_if_replicated(self, param: torch.nn.Parameter) -> None: - if not param.lora_tp_replicated: # type: ignore[unresolved-attribute] + if not param.lora_tp_replicated: # ty: ignore[unresolved-attribute] return - domain: ShardDomain = param.lora_shard_domain # type: ignore[unresolved-attribute] + domain = param.lora_shard_domain # ty: ignore[unresolved-attribute] world_size = _get_shard_world_size(domain) if world_size <= 1: return @@ -369,9 +408,9 @@ def load_weights( self.load_weight(weight, into=into) def load_weight(self, weight: torch.Tensor, *, into: torch.nn.Parameter) -> None: - domain: ShardDomain = into.lora_shard_domain # type: ignore[unresolved-attribute] - if into.lora_tp_sharded: # type: ignore[unresolved-attribute] - axis: int = into.lora_tp_shard_dim # type: ignore[unresolved-attribute] + domain = into.lora_shard_domain # ty: ignore[unresolved-attribute] + if into.lora_tp_sharded: # ty: ignore[unresolved-attribute] + axis = into.lora_tp_shard_dim # ty: ignore[unresolved-attribute] axis = _normalize_axis(axis, weight.ndim) world_size = _get_shard_world_size(domain) rank = _get_shard_rank(domain) @@ -434,34 +473,24 @@ def _should_export_parameter(self, param: torch.nn.Parameter) -> bool: return False # this param is fully sharded, all shard ranks participate - if param.lora_tp_sharded: # type: ignore[unresolved-attribute] + if param.lora_tp_sharded: # ty: ignore[unresolved-attribute] return True # param is replicated, tp rank 0 or etp rank 0 participates - return ( - _get_shard_rank(param.lora_shard_domain) == 0 # type: ignore[unresolved-attribute] - ) + return _get_shard_rank(param.lora_shard_domain) == 0 # ty: ignore[unresolved-attribute] def _manifest_for_param(self, param: torch.nn.Parameter) -> dict[str, Any]: manifest = { - "domain": param.lora_shard_domain, # type: ignore[unresolved-attribute] - "sharded": param.lora_tp_sharded, # type: ignore[unresolved-attribute] - "shard_dim": param.lora_tp_shard_dim, # type: ignore[unresolved-attribute] - "shard_world_size": ( - _get_shard_world_size( - param.lora_shard_domain # type: ignore[unresolved-attribute] - ) - if param.lora_tp_sharded # type: ignore[unresolved-attribute] - else 1 - ), - "shard_rank": ( - _get_shard_rank( - param.lora_shard_domain # type: ignore[unresolved-attribute] - ) - if param.lora_tp_sharded # type: ignore[unresolved-attribute] - else 0 - ), + "domain": param.lora_shard_domain, # ty: ignore[unresolved-attribute] + "sharded": param.lora_tp_sharded, # ty: ignore[unresolved-attribute] + "shard_dim": param.lora_tp_shard_dim, # ty: ignore[unresolved-attribute] + "shard_world_size": _get_shard_world_size(param.lora_shard_domain) # ty: ignore[unresolved-attribute] + if param.lora_tp_sharded # ty: ignore[unresolved-attribute] + else 1, + "shard_rank": _get_shard_rank(param.lora_shard_domain) # ty: ignore[unresolved-attribute] + if param.lora_tp_sharded # ty: ignore[unresolved-attribute] + else 0, } - if param.lora_tp_sharded: # type: ignore[unresolved-attribute] + if param.lora_tp_sharded: # ty: ignore[unresolved-attribute] manifest["export_shard_dim"] = _exported_shard_dim(param) manifest["export_shard_strategy"] = getattr( param, @@ -513,11 +542,11 @@ def sharded_lora_grad_dict(self) -> dict[str, torch.Tensor]: raise RuntimeError( f"LoRA param missing main_grad attribute for key '{key}'" ) - grad: torch.Tensor | None = param.main_grad # type: ignore[unresolved-attribute] + grad = cast(torch.Tensor, param.main_grad) if grad is None: raise RuntimeError(f"LoRA param main_grad is None for key '{key}'") if hasattr(grad, "_local_tensor"): - grad = cast(torch.Tensor, grad._local_tensor) # type: ignore[unresolved-attribute] + grad = cast(Any, grad)._local_tensor local_grad = grad[expert] if expert is not None else grad grads[key] = local_grad.T return grads @@ -541,6 +570,258 @@ def forward( return out * self.scale +class LoRAPublishPlanner: + def __init__(self, model_chunks: Sequence[torch.nn.Module]) -> None: + self.templates = tuple(self._collect_templates(model_chunks)) + + def global_metadata( + self, + adapter_model: dict[str, torch.Tensor], + ) -> list[LoraShardMeta]: + if _distributed_initialized(): + pp_world_size = ps.get_pipeline_model_parallel_world_size() + if pp_world_size != 1: + raise RuntimeError( + "LoRA publish planner requires pipeline_model_parallel_size=1; " + f"got {pp_world_size}. Rank-local modules cannot describe remote " + "pipeline stages without exchanging templates." + ) + return [ + meta + for template in self.templates + for meta in self._metadata_for_template(template, adapter_model) + ] + + @staticmethod + def _collect_templates( + model_chunks: Sequence[torch.nn.Module], + ) -> list[_LoraPublishTemplate]: + templates: list[_LoraPublishTemplate] = [] + for chunk in model_chunks: + for module in chunk.modules(): + if not isinstance(module, LoRA): + continue + for suffix, param in module._lora_params(): + if not module._should_export_parameter(param): + continue + sharded = bool(param.lora_tp_sharded) # type: ignore[attr-defined] + shard_domain = param.lora_shard_domain # type: ignore[attr-defined] + templates.append( + _LoraPublishTemplate( + adapter_model_prefix=module.adapter_model_prefix, + suffix=suffix, + shape=_exported_param_shape(module, param), + dtype_name=_dtype_name(param.dtype), + num_local_experts=module.num_local_experts, + shard_domain=shard_domain, + sharded=sharded, + shard_world_size=( + _get_shard_world_size(shard_domain) if sharded else 1 + ), + export_shard_dim=( + _exported_shard_dim(param) if sharded else -1 + ), + export_shard_strategy=( + getattr(param, "lora_tp_shard_strategy", "uniform") + if sharded + else None + ), + component_sizes=tuple( + int(size) + for size in getattr( + param, + "lora_tp_component_sizes", + (), + ) + ), + ) + ) + return templates + + def _metadata_for_template( + self, + template: _LoraPublishTemplate, + adapter_model: dict[str, torch.Tensor], + ) -> list[LoraShardMeta]: + if template.num_local_experts > 1: + return self._expert_metadata_for_template(template, adapter_model) + return self._dense_metadata_for_template(template, adapter_model) + + def _dense_metadata_for_template( + self, + template: _LoraPublishTemplate, + adapter_model: dict[str, torch.Tensor], + ) -> list[LoraShardMeta]: + tp_ranks = self._dense_tp_ranks() + shard_ranks = range(template.shard_world_size) if template.sharded else (0,) + return [ + self._make_metadata( + template, + key=f"{template.adapter_model_prefix}.{template.suffix}", + owner_rank=tp_ranks[shard_rank], + shard_rank=shard_rank, + adapter_model=adapter_model, + ) + for shard_rank in shard_ranks + ] + + def _expert_metadata_for_template( + self, + template: _LoraPublishTemplate, + adapter_model: dict[str, torch.Tensor], + ) -> list[LoraShardMeta]: + ep_world_size = self._expert_model_world_size() + shard_ranks = range(template.shard_world_size) if template.sharded else (0,) + metadata: list[LoraShardMeta] = [] + for ep_rank in range(ep_world_size): + for local_expert in range(template.num_local_experts): + expert = ep_rank * template.num_local_experts + local_expert + key = f"{template.adapter_model_prefix.format(expert=expert)}.{template.suffix}" + for shard_rank in shard_ranks: + metadata.append( + self._make_metadata( + template, + key=key, + owner_rank=self._expert_owner_rank(ep_rank, shard_rank), + shard_rank=shard_rank, + adapter_model=adapter_model, + ) + ) + return metadata + + @staticmethod + def _make_metadata( + template: _LoraPublishTemplate, + *, + key: str, + owner_rank: int, + shard_rank: int, + adapter_model: dict[str, torch.Tensor], + ) -> LoraShardMeta: + return LoraShardMeta( + key=key, + owner_rank=owner_rank, + shape=template.shape, + dtype_name=( + _dtype_name(adapter_model[key].dtype) + if key in adapter_model + else template.dtype_name + ), + manifest=_publish_manifest(template, shard_rank=shard_rank), + block=_block_for_key(key), + ) + + @staticmethod + def _dense_tp_ranks() -> tuple[int, ...]: + if not _distributed_initialized(): + return (0,) + return _process_group_ranks(ps.get_tensor_model_parallel_group()) + + @staticmethod + def _expert_model_world_size() -> int: + if not _distributed_initialized(): + return 1 + return ps.get_expert_model_parallel_world_size() + + @staticmethod + def _expert_owner_rank(ep_rank: int, shard_rank: int) -> int: + if not _distributed_initialized(): + return 0 + joint_ranks = _process_group_ranks( + ps.get_expert_tensor_and_model_parallel_group(check_initialized=False) + ) + ep_world_size = ps.get_expert_model_parallel_world_size() + etp_world_size = _get_shard_world_size("expert_tp") + expected_size = ep_world_size * etp_world_size + if len(joint_ranks) != expected_size: + raise RuntimeError( + "Unexpected expert TP x EP group size: " + f"got {len(joint_ranks)}, expected {expected_size}" + ) + if shard_rank >= etp_world_size: + raise RuntimeError( + f"Invalid expert tensor shard rank {shard_rank} for world size {etp_world_size}" + ) + if ep_rank >= ep_world_size: + raise RuntimeError( + f"Invalid expert parallel rank {ep_rank} for world size {ep_world_size}" + ) + + ep_group_ranks = _process_group_ranks(ps.get_expert_model_parallel_group()) + etp_group = ps.get_expert_tensor_parallel_group(check_initialized=False) + etp_group_ranks = _process_group_ranks(etp_group) + ep_positions = [joint_ranks.index(rank) for rank in ep_group_ranks] + etp_positions = [joint_ranks.index(rank) for rank in etp_group_ranks] + + if etp_positions == list(range(etp_world_size)): + return joint_ranks[ep_rank * etp_world_size + shard_rank] + if ep_positions == list(range(ep_world_size)): + return joint_ranks[shard_rank * ep_world_size + ep_rank] + raise RuntimeError( + "Unsupported expert TP x EP group rank order: " + f"joint={joint_ranks}, ep_positions={ep_positions}, etp_positions={etp_positions}" + ) + + +def _exported_param_shape(module: LoRA, param: torch.nn.Parameter) -> tuple[int, ...]: + if module.num_local_experts > 1: + return tuple(int(dim) for dim in param[0].T.shape) + return tuple(int(dim) for dim in param.T.shape) + + +def _publish_manifest( + template: _LoraPublishTemplate, + *, + shard_rank: int, +) -> dict[str, Any]: + manifest: dict[str, Any] = { + "sharded": template.sharded, + "shard_world_size": template.shard_world_size if template.sharded else 1, + "shard_rank": shard_rank if template.sharded else 0, + } + if template.sharded: + manifest["export_shard_dim"] = template.export_shard_dim + manifest["export_shard_strategy"] = template.export_shard_strategy or "uniform" + if template.component_sizes: + manifest["component_sizes"] = list(template.component_sizes) + return manifest + + +@torch.compiler.disable +def _expert_grouped_lora_forward( + lora: LoRA, + x: torch.Tensor, + tokens_per_expert: list[int] | torch.Tensor, + out_features: int, +) -> torch.Tensor: + if x.shape[0] == 0: + return x.new_zeros((x.shape[0], out_features)) + return lora(x, tokens_per_expert=tokens_per_expert) + + +@torch.compiler.disable +def _expert_grouped_lora_dual_forward( + module: "MLPExpertsLinearFC1LoRA", + x: torch.Tensor, + tokens_per_expert: list[int] | torch.Tensor, +) -> torch.Tensor: + counts = tokens_per_expert + if isinstance(counts, list): + counts = torch.tensor(counts, dtype=torch.int64, device="cpu") + if x.shape[0] == 0: + return x.new_zeros((x.shape[0], module.linear_fc1.out_features)) + return quack_grouped_lora_dual( + x, + module.gate_lora.A_T, + module.gate_lora.B_T, + module.up_lora.A_T, + module.up_lora.B_T, + counts, + scale_gate=module.gate_lora.scale, + scale_up=module.up_lora.scale, + ) + + class SelfAttentionLinearProjLoRA(torch.nn.Module): def __init__( self, @@ -588,18 +869,15 @@ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: base_output, bias_output = self.linear_proj(x) assert isinstance(base_output, torch.Tensor) assert isinstance(bias_output, (torch.Tensor, type(None))) + lora_output = self.lora(x) if self.reduce_output and self.provider.tensor_model_parallel_size > 1: if self.provider.sequence_parallel: lora_output = reduce_scatter_to_sequence_parallel_region(lora_output) else: lora_output = reduce_from_tensor_model_parallel_region(lora_output) - assert isinstance(lora_output, torch.Tensor) return base_output + lora_output, bias_output - def backward_dw(self) -> None: - _forward_backward_dw(self.linear_proj) - class SelfAttentionLinearQKVLoRA(torch.nn.Module): def __init__( @@ -755,9 +1033,6 @@ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: return linear_output + adapter_output, bias - def backward_dw(self) -> None: - _forward_backward_dw(self.linear_qkv) - class GatedDeltaNetInProjLoRA(torch.nn.Module): def __init__( @@ -772,7 +1047,6 @@ def __init__( in_proj.return_layernorm_output = True in_proj.return_layernorm_output_gathered = True self.in_proj = in_proj - assert gated_delta_net.num_value_heads is not None self.num_value_heads_per_partition = ( gated_delta_net.num_value_heads // ps.get_tensor_model_parallel_world_size() ) @@ -862,9 +1136,6 @@ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: adapter_output = torch.cat([qkv, z, beta, alpha], dim=-1) return linear_output + adapter_output, bias - def backward_dw(self) -> None: - _forward_backward_dw(self.in_proj) - class MLPExpertsLinearFC1LoRA(torch.nn.Module): def __init__( @@ -939,27 +1210,9 @@ def forward( self, x: torch.Tensor, tokens_per_expert: list[int] | torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor | None]: base_out, bias_out = self.linear_fc1(x, tokens_per_expert) - counts = tokens_per_expert - if isinstance(counts, list): - counts = torch.tensor(counts, dtype=torch.int64, device="cpu") - if x.shape[0] == 0: - adapter_out = x.new_zeros((x.shape[0], self.linear_fc1.out_features)) - else: - adapter_out = quack_grouped_lora_dual( - x, - self.gate_lora.A_T, - self.gate_lora.B_T, - self.up_lora.A_T, - self.up_lora.B_T, - counts, - scale_gate=self.gate_lora.scale, - scale_up=self.up_lora.scale, - ) + adapter_out = _expert_grouped_lora_dual_forward(self, x, tokens_per_expert) return base_out + adapter_out, bias_out - def backward_dw(self) -> None: - _forward_backward_dw(self.linear_fc1) - class MLPExpertsLinearFC1FusedLoRA(torch.nn.Module): def __init__( @@ -1002,25 +1255,26 @@ def __init__( b_parallel_spec=b_parallel_spec, allreduce=False, ) - component_size = ( - linear_fc1.out_features * _get_shard_world_size("expert_tp") - ) // 2 + gate_out_features = linear_fc1.out_features // 2 + expert_tp_world_size = _get_shard_world_size("expert_tp") _set_lora_shard_strategy_metadata( self.lora.B_T, strategy="componentwise", - component_sizes=(component_size, component_size), + component_sizes=( + gate_out_features * expert_tp_world_size, + gate_out_features * expert_tp_world_size, + ), ) def forward( self, x: torch.Tensor, tokens_per_expert: list[int] | torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor | None]: base_out, bias_out = self.linear_fc1(x, tokens_per_expert) - adapter_out = self.lora(x, tokens_per_expert=tokens_per_expert) + adapter_out = _expert_grouped_lora_forward( + self.lora, x, tokens_per_expert, self.linear_fc1.out_features + ) return base_out + adapter_out, bias_out - def backward_dw(self) -> None: - _forward_backward_dw(self.linear_fc1) - class MLPExpertsLinearFC2LoRA(torch.nn.Module): def __init__( @@ -1069,14 +1323,13 @@ def forward( self, x: torch.Tensor, tokens_per_expert: list[int] | torch.Tensor ) -> tuple[torch.Tensor, torch.Tensor | None]: base_out, bias_out = self.linear_fc2(x, tokens_per_expert) - adapter_out = self.lora(x, tokens_per_expert=tokens_per_expert) + adapter_out = _expert_grouped_lora_forward( + self.lora, x, tokens_per_expert, self.linear_fc2.out_features + ) # the reason there is no TP comm here is because the MoE token routing handles # expert TP comm externally return base_out + adapter_out, bias_out - def backward_dw(self) -> None: - _forward_backward_dw(self.linear_fc2) - class SharedExpertsLinearFC1LoRA(torch.nn.Module): def __init__( @@ -1160,9 +1413,6 @@ def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: ) return base_out + adapter_out, bias_out - def backward_dw(self) -> None: - _forward_backward_dw(self.linear_fc1) - class SharedExpertsLinearFC2LoRA(torch.nn.Module): def __init__( @@ -1186,9 +1436,6 @@ def __init__( def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor | None]: return self.row_parallel_lora(x) - def backward_dw(self) -> None: - self.row_parallel_lora.backward_dw() - def _unwrap_attr( value: Any, @@ -1434,22 +1681,19 @@ def wrap_shared_experts_mlp( def apply_lora_adapters( - model: Sequence[MegatronModule], + model: Sequence[torch.nn.Module], provider: GPTModelProvider, - lora_config: Mapping[str, Any] | None = None, -) -> list[MegatronModule]: - lora_config = lora_config or {} - provider_with_art_attrs = cast(Any, provider) - handler = provider_with_art_attrs._art_model_support_handler - spec = provider_with_art_attrs._art_model_support_spec - target_modules = list( - lora_config.get("target_modules", spec.default_target_modules) - ) +) -> list[torch.nn.Module]: + provider = cast(Any, provider) + handler = provider._art_model_support_handler + spec = provider._art_model_support_spec + target_modules = list(spec.default_target_modules) + rank = default_lora_rank_for_handler(handler) handler.apply_lora_adapters( model, provider, target_modules=target_modules, - rank=lora_config.get("rank", default_lora_rank_for_handler(handler)), - alpha=lora_config.get("alpha", LORA_ALPHA), + rank=rank, + alpha=LORA_ALPHA, ) return list(model) diff --git a/src/art/megatron/megatron_patches.py b/src/art/megatron/megatron_patches.py new file mode 100644 index 000000000..c1e32ae19 --- /dev/null +++ b/src/art/megatron/megatron_patches.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import Any + +import torch + + +def _frozen_linear_grad_input( + grad_output: torch.Tensor, + weight: torch.Tensor, +) -> torch.Tensor: + if grad_output.dim() <= 2 or weight.dim() != 2: + return grad_output.matmul(weight) + grad_output_2d = grad_output.reshape(-1, int(grad_output.shape[-1])) + grad_input_2d = grad_output_2d.matmul(weight) + return grad_input_2d.reshape(*grad_output.shape[:-1], int(weight.shape[-1])) + + +def install_fast_frozen_output_backward() -> None: + from megatron.core.tensor_parallel.layers import LinearWithFrozenWeight + + if getattr(LinearWithFrozenWeight.backward, "__art_fast_output_backward__", False): + return + + def _fast_backward( + ctx: Any, + grad_output: torch.Tensor, + ) -> tuple[torch.Tensor, None, None, None, None]: + (weight,) = ctx.saved_tensors + grad_input = _frozen_linear_grad_input(grad_output, weight) + if ctx.allreduce_dgrad: + torch.distributed.all_reduce( # ty: ignore[possibly-missing-attribute] + grad_input, + group=ctx.tp_group, + ) + return grad_input, None, None, None, None + + setattr(_fast_backward, "__art_fast_output_backward__", True) + LinearWithFrozenWeight.backward = staticmethod(_fast_backward) diff --git a/src/art/megatron/model_support/__init__.py b/src/art/megatron/model_support/__init__.py index 60862ac54..fb806e9b2 100644 --- a/src/art/megatron/model_support/__init__.py +++ b/src/art/megatron/model_support/__init__.py @@ -13,6 +13,7 @@ VALIDATED_MODEL_SUPPORT_SPECS, UnsupportedModelArchitectureError, default_target_modules_for_model, + ensure_model_support_bridge_registered_for_spec, get_model_support_handler, get_model_support_handler_for_spec, get_model_support_spec, @@ -26,13 +27,10 @@ ArchitectureReport, DependencyFloor, LayerFamilyInstance, - MinimalLayerCoverageReport, ModelSupportHandler, ModelSupportSpec, NativeVllmLoraStatus, RolloutWeightsMode, - ValidationReport, - ValidationStageResult, ) _LAZY_EXPORT_MODULES = { @@ -58,7 +56,6 @@ def __getattr__(name: str): "DEFAULT_DENSE_SPEC", "DependencyFloor", "LayerFamilyInstance", - "MinimalLayerCoverageReport", "ModelSupportHandler", "ModelSupportSpec", "NativeVllmLoraStatus", @@ -73,11 +70,10 @@ def __getattr__(name: str): "QWEN3_5_MOE_SPEC", "PROBE_ONLY_MODEL_SUPPORT_SPECS", "RolloutWeightsMode", - "ValidationReport", - "ValidationStageResult", "UnsupportedModelArchitectureError", "VALIDATED_MODEL_SUPPORT_SPECS", "default_target_modules_for_model", + "ensure_model_support_bridge_registered_for_spec", "get_model_support_handler", "get_model_support_handler_for_spec", "get_model_support_spec", diff --git a/src/art/megatron/model_support/handlers/__init__.py b/src/art/megatron/model_support/handlers/__init__.py index 80b18c7ce..e38c35069 100644 --- a/src/art/megatron/model_support/handlers/__init__.py +++ b/src/art/megatron/model_support/handlers/__init__.py @@ -1,33 +1,64 @@ -from art.megatron.model_support.handlers.default_dense import ( - DEFAULT_DENSE_HANDLER, - DefaultDenseHandler, - DefaultMoeHandler, -) -from art.megatron.model_support.handlers.qwen3_5 import ( - QWEN3_5_DENSE_HANDLER, - QWEN3_5_MOE_HANDLER, - Qwen35DenseHandler, - Qwen35MoeHandler, -) -from art.megatron.model_support.handlers.qwen3_dense import ( - QWEN3_DENSE_HANDLER, - Qwen3DenseHandler, -) -from art.megatron.model_support.handlers.qwen3_moe import ( - QWEN3_MOE_HANDLER, - Qwen3MoeHandler, -) +from __future__ import annotations -__all__ = [ - "DEFAULT_DENSE_HANDLER", - "DefaultDenseHandler", - "DefaultMoeHandler", - "QWEN3_5_DENSE_HANDLER", - "Qwen35DenseHandler", - "QWEN3_DENSE_HANDLER", - "Qwen3DenseHandler", - "QWEN3_MOE_HANDLER", - "Qwen3MoeHandler", - "QWEN3_5_MOE_HANDLER", - "Qwen35MoeHandler", -] +from importlib import import_module +from typing import Any + +_LAZY_EXPORTS = { + "DEFAULT_DENSE_HANDLER": ( + "art.megatron.model_support.handlers.default_dense", + "DEFAULT_DENSE_HANDLER", + ), + "DefaultDenseHandler": ( + "art.megatron.model_support.handlers.default_dense", + "DefaultDenseHandler", + ), + "DefaultMoeHandler": ( + "art.megatron.model_support.handlers.default_dense", + "DefaultMoeHandler", + ), + "QWEN3_DENSE_HANDLER": ( + "art.megatron.model_support.handlers.qwen3_dense", + "QWEN3_DENSE_HANDLER", + ), + "Qwen3DenseHandler": ( + "art.megatron.model_support.handlers.qwen3_dense", + "Qwen3DenseHandler", + ), + "QWEN3_MOE_HANDLER": ( + "art.megatron.model_support.handlers.qwen3_moe", + "QWEN3_MOE_HANDLER", + ), + "Qwen3MoeHandler": ( + "art.megatron.model_support.handlers.qwen3_moe", + "Qwen3MoeHandler", + ), + "QWEN3_5_DENSE_HANDLER": ( + "art.megatron.model_support.handlers.qwen3_5", + "QWEN3_5_DENSE_HANDLER", + ), + "Qwen35DenseHandler": ( + "art.megatron.model_support.handlers.qwen3_5", + "Qwen35DenseHandler", + ), + "QWEN3_5_MOE_HANDLER": ( + "art.megatron.model_support.handlers.qwen3_5", + "QWEN3_5_MOE_HANDLER", + ), + "Qwen35MoeHandler": ( + "art.megatron.model_support.handlers.qwen3_5", + "Qwen35MoeHandler", + ), +} + + +def __getattr__(name: str) -> Any: + try: + module_name, attribute_name = _LAZY_EXPORTS[name] + except KeyError as exc: + raise AttributeError(name) from exc + value = getattr(import_module(module_name), attribute_name) + globals()[name] = value + return value + + +__all__ = list(_LAZY_EXPORTS) diff --git a/src/art/megatron/model_support/handlers/default_dense.py b/src/art/megatron/model_support/handlers/default_dense.py index bb5cffaab..bd79332ae 100644 --- a/src/art/megatron/model_support/handlers/default_dense.py +++ b/src/art/megatron/model_support/handlers/default_dense.py @@ -1,17 +1,38 @@ -import re from typing import Any, Sequence import torch from art.megatron.model_support.spec import ( CompileWorkaroundConfig, + ExpertPackedLoraGroup, LayerFamilyInstance, SharedExpertCompileState, ) +_CONTEXT_PARALLEL_ATTENTION_WORKAROUND_FLAG = "context_parallel_attention" +_SELF_ATTN_LINEAR_PROJ_REDUCE_SCATTER_WORKAROUND_FLAG = ( + "disable_compile_self_attn_linear_proj_reduce_scatter" +) + + +def _compile_workaround_flags_for_provider( + provider: Any, + base_flags: tuple[str, ...] = (), +) -> tuple[str, ...]: + flags = base_flags + if ( + bool(getattr(provider, "sequence_parallel", False)) + and int(getattr(provider, "tensor_model_parallel_size", 1) or 1) > 1 + ): + flags = (*flags, _SELF_ATTN_LINEAR_PROJ_REDUCE_SCATTER_WORKAROUND_FLAG) + if int(getattr(provider, "context_parallel_size", 1) or 1) <= 1: + return flags + return (*flags, _CONTEXT_PARALLEL_ATTENTION_WORKAROUND_FLAG) + class DefaultDenseHandler: key = "default_dense" + build_gdn_execution_spec = False is_moe = False native_vllm_lora_status = "disabled" @@ -66,17 +87,6 @@ def install_preprocess_patch(self, model_chunks: Sequence[Any]) -> None: del model_chunks return None - def hf_tensor_map_to_art_canonical( - self, - hf_tensor_map: dict[str, torch.Tensor], - *, - expected_keys: set[str], - ) -> dict[str, torch.Tensor]: - return _unfuse_moe_hf_tensor_map_for_expected_keys( - hf_tensor_map, - expected_keys=expected_keys, - ) - def to_vllm_lora_tensors( self, tensors: dict[str, torch.Tensor], @@ -94,6 +104,9 @@ def from_vllm_lora_tensors( del adapter_config return tensors + def expert_packed_lora_groups(self) -> tuple[ExpertPackedLoraGroup, ...]: + return () + def _shared_expert_compile_state( self, provider: Any, @@ -187,7 +200,8 @@ def compile_workaround_config( provider: Any, ) -> CompileWorkaroundConfig: return CompileWorkaroundConfig( - shared_expert_state=self._shared_expert_compile_state(provider) + flags=_compile_workaround_flags_for_provider(provider), + shared_expert_state=self._shared_expert_compile_state(provider), ) def get_forward_kwargs(self, model: Any, **kwargs: Any) -> dict[str, Any]: @@ -315,73 +329,4 @@ def _require_moe_experts(module: Any) -> Any: return experts -_FUSED_MOE_EXPERT_PATTERN = re.compile( - r"^(?P.*\.mlp\.experts)\.(?Pgate_up_proj|down_proj)(?:\.weight)?$" -) - - -def _strip_language_model_prefix(key: str) -> str: - if key.startswith("model.language_model."): - return f"model.{key.removeprefix('model.language_model.')}" - return key - - -def _expected_unfused_experts_for_prefix( - expected_keys: set[str], - prefix: str, - *, - param: str, -) -> bool: - simplified_expected_keys = { - _strip_language_model_prefix(key) for key in expected_keys - } - if param == "gate_up_proj": - return ( - f"{prefix}.0.gate_proj.weight" in simplified_expected_keys - or f"{prefix}.0.up_proj.weight" in simplified_expected_keys - ) - if param == "down_proj": - return f"{prefix}.0.down_proj.weight" in simplified_expected_keys - return False - - -def _unfuse_moe_hf_tensor_map_for_expected_keys( - hf_tensor_map: dict[str, torch.Tensor], - *, - expected_keys: set[str], -) -> dict[str, torch.Tensor]: - canonical: dict[str, torch.Tensor] = {} - for key, value in hf_tensor_map.items(): - match = _FUSED_MOE_EXPERT_PATTERN.match(key) - if match is None: - canonical[key] = value - continue - - prefix = match.group("prefix") - param = match.group("param") - if value.ndim != 3 or not _expected_unfused_experts_for_prefix( - expected_keys, - prefix, - param=param, - ): - canonical[key] = value - continue - - num_experts = int(value.shape[0]) - if param == "gate_up_proj": - if value.shape[1] % 2 != 0: - canonical[key] = value - continue - gate_proj, up_proj = value.chunk(2, dim=1) - for expert in range(num_experts): - canonical[f"{prefix}.{expert}.gate_proj.weight"] = gate_proj[expert] - canonical[f"{prefix}.{expert}.up_proj.weight"] = up_proj[expert] - continue - - for expert in range(num_experts): - canonical[f"{prefix}.{expert}.down_proj.weight"] = value[expert] - - return canonical - - DEFAULT_DENSE_HANDLER = DefaultDenseHandler() diff --git a/src/art/megatron/model_support/handlers/qwen3_5.py b/src/art/megatron/model_support/handlers/qwen3_5.py index 06ae9392d..60d2f92b0 100644 --- a/src/art/megatron/model_support/handlers/qwen3_5.py +++ b/src/art/megatron/model_support/handlers/qwen3_5.py @@ -1,30 +1,41 @@ +from __future__ import annotations + from copy import copy from functools import lru_cache import re from types import MethodType from typing import Any, Sequence, cast -from megatron.core.models.gpt.gpt_model import GPTModel -from megatron.core.ssm.gated_delta_net import GatedDeltaNet import torch from art.megatron.model_support.handlers.default_dense import ( DefaultDenseHandler, + _compile_workaround_flags_for_provider, _require_dense_mlp, _require_moe_experts, ) +from art.megatron.model_support.handlers.qwen3_common import ( + _context_parallel_world_size, +) from art.megatron.model_support.spec import ( CompileWorkaroundConfig, + ExpertPackedLoraGroup, + ExpertPackedLoraSlot, LayerFamilyInstance, ) -from art.megatron.provider_common import patch_layer_spec_tree -from art.megatron.training.model_chunks import ModelChunks _QWEN35_MOE_COMPILE_WORKAROUND_FLAGS = ( "alltoall_dtoh", "alltoall_dispatch_preprocess", + "deepep_dispatch_combine", "deepep_permute_restore", + "flex_token_dispatch_combine", + "te_triton_permute_with_mask_map", + # Torch 2.11.0 compiles Megatron's weighted SwiGLU custom autograd + # function with zero cotangents when its forward casts internally. + "weighted_bias_swiglu_no_inner_forward_cast", ) +_QWEN35_MOE_UNCONDITIONAL_COMPILE_WORKAROUND_FLAGS: tuple[str, ...] = () _ART_LAYER_PREFIX = "base_model.model.model.layers." _VLLM_LAYER_PREFIX = "base_model.model.model.language_model.layers." _ART_MOE_EXPERT_KEY_RE = re.compile( @@ -35,10 +46,15 @@ r"^(?P.*\.mlp\.experts)\." r"(?:(?Pbase_layer)\.)?(?Plora_[AB])\.weight$" ) +_VLLM_MOE_EXPERT_KEY_RE = re.compile( + r"^(?P.*\.mlp\.experts)\.(?P\d+)\." + r"(?Pgate_proj|up_proj|down_proj)\.(?Plora_[AB])\.weight$" +) class Qwen35BaseHandler(DefaultDenseHandler): key = "qwen3_5_base" + build_gdn_execution_spec = True native_vllm_lora_status = "validated" def identity_lora_model_config(self, base_config: Any) -> Any: @@ -98,6 +114,8 @@ def from_vllm_lora_tensors( return transformed def install_preprocess_patch(self, model_chunks: Sequence[Any]) -> None: + from megatron.core.models.gpt.gpt_model import GPTModel + from art.megatron.gdn.operator import ( install_gdn_island_hooks, install_shared_prefix_gdn_hooks, @@ -105,7 +123,7 @@ def install_preprocess_patch(self, model_chunks: Sequence[Any]) -> None: install_shared_prefix_gdn_hooks(model_chunks) install_gdn_island_hooks(model_chunks) - for chunk in cast(ModelChunks, list(model_chunks)): + for chunk in list(model_chunks): module: Any = chunk while hasattr(module, "module"): module = module.module @@ -127,7 +145,24 @@ def preprocess_hook(*args, _preprocess=preprocess, **kwargs): position_ids.shape[0], position_ids.shape[1], ) - preproc_output = list(_preprocess(*args, **kwargs)) + rotary_pos_emb = getattr(gpt_module, "rotary_pos_emb", None) + rotary_cp_group = getattr(rotary_pos_emb, "cp_group", None) + dispatched_local_cp_positions = ( + isinstance(position_ids, torch.Tensor) + and position_ids.ndim == 2 + and _context_parallel_world_size( + getattr(gpt_module, "config", None) + ) + > 1 + and rotary_cp_group is not None + ) + if dispatched_local_cp_positions: + setattr(rotary_pos_emb, "cp_group", None) + try: + preproc_output = list(_preprocess(*args, **kwargs)) + finally: + if dispatched_local_cp_positions: + setattr(rotary_pos_emb, "cp_group", rotary_cp_group) decoder_input = cast(torch.Tensor, preproc_output[0]) if not decoder_input.requires_grad and decoder_input.is_leaf: decoder_input.requires_grad_(True) @@ -165,7 +200,6 @@ def collect_layer_families(self, provider: Any) -> list[LayerFamilyInstance]: def patch_bridge(self, bridge: Any) -> None: del bridge - _ensure_qwen35_text_only_bridge_registered() def configure_provider_for_runtime(self, provider: Any) -> None: provider.mtp_num_layers = None @@ -179,7 +213,7 @@ def patch_provider(self, provider: Any, bridge: Any) -> None: patch_standard_attention_specs, transformer_block_spec_factory, ) = _require_qwen35_provider_symbols() - from art.megatron.flex_attention import FlexDotProductAttention + from art.megatron.provider import patch_art_flex_attention matched_provider_type = next( provider_type @@ -187,14 +221,14 @@ def patch_provider(self, provider: Any, bridge: Any) -> None: if isinstance(provider, provider_type) ) - def _patch_qwen35_block_spec(block_spec: object) -> None: + def _patch_qwen35_block_spec(block_spec: object, config: Any) -> None: patch_standard_attention_specs(block_spec, qwen3_vl_self_attention) for layer_spec in getattr(block_spec, "layer_specs", ()): - patch_layer_spec_tree(layer_spec, FlexDotProductAttention) + patch_art_flex_attention(layer_spec, config) def _qwen35_layer_spec(config: Any, vp_stage: int | None = None) -> object: block_spec = transformer_block_spec_factory(config, vp_stage=vp_stage) - _patch_qwen35_block_spec(block_spec) + _patch_qwen35_block_spec(block_spec, config) return block_spec def _provide_qwen35_with_flex_attention( @@ -224,6 +258,7 @@ def apply_lora_adapters( rank: int, alpha: int, ) -> None: + from megatron.core.ssm.gated_delta_net import GatedDeltaNet from megatron.core.transformer.attention import SelfAttention from megatron.core.transformer.transformer_layer import TransformerLayer @@ -278,6 +313,7 @@ def build_adapter_weights_by_base( self, model_chunks: Sequence[Any], ) -> dict[str, list[Any]]: + from megatron.core.ssm.gated_delta_net import GatedDeltaNet from megatron.core.transformer.attention import SelfAttention from megatron.core.transformer.transformer_layer import TransformerLayer @@ -371,6 +407,39 @@ class Qwen35MoeHandler(Qwen35BaseHandler): key = "qwen3_5_moe" is_moe = True + def expert_packed_lora_groups(self) -> tuple[ExpertPackedLoraGroup, ...]: + return ( + ExpertPackedLoraGroup( + art_group_suffix=".mlp.experts", + slots=( + ExpertPackedLoraSlot( + source_projection="gate_up_proj", + source_lora="lora_A", + output_suffix="base_layer.lora_A.weight", + pack_layout="expert_rows", + ), + ExpertPackedLoraSlot( + source_projection="gate_up_proj", + source_lora="lora_B", + output_suffix="base_layer.lora_B.weight", + pack_layout="rank_major_expert_cols", + ), + ExpertPackedLoraSlot( + source_projection="down_proj", + source_lora="lora_A", + output_suffix="lora_A.weight", + pack_layout="expert_rows", + ), + ExpertPackedLoraSlot( + source_projection="down_proj", + source_lora="lora_B", + output_suffix="lora_B.weight", + pack_layout="rank_major_expert_cols", + ), + ), + ), + ) + def to_vllm_lora_tensors( self, tensors: dict[str, torch.Tensor], @@ -448,11 +517,16 @@ def compile_workaround_config( if bool(getattr(provider, "moe_shared_expert_overlap", False)): return CompileWorkaroundConfig( flags=("moe_forward",), + unconditional_flags=_QWEN35_MOE_UNCONDITIONAL_COMPILE_WORKAROUND_FLAGS, shared_expert_state="shared_expert_overlap", disable_compile=True, ) return CompileWorkaroundConfig( - flags=_QWEN35_MOE_COMPILE_WORKAROUND_FLAGS, + flags=_compile_workaround_flags_for_provider( + provider, + _QWEN35_MOE_COMPILE_WORKAROUND_FLAGS, + ), + unconditional_flags=_QWEN35_MOE_UNCONDITIONAL_COMPILE_WORKAROUND_FLAGS, shared_expert_state="shared_experts", disable_compile=False, ) @@ -602,6 +676,10 @@ def _unpack_vllm_3d_lora_b( return tensor.reshape(tensor.shape[0], rank, num_experts).permute(2, 0, 1) +def _clone(tensor: torch.Tensor) -> torch.Tensor: + return tensor.clone().contiguous() + + def _vllm_moe_config(adapter_config: dict[str, Any]) -> dict[str, Any]: config = dict(adapter_config) target_modules = [ @@ -637,6 +715,7 @@ def _to_vllm_lora_tensors( ) -> tuple[dict[str, torch.Tensor], dict[str, Any]]: grouped = _group_art_moe_tensors(tensors) if not grouped: + has_fused_experts = any(_VLLM_MOE_KEY_RE.match(key) for key in tensors) transformed: dict[str, torch.Tensor] = {} for key, tensor in tensors.items(): vllm_key, tensor = _to_vllm_lora_tensor( @@ -644,8 +723,14 @@ def _to_vllm_lora_tensors( tensor, adapter_config=adapter_config, ) + if vllm_key in transformed: + raise RuntimeError( + f"Duplicate Qwen3.5 LoRA tensor after conversion: {vllm_key}" + ) transformed[vllm_key] = tensor - return transformed, adapter_config + return transformed, _vllm_moe_config( + adapter_config + ) if has_fused_experts else adapter_config transformed: dict[str, torch.Tensor] = {} used_keys: set[str] = set() for prefix, experts in grouped.items(): @@ -665,6 +750,11 @@ def _to_vllm_lora_tensors( raise RuntimeError( f"Incomplete Qwen3.5 MoE LoRA block for {prefix}.{expert}" ) from exc + if gate_up_b_tensor.shape[0] % 2 != 0: + raise RuntimeError( + f"{prefix}.{expert}: gate/up lora_B rows " + f"{gate_up_b_tensor.shape[0]} are not even" + ) gate_up_a.append(gate_up_a_tensor.contiguous()) gate_up_b.append(gate_up_b_tensor.contiguous()) down_a.append(d_a.contiguous()) @@ -701,6 +791,69 @@ def _from_vllm_lora_tensors( *, adapter_config: dict[str, Any], ) -> dict[str, torch.Tensor]: + expert_grouped: dict[str, dict[int, dict[str, dict[str, torch.Tensor]]]] = {} + for key, tensor in tensors.items(): + match = _VLLM_MOE_EXPERT_KEY_RE.match(key) + if match is None: + continue + expert_grouped.setdefault(match.group("prefix"), {}).setdefault( + int(match.group("expert")), + {}, + ).setdefault(match.group("module"), {})[match.group("lora")] = tensor + if expert_grouped: + transformed: dict[str, torch.Tensor] = {} + used_keys: set[str] = set() + for prefix, experts in expert_grouped.items(): + art_prefix = _from_vllm_key(prefix) + for expert, modules in experts.items(): + try: + gate_a = modules["gate_proj"]["lora_A"] + gate_b = modules["gate_proj"]["lora_B"] + up_a = modules["up_proj"]["lora_A"] + up_b = modules["up_proj"]["lora_B"] + down_a = modules["down_proj"]["lora_A"] + down_b = modules["down_proj"]["lora_B"] + except KeyError as exc: + raise RuntimeError( + f"Incomplete Qwen3.5 vLLM MoE LoRA block for {prefix}.{expert}" + ) from exc + if not torch.equal(gate_a, up_a): + raise RuntimeError( + "Qwen3.5 Megatron gate_up_proj requires gate/up " + f"LoRA-A tensors to match for {prefix}.{expert}" + ) + transformed[f"{art_prefix}.{expert}.gate_up_proj.lora_A.weight"] = ( + _clone(gate_a) + ) + transformed[f"{art_prefix}.{expert}.gate_up_proj.lora_B.weight"] = ( + torch.cat([gate_b, up_b], dim=0).contiguous() + ) + transformed[f"{art_prefix}.{expert}.down_proj.lora_A.weight"] = _clone( + down_a + ) + transformed[f"{art_prefix}.{expert}.down_proj.lora_B.weight"] = _clone( + down_b + ) + for module_name in ("gate_proj", "up_proj", "down_proj"): + for lora_name in ("lora_A", "lora_B"): + used_keys.add( + f"{prefix}.{expert}.{module_name}.{lora_name}.weight" + ) + for key, tensor in tensors.items(): + if key in used_keys: + continue + if _VLLM_MOE_KEY_RE.match(key) is not None: + raise RuntimeError( + "Mixed fused and per-expert Qwen3.5 vLLM MoE LoRA tensors" + ) + art_key, tensor = _from_vllm_lora_tensor( + key, + tensor, + adapter_config=adapter_config, + ) + transformed[art_key] = tensor + return transformed + grouped: dict[str, dict[str, torch.Tensor]] = {} for key, tensor in tensors.items(): match = _VLLM_MOE_KEY_RE.match(key) @@ -865,177 +1018,213 @@ def _qwen35_text_only_mapping_registry( def _text_only_qwen35_mapping(mapping: Any) -> Any: - from megatron.bridge.models.qwen_vl.qwen3_vl_bridge import ( - FusedExpertMapping, - FusedGatedExpertMapping, - ) - + ( + bridge_gate_up_mapping, + bridge_down_mapping, + art_gate_up_mapping, + art_down_mapping, + ) = _art_qwen35_expert_mapping_types() megatron_param = mapping.megatron_param.removeprefix("language_model.") - if isinstance(mapping, FusedGatedExpertMapping): - return _ArtExpertMLPGateUpProjMapping(megatron_param, mapping.hf_param) - if isinstance(mapping, FusedExpertMapping): - return _ArtExpertMLPDownProjMapping(megatron_param, mapping.hf_param) + if isinstance(mapping, bridge_gate_up_mapping): + return art_gate_up_mapping(megatron_param, mapping.hf_param) + if isinstance(mapping, bridge_down_mapping): + return art_down_mapping(megatron_param, mapping.hf_param) cloned = copy(mapping) cloned.megatron_param = megatron_param return cloned -from megatron.bridge.models.qwen_vl.qwen3_vl_bridge import ( - FusedExpertMapping as _BridgeExpertMLPDownProjMapping, -) -from megatron.bridge.models.qwen_vl.qwen3_vl_bridge import ( - FusedGatedExpertMapping as _BridgeExpertMLPGateUpProjMapping, -) +@lru_cache(maxsize=1) +def _art_qwen35_expert_mapping_types() -> tuple[ + type[Any], type[Any], type[Any], type[Any] +]: + from megatron.bridge.models.qwen_vl.qwen3_vl_bridge import ( + FusedExpertMapping, + FusedGatedExpertMapping, + ) + class _ArtExpertMLPGateUpProjMapping(FusedGatedExpertMapping): + def hf_to_megatron( + self, + hf_weights: Any, + megatron_module: Any, + ) -> torch.Tensor: + from megatron.bridge.models.conversion.param_mapping import ( + _align_expert_weight_to_shape, + ) + from megatron.bridge.models.conversion.utils import ( + get_module_and_param_from_name, + ) + from megatron.bridge.utils.common_utils import ( + extract_expert_number_from_param, + ) -class _ArtExpertMLPGateUpProjMapping(_BridgeExpertMLPGateUpProjMapping): - def hf_to_megatron( - self, - hf_weights: torch.Tensor | dict[str, torch.Tensor], - megatron_module: Any, - ) -> torch.Tensor: - from megatron.bridge.models.conversion.param_mapping import ( - _align_expert_weight_to_shape, - ) - from megatron.bridge.models.conversion.utils import ( - get_module_and_param_from_name, - ) - from megatron.bridge.utils.common_utils import ( - extract_expert_number_from_param, - ) + global_expert_number = extract_expert_number_from_param(self.megatron_param) + expert_weight = _select_qwen35_expert_weight( + hf_weights, + global_expert_number=global_expert_number, + ep_size=int(self.ep_size), + ) + normalized_param = self._normalize_expert_param_name(self.megatron_param) + target_param = get_module_and_param_from_name( + megatron_module, normalized_param + )[1] + full_target_shape = ( + target_param.shape[0] * self.tp_size, + target_param.shape[1], + ) + gate_target_shape = ( + full_target_shape[0] // 2, + full_target_shape[1], + ) + if full_target_shape[0] % 2 != 0: + raise ValueError( + f"Expected even fused dim for {self.megatron_param}, got {full_target_shape}." + ) + if ( + isinstance(expert_weight, torch.Tensor) + and expert_weight.ndim == 3 + and expert_weight.shape[0] == 2 + ): + gate = _align_expert_weight_to_shape( + expert_weight[0], torch.Size(gate_target_shape), "gate" + ) + up = _align_expert_weight_to_shape( + expert_weight[1], torch.Size(gate_target_shape), "up" + ) + else: + fused = _align_expert_weight_to_shape( + cast(torch.Tensor, expert_weight), + torch.Size(full_target_shape), + "gate_up", + ) + gate, up = torch.chunk(fused, 2, dim=0) + return self._gated_mapping.hf_to_megatron( + {"gate": gate, "up": up}, + megatron_module, + ) - global_expert_number = extract_expert_number_from_param(self.megatron_param) - expert_weight = ( - hf_weights[global_expert_number] - if isinstance(hf_weights, torch.Tensor) and hf_weights.ndim >= 3 - else hf_weights - ) - normalized_param = self._normalize_expert_param_name(self.megatron_param) - target_param = get_module_and_param_from_name( - megatron_module, normalized_param - )[1] - full_target_shape = ( - target_param.shape[0] * self.tp_size, - target_param.shape[1], - ) - gate_target_shape = ( - full_target_shape[0] // 2, - full_target_shape[1], - ) - if full_target_shape[0] % 2 != 0: - raise ValueError( - f"Expected even fused dim for {self.megatron_param}, got {full_target_shape}." + class _ArtExpertMLPDownProjMapping(FusedExpertMapping): + def hf_to_megatron( + self, + hf_weights: Any, + megatron_module: Any, + ) -> torch.Tensor: + from megatron.bridge.models.conversion.param_mapping import ( + ColumnParallelMapping, + RowParallelMapping, + _align_expert_weight_to_shape, ) - if ( - isinstance(expert_weight, torch.Tensor) - and expert_weight.ndim == 3 - and expert_weight.shape[0] == 2 - ): - gate = _align_expert_weight_to_shape( - expert_weight[0], torch.Size(gate_target_shape), "gate" + from megatron.bridge.models.conversion.utils import ( + get_module_and_param_from_name, ) - up = _align_expert_weight_to_shape( - expert_weight[1], torch.Size(gate_target_shape), "up" + from megatron.bridge.utils.common_utils import ( + extract_expert_number_from_param, ) - else: - fused = _align_expert_weight_to_shape( - cast(torch.Tensor, expert_weight), + + global_expert_number = extract_expert_number_from_param(self.megatron_param) + expert_weight = _select_qwen35_expert_weight( + hf_weights, + global_expert_number=global_expert_number, + ep_size=int(self.ep_size), + ) + normalized_param = self._normalize_expert_param_name(self.megatron_param) + target_param = get_module_and_param_from_name( + megatron_module, normalized_param + )[1] + if self._mapping is None: + self._detected_type = self._detect_parallelism_type(megatron_module) + self._mapping = self._get_or_create_mapping(self._detected_type) + if isinstance(self._mapping, ColumnParallelMapping): + full_target_shape = ( + target_param.shape[0] * self.tp_size, + target_param.shape[1], + ) + elif isinstance(self._mapping, RowParallelMapping): + full_target_shape = ( + target_param.shape[0], + target_param.shape[1] * self.tp_size, + ) + else: + full_target_shape = tuple(target_param.shape) + aligned = _align_expert_weight_to_shape( + expert_weight, torch.Size(full_target_shape), - "gate_up", + "down_proj", ) - gate, up = torch.chunk(fused, 2, dim=0) - return self._gated_mapping.hf_to_megatron( - {"gate": gate, "up": up}, - megatron_module, - ) + return self._mapping.hf_to_megatron(aligned, megatron_module) + return ( + FusedGatedExpertMapping, + FusedExpertMapping, + _ArtExpertMLPGateUpProjMapping, + _ArtExpertMLPDownProjMapping, + ) -class _ArtExpertMLPDownProjMapping(_BridgeExpertMLPDownProjMapping): - def hf_to_megatron( - self, - hf_weights: torch.Tensor, - megatron_module: Any, - ) -> torch.Tensor: - from megatron.bridge.models.conversion.param_mapping import ( - ColumnParallelMapping, - RowParallelMapping, - _align_expert_weight_to_shape, - ) - from megatron.bridge.models.conversion.utils import ( - get_module_and_param_from_name, - ) - from megatron.bridge.utils.common_utils import ( - extract_expert_number_from_param, - ) - global_expert_number = extract_expert_number_from_param(self.megatron_param) - expert_weight = ( - hf_weights[global_expert_number] if hf_weights.ndim >= 3 else hf_weights - ) - normalized_param = self._normalize_expert_param_name(self.megatron_param) - target_param = get_module_and_param_from_name( - megatron_module, normalized_param - )[1] - if self._mapping is None: - self._detected_type = self._detect_parallelism_type(megatron_module) - self._mapping = self._get_or_create_mapping(self._detected_type) - if isinstance(self._mapping, ColumnParallelMapping): - full_target_shape = ( - target_param.shape[0] * self.tp_size, - target_param.shape[1], - ) - elif isinstance(self._mapping, RowParallelMapping): - full_target_shape = ( - target_param.shape[0], - target_param.shape[1] * self.tp_size, - ) - else: - full_target_shape = tuple(target_param.shape) - aligned = _align_expert_weight_to_shape( - expert_weight, - torch.Size(full_target_shape), - "down_proj", - ) - return self._mapping.hf_to_megatron(aligned, megatron_module) +def _select_qwen35_expert_weight( + hf_weights: Any, + *, + global_expert_number: int, + ep_size: int, +) -> Any: + from art.megatron.runtime.bridge_runtime import ExpertTensorSlice + if isinstance(hf_weights, ExpertTensorSlice): + return hf_weights.get(global_expert_number) + if isinstance(hf_weights, torch.Tensor) and hf_weights.ndim >= 3: + if ep_size > 1: + raise RuntimeError( + "Qwen3.5 EP expert loading expected a sliced fused-expert " + "HF tensor, but received the full all-expert tensor for " + f"global expert {global_expert_number}." + ) + return hf_weights[global_expert_number] + return hf_weights -def _ensure_qwen35_text_only_bridge_registered() -> None: - return None +_QWEN35_TEXT_ONLY_BRIDGE_REGISTERED = False -from megatron.bridge.models.conversion.model_bridge import MegatronModelBridge -from megatron.bridge.models.qwen_vl.qwen35_vl_bridge import ( - _QWEN3_5_DENSE_HF_CLASS_NAME, - _QWEN3_5_MOE_HF_CLASS_NAME, - Qwen35VLBridge, - Qwen35VLMoEBridge, -) -from megatron.bridge.models.qwen_vl.qwen35_vl_provider import ( - Qwen35VLModelProvider, - Qwen35VLMoEModelProvider, -) +def ensure_qwen35_text_only_bridge_registered() -> None: + global _QWEN35_TEXT_ONLY_BRIDGE_REGISTERED + if _QWEN35_TEXT_ONLY_BRIDGE_REGISTERED: + return -@MegatronModelBridge.register_bridge( - source=_QWEN3_5_DENSE_HF_CLASS_NAME, - target=GPTModel, - provider=Qwen35VLModelProvider, - model_type="qwen3_5_moe", -) -class _ArtQwen35DenseTextOnlyBridge(Qwen35VLBridge): - def mapping_registry(self) -> Any: - return _qwen35_text_only_mapping_registry(Qwen35VLBridge) + from megatron.bridge.models.conversion.model_bridge import MegatronModelBridge + from megatron.bridge.models.qwen_vl.qwen35_vl_bridge import ( + _QWEN3_5_DENSE_HF_CLASS_NAME, + _QWEN3_5_MOE_HF_CLASS_NAME, + Qwen35VLBridge, + Qwen35VLMoEBridge, + ) + from megatron.bridge.models.qwen_vl.qwen35_vl_provider import ( + Qwen35VLModelProvider, + Qwen35VLMoEModelProvider, + ) + from megatron.core.models.gpt.gpt_model import GPTModel + @MegatronModelBridge.register_bridge( + source=_QWEN3_5_DENSE_HF_CLASS_NAME, + target=GPTModel, + provider=Qwen35VLModelProvider, + model_type="qwen3_5", + ) + class _ArtQwen35DenseTextOnlyBridge(Qwen35VLBridge): + def mapping_registry(self) -> Any: + return _qwen35_text_only_mapping_registry(Qwen35VLBridge) + + @MegatronModelBridge.register_bridge( + source=_QWEN3_5_MOE_HF_CLASS_NAME, + target=GPTModel, + provider=Qwen35VLMoEModelProvider, + model_type="qwen3_5_moe", + ) + class _ArtQwen35TextOnlyBridge(Qwen35VLMoEBridge): + def mapping_registry(self) -> Any: + return _qwen35_text_only_mapping_registry(Qwen35VLMoEBridge) -@MegatronModelBridge.register_bridge( - source=_QWEN3_5_MOE_HF_CLASS_NAME, - target=GPTModel, - provider=Qwen35VLMoEModelProvider, - model_type="qwen3_5_moe", -) -class _ArtQwen35TextOnlyBridge(Qwen35VLMoEBridge): - def mapping_registry(self) -> Any: - return _qwen35_text_only_mapping_registry(Qwen35VLMoEBridge) + _QWEN35_TEXT_ONLY_BRIDGE_REGISTERED = True def _linear_attention_pattern(provider: Any) -> list[int]: diff --git a/src/art/megatron/model_support/handlers/qwen3_common.py b/src/art/megatron/model_support/handlers/qwen3_common.py index d8cca9754..f00a4fbf8 100644 --- a/src/art/megatron/model_support/handlers/qwen3_common.py +++ b/src/art/megatron/model_support/handlers/qwen3_common.py @@ -1,13 +1,57 @@ +from __future__ import annotations + from typing import Any, Sequence, cast -from megatron.core.models.gpt.gpt_model import GPTModel -import torch -from art.megatron.training.model_chunks import ModelChunks +def _context_parallel_world_size(config: Any) -> int: + from megatron.core import parallel_state as ps + from torch.distributed import is_initialized + + if is_initialized() and ps.model_parallel_is_initialized(): + return int(ps.get_context_parallel_world_size()) + return int(getattr(config, "context_parallel_size", 1) or 1) + + +def _build_absolute_rotary_pos_emb( + module: Any, + *, + max_position: int, + dtype: Any, + device: Any, +) -> Any: + import torch + + rotary_pos_emb = module.rotary_pos_emb + cache = getattr(module, "_art_absolute_rotary_pos_emb_cache", None) + if cache is None: + cache = {} + setattr(module, "_art_absolute_rotary_pos_emb_cache", cache) + cache_key = (str(device), max_position + 1) + cached = cache.get(cache_key) + if cached is not None: + return cached + + freqs = rotary_pos_emb.get_freqs_non_repeated(max_position + 1) + if not rotary_pos_emb.rotary_interleaved: + absolute_rotary_pos_emb = torch.cat((freqs, freqs), dim=-1) + else: + absolute_rotary_pos_emb = torch.stack( + (freqs.view(-1, 1), freqs.view(-1, 1)), + dim=-1, + ).view(freqs.shape[0], -1) + absolute_rotary_pos_emb = absolute_rotary_pos_emb[:, None, None, :].to( + device=device, + dtype=dtype, + ) + cache[cache_key] = absolute_rotary_pos_emb + return absolute_rotary_pos_emb def install_qwen3_text_preprocess_patch(model_chunks: Sequence[Any]) -> None: - for chunk in cast(ModelChunks, list(model_chunks)): + from megatron.core.models.gpt.gpt_model import GPTModel + import torch + + for chunk in list(model_chunks): module: Any = chunk while hasattr(module, "module"): module = module.module @@ -19,15 +63,49 @@ def install_qwen3_text_preprocess_patch(model_chunks: Sequence[Any]) -> None: preprocess = gpt_module._preprocess def preprocess_hook(*args, _preprocess=preprocess, **kwargs): - preproc_output = list(_preprocess(*args, **kwargs)) + position_ids = kwargs.get("position_ids") + rotary_pos_emb = getattr(gpt_module, "rotary_pos_emb", None) + rotary_cp_group = getattr(rotary_pos_emb, "cp_group", None) + config = getattr(gpt_module, "config", None) + cp_world_size = _context_parallel_world_size(config) + uses_dispatched_local_cp_positions = ( + isinstance(position_ids, torch.Tensor) + and position_ids.ndim == 2 + and cp_world_size > 1 + and rotary_cp_group is not None + ) + if uses_dispatched_local_cp_positions: + setattr(rotary_pos_emb, "cp_group", None) + try: + preproc_output = list(_preprocess(*args, **kwargs)) + finally: + if uses_dispatched_local_cp_positions: + setattr(rotary_pos_emb, "cp_group", rotary_cp_group) decoder_input = cast(torch.Tensor, preproc_output[0]) if not decoder_input.requires_grad and decoder_input.is_leaf: decoder_input.requires_grad_(True) - position_ids = cast(torch.Tensor, kwargs["position_ids"]) + position_ids = cast(torch.Tensor, position_ids) table = cast(torch.Tensor, preproc_output[1]) + if table is None: + return tuple(preproc_output) embedding_dim = int(table.shape[-1]) + if ( + rotary_pos_emb is not None + and getattr(gpt_module, "position_embedding_type", None) == "rope" + and cp_world_size > 1 + ): + table_source = _build_absolute_rotary_pos_emb( + gpt_module, + max_position=int(position_ids.max().item()), + dtype=table.dtype, + device=table.device, + ) + else: + table_source = table batch_size, sequence_length = position_ids.shape - gathered = table.view(table.shape[0], embedding_dim).index_select( + gathered = table_source.view( + table_source.shape[0], embedding_dim + ).index_select( 0, position_ids.reshape(-1), ) diff --git a/src/art/megatron/model_support/handlers/qwen3_moe.py b/src/art/megatron/model_support/handlers/qwen3_moe.py index 61aa8fcac..8eb58d28a 100644 --- a/src/art/megatron/model_support/handlers/qwen3_moe.py +++ b/src/art/megatron/model_support/handlers/qwen3_moe.py @@ -3,7 +3,10 @@ import torch -from art.megatron.model_support.handlers.default_dense import DefaultMoeHandler +from art.megatron.model_support.handlers.default_dense import ( + DefaultMoeHandler, + _compile_workaround_flags_for_provider, +) from art.megatron.model_support.handlers.qwen3_common import ( install_qwen3_text_preprocess_patch, ) @@ -12,16 +15,11 @@ _QWEN3_MOE_COMPILE_WORKAROUND_FLAGS = ( "alltoall_dtoh", "alltoall_dispatch_preprocess", + "deepep_dispatch_combine", "deepep_permute_restore", + "te_triton_permute_with_mask_map", ) -_QWEN3_FUSED_MOE_KEY_RE = re.compile( - r"^(?P.*\.mlp\.experts)\." - r"(?:(?Pbase_layer)\.)?(?Plora_[AB])\.weight$" -) -_QWEN3_EXPERT_MOE_KEY_RE = re.compile( - r"^.*\.mlp\.experts\.\d+\." - r"(?:gate_proj|up_proj|down_proj)\.lora_[AB]\.weight$" -) +_QWEN3_MOE_UNCONDITIONAL_COMPILE_WORKAROUND_FLAGS: tuple[str, ...] = () class Qwen3MoeHandler(DefaultMoeHandler): @@ -43,13 +41,28 @@ def compile_workaround_config( self, provider: Any, ) -> CompileWorkaroundConfig: - del provider - return CompileWorkaroundConfig(flags=_QWEN3_MOE_COMPILE_WORKAROUND_FLAGS) + return CompileWorkaroundConfig( + flags=_compile_workaround_flags_for_provider( + provider, + _QWEN3_MOE_COMPILE_WORKAROUND_FLAGS, + ), + unconditional_flags=_QWEN3_MOE_UNCONDITIONAL_COMPILE_WORKAROUND_FLAGS, + ) QWEN3_MOE_HANDLER = Qwen3MoeHandler() +_QWEN3_FUSED_MOE_KEY_RE = re.compile( + r"^(?P.*\.mlp\.experts)\." + r"(?:(?Pbase_layer)\.)?(?Plora_[AB])\.weight$" +) +_QWEN3_EXPERT_MOE_KEY_RE = re.compile( + r"^.*\.mlp\.experts\.\d+\." + r"(?:gate_proj|up_proj|down_proj)\.lora_[AB]\.weight$" +) + + def _qwen3_moe_config(adapter_config: dict[str, Any]) -> dict[str, Any]: config = dict(adapter_config) target_modules = list(config.get("target_modules") or []) @@ -98,12 +111,39 @@ def _expand_fused_moe_lora( f"{prefix}: gate/up lora_A shape {tuple(gate_up_a.shape)} " f"is not divisible by rank {rank}" ) + num_experts = gate_up_a.shape[0] // rank + expected_rank_cols = num_experts * rank + if ( + gate_up_b.shape[1] == expected_rank_cols + and down_b.shape[1] == expected_rank_cols + and gate_up_a.shape[1] == 2 * down_b.shape[0] + and down_a.shape == (expected_rank_cols, gate_up_b.shape[0]) + ): + expanded: dict[str, torch.Tensor] = {} + intermediate = down_b.shape[0] + for expert in range(num_experts): + rows = slice(expert * rank, (expert + 1) * rank) + gate_a, up_a = gate_up_a[rows].split(intermediate, dim=1) + expert_prefix = f"{prefix}.{expert}" + expanded[f"{expert_prefix}.gate_proj.lora_A.weight"] = _clone( + gate_up_b[:, rows].T + ) + expanded[f"{expert_prefix}.gate_proj.lora_B.weight"] = _clone(gate_a.T) + expanded[f"{expert_prefix}.up_proj.lora_A.weight"] = _clone( + gate_up_b[:, rows].T + ) + expanded[f"{expert_prefix}.up_proj.lora_B.weight"] = _clone(up_a.T) + expanded[f"{expert_prefix}.down_proj.lora_A.weight"] = _clone( + down_b[:, rows].T + ) + expanded[f"{expert_prefix}.down_proj.lora_B.weight"] = _clone( + down_a[rows].T + ) + return expanded if gate_up_b.shape[0] % 2 != 0: raise RuntimeError( f"{prefix}: gate/up lora_B rows {gate_up_b.shape[0]} are not even" ) - num_experts = gate_up_a.shape[0] // rank - expected_rank_cols = num_experts * rank intermediate = gate_up_b.shape[0] // 2 if gate_up_b.shape[1] != expected_rank_cols: raise RuntimeError( diff --git a/src/art/megatron/model_support/registry.py b/src/art/megatron/model_support/registry.py index 1837d0090..09b47a8c8 100644 --- a/src/art/megatron/model_support/registry.py +++ b/src/art/megatron/model_support/registry.py @@ -1,11 +1,20 @@ -import importlib +from importlib import import_module from art.megatron.model_support.spec import ( DependencyFloor, ModelSupportHandler, ModelSupportSpec, + NativeVllmLoraStatus, ) +_DEFAULT_DENSE_HANDLER_KEY = "default_dense" +_QWEN3_DENSE_HANDLER_KEY = "qwen3_dense" +_QWEN3_MOE_HANDLER_KEY = "qwen3_moe" +_QWEN3_5_DENSE_HANDLER_KEY = "qwen3_5_dense" +_QWEN3_5_MOE_HANDLER_KEY = "qwen3_5_moe" +_VALIDATED_NATIVE_VLLM_LORA_STATUS: NativeVllmLoraStatus = "validated" +_DISABLED_NATIVE_VLLM_LORA_STATUS: NativeVllmLoraStatus = "disabled" + _DENSE_TARGET_MODULES = ( "q_proj", "k_proj", @@ -44,14 +53,15 @@ DEFAULT_DENSE_SPEC = ModelSupportSpec( key="default_dense", - handler_key="default_dense", + handler_key=_DEFAULT_DENSE_HANDLER_KEY, default_target_modules=_DENSE_TARGET_MODULES, - native_vllm_lora_status="disabled", + native_vllm_lora_status=_DISABLED_NATIVE_VLLM_LORA_STATUS, ) QWEN3_MOE_SPEC = ModelSupportSpec( key="qwen3_moe", - handler_key="qwen3_moe", + handler_key=_QWEN3_MOE_HANDLER_KEY, + is_moe=True, model_names=( "Qwen/Qwen3-30B-A3B", "Qwen/Qwen3-30B-A3B-Base", @@ -59,12 +69,12 @@ "Qwen/Qwen3-235B-A22B-Instruct-2507", ), default_target_modules=_QWEN3_MOE_TARGET_MODULES, - native_vllm_lora_status="validated", + native_vllm_lora_status=_VALIDATED_NATIVE_VLLM_LORA_STATUS, ) QWEN3_DENSE_SPEC = ModelSupportSpec( key="qwen3_dense", - handler_key="qwen3_dense", + handler_key=_QWEN3_DENSE_HANDLER_KEY, model_names=( "Qwen/Qwen3-0.6B", "Qwen/Qwen3-0.6B-Base", @@ -82,19 +92,19 @@ "Qwen/Qwen3-32B-Base", ), default_target_modules=_DENSE_TARGET_MODULES, - native_vllm_lora_status="validated", + native_vllm_lora_status=_VALIDATED_NATIVE_VLLM_LORA_STATUS, ) QWEN3_5_DENSE_SPEC = ModelSupportSpec( key="qwen3_5_dense", - handler_key="qwen3_5_dense", + handler_key=_QWEN3_5_DENSE_HANDLER_KEY, model_names=( "Qwen/Qwen3.5-4B", "Qwen/Qwen3.5-27B", "Qwen/Qwen3.6-27B", ), default_target_modules=_QWEN3_5_DENSE_TARGET_MODULES, - native_vllm_lora_status="validated", + native_vllm_lora_status=_VALIDATED_NATIVE_VLLM_LORA_STATUS, dependency_floor=DependencyFloor( megatron_bridge="e049cc00c24d03e2ae45d2608c7a44e2d2364e3d", ), @@ -102,14 +112,15 @@ QWEN3_5_MOE_SPEC = ModelSupportSpec( key="qwen3_5_moe", - handler_key="qwen3_5_moe", + handler_key=_QWEN3_5_MOE_HANDLER_KEY, + is_moe=True, model_names=( "Qwen/Qwen3.5-35B-A3B", "Qwen/Qwen3.5-397B-A17B", "Qwen/Qwen3.6-35B-A3B", ), default_target_modules=_QWEN3_5_MOE_TARGET_MODULES, - native_vllm_lora_status="validated", + native_vllm_lora_status=_VALIDATED_NATIVE_VLLM_LORA_STATUS, dependency_floor=DependencyFloor( megatron_bridge="e049cc00c24d03e2ae45d2608c7a44e2d2364e3d", ), @@ -138,14 +149,40 @@ for spec in PROBE_ONLY_MODEL_SUPPORT_SPECS for model_name in spec.model_names } -_HANDLER_EXPORTS_BY_KEY = { - "default_dense": ("default_dense", "DEFAULT_DENSE_HANDLER"), - "qwen3_dense": ("qwen3_dense", "QWEN3_DENSE_HANDLER"), - "qwen3_moe": ("qwen3_moe", "QWEN3_MOE_HANDLER"), - "qwen3_5_dense": ("qwen3_5", "QWEN3_5_DENSE_HANDLER"), - "qwen3_5_moe": ("qwen3_5", "QWEN3_5_MOE_HANDLER"), +_HANDLER_IMPORTS: dict[str, tuple[str, str]] = { + _DEFAULT_DENSE_HANDLER_KEY: ( + "art.megatron.model_support.handlers.default_dense", + "DEFAULT_DENSE_HANDLER", + ), + _QWEN3_DENSE_HANDLER_KEY: ( + "art.megatron.model_support.handlers.qwen3_dense", + "QWEN3_DENSE_HANDLER", + ), + _QWEN3_MOE_HANDLER_KEY: ( + "art.megatron.model_support.handlers.qwen3_moe", + "QWEN3_MOE_HANDLER", + ), + _QWEN3_5_DENSE_HANDLER_KEY: ( + "art.megatron.model_support.handlers.qwen3_5", + "QWEN3_5_DENSE_HANDLER", + ), + _QWEN3_5_MOE_HANDLER_KEY: ( + "art.megatron.model_support.handlers.qwen3_5", + "QWEN3_5_MOE_HANDLER", + ), } -_MOE_HANDLER_KEYS = {"qwen3_moe", "qwen3_5_moe"} +_BRIDGE_REGISTRATION_IMPORTS: dict[str, tuple[str, str]] = { + "qwen3_5_dense": ( + "art.megatron.model_support.handlers.qwen3_5", + "ensure_qwen35_text_only_bridge_registered", + ), + "qwen3_5_moe": ( + "art.megatron.model_support.handlers.qwen3_5", + "ensure_qwen35_text_only_bridge_registered", + ), +} +_HANDLERS_BY_KEY: dict[str, ModelSupportHandler] = {} +_REGISTERED_BRIDGE_KEYS: set[str] = set() QWEN3_DENSE_MODELS = frozenset(QWEN3_DENSE_SPEC.model_names) QWEN3_MOE_MODELS = frozenset(QWEN3_MOE_SPEC.model_names) @@ -191,11 +228,35 @@ def get_model_support_handler( def get_model_support_handler_for_spec( spec: ModelSupportSpec, ) -> ModelSupportHandler: - module_name, export_name = _HANDLER_EXPORTS_BY_KEY[spec.handler_key] - return getattr( - importlib.import_module(f"art.megatron.model_support.handlers.{module_name}"), - export_name, - ) + if handler := _HANDLERS_BY_KEY.get(spec.handler_key): + return handler + try: + module_name, attribute_name = _HANDLER_IMPORTS[spec.handler_key] + except KeyError as exc: + raise KeyError( + f"No model support handler registered for {spec.handler_key}" + ) from exc + handler = getattr(import_module(module_name), attribute_name) + if handler.key != spec.handler_key: + raise RuntimeError( + f"Model support handler {module_name}.{attribute_name} has key " + f"{handler.key!r}; expected {spec.handler_key!r}." + ) + _HANDLERS_BY_KEY[spec.handler_key] = handler + return handler + + +def ensure_model_support_bridge_registered_for_spec( + spec: ModelSupportSpec, +) -> None: + if spec.key in _REGISTERED_BRIDGE_KEYS: + return + bridge_registration = _BRIDGE_REGISTRATION_IMPORTS.get(spec.key) + if bridge_registration is not None: + module_name, attribute_name = bridge_registration + ensure_registered = getattr(import_module(module_name), attribute_name) + ensure_registered() + _REGISTERED_BRIDGE_KEYS.add(spec.key) def default_target_modules_for_model( @@ -241,13 +302,10 @@ def model_uses_expert_parallel( *, allow_unvalidated_arch: bool = False, ) -> bool: - return ( - get_model_support_spec( - base_model, - allow_unvalidated_arch=allow_unvalidated_arch, - ).handler_key - in _MOE_HANDLER_KEYS - ) + return get_model_support_spec( + base_model, + allow_unvalidated_arch=allow_unvalidated_arch, + ).is_moe def is_model_support_registered(base_model: str) -> bool: diff --git a/src/art/megatron/model_support/spec.py b/src/art/megatron/model_support/spec.py index d5d37579d..15c6f8d96 100644 --- a/src/art/megatron/model_support/spec.py +++ b/src/art/megatron/model_support/spec.py @@ -13,6 +13,7 @@ "shared_experts", "shared_expert_overlap", ] +ExpertPackedLoraLayout = Literal["expert_rows", "rank_major_expert_cols"] class DependencyFloor(BaseModel): @@ -40,39 +41,29 @@ class ArchitectureReport(BaseModel): unresolved_risks: list[str] = Field(default_factory=list) -class MinimalLayerCoverageReport(BaseModel): - base_model: str - model_key: str - requested_num_layers: int - recommended_min_layers: int - covered: bool - missing_layer_families: list[str] = Field(default_factory=list) - unresolved_risks: list[str] = Field(default_factory=list) - - -class ValidationStageResult(BaseModel): - name: str - passed: bool = False - metrics: dict[str, Any] = Field(default_factory=dict) - artifact_dir: str | None = None - - -class ValidationReport(BaseModel): - base_model: str - model_key: str - dependency_versions: dict[str, str] = Field(default_factory=dict) - stages: list[ValidationStageResult] = Field(default_factory=list) - - class CompileWorkaroundConfig(BaseModel): flags: tuple[str, ...] = () + unconditional_flags: tuple[str, ...] = () shared_expert_state: SharedExpertCompileState = "none" disable_compile: bool = False +class ExpertPackedLoraSlot(BaseModel): + source_projection: str + source_lora: Literal["lora_A", "lora_B"] + output_suffix: str + pack_layout: ExpertPackedLoraLayout + + +class ExpertPackedLoraGroup(BaseModel): + art_group_suffix: str + slots: tuple[ExpertPackedLoraSlot, ...] + + class ModelSupportSpec(BaseModel): key: str handler_key: str + is_moe: bool = False model_names: tuple[str, ...] = () default_target_modules: tuple[str, ...] default_rollout_weights_mode: RolloutWeightsMode = "lora" @@ -127,22 +118,6 @@ def build_adapter_weights_by_base( model_chunks: Sequence[Any], ) -> dict[str, list[Any]]: ... - def hf_tensor_map_to_art_canonical( - self, - hf_tensor_map: dict[str, Any], - *, - expected_keys: set[str], - ) -> dict[str, Any]: - """ - Testing-only hook for canonicalizing raw HuggingFace tensor maps into the - ART tensor-map keyspace expected by model-support probes. - - This currently exists to support validations such as HF parity, where the - raw HF model can expose fused parameter names or layouts that differ from - the canonical names ART compares against. - """ - ... - def to_vllm_lora_tensors( self, tensors: dict[str, Any], @@ -150,6 +125,8 @@ def to_vllm_lora_tensors( adapter_config: dict[str, Any], ) -> tuple[dict[str, Any], dict[str, Any]]: ... + def expert_packed_lora_groups(self) -> tuple[ExpertPackedLoraGroup, ...]: ... + def from_vllm_lora_tensors( self, tensors: dict[str, Any], diff --git a/src/art/megatron/provider.py b/src/art/megatron/provider.py index 34f8e3269..c68b21341 100644 --- a/src/art/megatron/provider.py +++ b/src/art/megatron/provider.py @@ -1,3 +1,6 @@ +from collections.abc import Mapping +import copy +import inspect import os from typing import Any, Literal, cast @@ -7,77 +10,320 @@ apply_flex_dispatcher_backend, ) from megatron.core.transformer.enums import AttnBackend +from pydantic import BaseModel, ConfigDict import torch -from art.megatron.flex_attention import FlexDotProductAttention from art.megatron.model_support.registry import ( + ensure_model_support_bridge_registered_for_spec, get_model_support_handler_for_spec, get_model_support_spec, ) -from art.megatron.provider_common import ( - ProviderBundle, - patch_layer_spec_tree, - resolve_layer_spec, +from art.megatron.model_support.spec import ModelSupportSpec +from art.megatron.runtime.bridge_runtime import install_art_bridge_runtime_patches + +install_art_bridge_runtime_patches() + + +_NONE_ENV_VALUES = {"", "none", "null", "off", "disable", "disabled"} +_TRUE_ENV_VALUES = {"1", "true", "yes", "on"} +_FALSE_ENV_VALUES = {"0", "false", "no", "off"} +_RECOMPUTE_GRANULARITIES = {"full", "selective"} +_RECOMPUTE_METHODS = {"uniform", "block"} +_FLEX_DISPATCHER_BACKENDS = {"deepep", "hybridep"} +_BOOL_ENV_FIELDS = ( + ( + "overlap_moe_expert_parallel_comm", + "ART_MEGATRON_OVERLAP_MOE_EXPERT_PARALLEL_COMM", + ), + ("delay_wgrad_compute", "ART_MEGATRON_DELAY_WGRAD_COMPUTE"), + ( + "ep_overlap_early_attn_memory_release", + "ART_MEGATRON_EP_OVERLAP_EARLY_ATTN_MEMORY_RELEASE", + ), + ("moe_apply_probs_on_input", "ART_MEGATRON_MOE_APPLY_PROBS_ON_INPUT"), + ("bias_activation_fusion", "ART_MEGATRON_BIAS_ACTIVATION_FUSION"), + ( + "fine_grained_activation_offloading", + "ART_MEGATRON_FINE_GRAINED_ACTIVATION_OFFLOADING", + ), + ("moe_shared_expert_overlap", "ART_MEGATRON_MOE_SHARED_EXPERT_OVERLAP"), +) +_INT_ENV_FIELDS = ( + ("tensor_model_parallel_size", "ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE"), + ("context_parallel_size", "ART_MEGATRON_CONTEXT_PARALLEL_SIZE"), + ("pipeline_model_parallel_size", "ART_MEGATRON_PIPELINE_MODEL_PARALLEL_SIZE"), + ( + "virtual_pipeline_model_parallel_size", + "ART_MEGATRON_VIRTUAL_PIPELINE_MODEL_PARALLEL_SIZE", + ), + ("expert_model_parallel_size", "ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE"), + ("recompute_num_layers", "ART_MEGATRON_RECOMPUTE_NUM_LAYERS"), +) +_STR_LIST_ENV_FIELDS = ( + ("offload_modules", "ART_MEGATRON_OFFLOAD_MODULES"), + ("recompute_modules", "ART_MEGATRON_RECOMPUTE_MODULES"), +) +_CHOICE_ENV_FIELDS = ( + ( + "recompute_granularity", + "ART_MEGATRON_RECOMPUTE_GRANULARITY", + _RECOMPUTE_GRANULARITIES, + ), + ("recompute_method", "ART_MEGATRON_RECOMPUTE_METHOD", _RECOMPUTE_METHODS), + ( + "moe_flex_dispatcher_backend", + "ART_MEGATRON_MOE_FLEX_DISPATCHER_BACKEND", + _FLEX_DISPATCHER_BACKENDS, + ), ) -def _env_flag(name: str) -> bool | None: - raw = os.environ.get(name) - if raw is None: +class ProviderBundle(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + provider: Any + bridge: Any + handler: Any + spec: ModelSupportSpec + + +def resolve_layer_spec( + base_layer_spec: Any, + config: Any, + vp_stage: int | None = None, +) -> Any: + module_spec_type = _optional_module_spec_type() + if module_spec_type is not None and isinstance(base_layer_spec, module_spec_type): + return copy.deepcopy(base_layer_spec) + kwargs = ( + {"vp_stage": vp_stage} + if vp_stage in inspect.signature(base_layer_spec).parameters + else {} + ) + return base_layer_spec(config, **kwargs) + + +def patch_core_attention(layer_spec: object, core_attention: object) -> None: + submodules = getattr(layer_spec, "submodules", None) + self_attention = getattr(submodules, "self_attention", None) + attention_submodules = getattr(self_attention, "submodules", None) + if attention_submodules is None or not hasattr( + attention_submodules, + "core_attention", + ): + return + attention_submodules.core_attention = core_attention + + +def patch_layer_spec_tree(layer_spec: object, core_attention: object) -> None: + layer_specs = getattr(layer_spec, "layer_specs", None) + if layer_specs is None: + patch_core_attention(layer_spec, core_attention) + return + for block_layer_spec in layer_specs: + patch_core_attention(block_layer_spec, core_attention) + + +def art_context_parallel_size(config: object) -> int: + configured = int(getattr(config, "context_parallel_size", 1) or 1) + return max(configured, _runtime_context_parallel_size()) + + +def patch_art_flex_attention(layer_spec: object, config: object) -> None: + patch_layer_spec_tree(layer_spec, _art_flex_core_attention(config)) + + +def _art_flex_core_attention(config: object) -> object: + if art_context_parallel_size(config) > 1: + from art.megatron.context_parallel.core_attention import ( + ArtContextParallelCoreAttention, + ) + + return ArtContextParallelCoreAttention + from art.megatron.flex_attn.attention import FlexDotProductAttention + + return FlexDotProductAttention + + +def _runtime_context_parallel_size() -> int: + try: + from megatron.core import parallel_state + + if not parallel_state.model_parallel_is_initialized(): + return 1 + return int(parallel_state.get_context_parallel_world_size()) + except (AssertionError, ImportError, RuntimeError, ValueError): + return 1 + + +def _optional_module_spec_type() -> type[Any] | None: + try: + from megatron.core.transformer.spec_utils import ModuleSpec + except ImportError: return None + return ModuleSpec + + +class _ProviderRuntimeEnv(BaseModel): + model_config = ConfigDict(frozen=True) + + overlap_moe_expert_parallel_comm: bool | None = None + delay_wgrad_compute: bool | None = None + ep_overlap_early_attn_memory_release: bool | None = None + moe_deepep_num_sms: int | None = None + moe_apply_probs_on_input: bool | None = None + bias_activation_fusion: bool | None = None + fine_grained_activation_offloading: bool | None = None + offload_modules: list[str] | None = None + tensor_model_parallel_size: int | None = None + context_parallel_size: int | None = None + pipeline_model_parallel_size: int | None = None + virtual_pipeline_model_parallel_size: int | None = None + expert_model_parallel_size: int | None = None + expert_tensor_parallel_size: int | None = None + recompute_granularity: Literal["full", "selective"] | None = None + recompute_method: Literal["uniform", "block"] | None = None + recompute_num_layers: int | None = None + recompute_modules: list[str] | None = None + moe_shared_expert_overlap: bool | None = None + moe_flex_dispatcher_backend: Literal["deepep", "hybridep"] | None = None + + @classmethod + def from_environ( + cls, + env: Mapping[str, str] | None = None, + ) -> "_ProviderRuntimeEnv": + env = os.environ if env is None else env + values: dict[str, Any] = {} + for field_name, env_name in _BOOL_ENV_FIELDS: + _set_if_found(values, field_name, _env_bool(env, env_name)) + for field_name, env_name in _INT_ENV_FIELDS: + _set_if_found(values, field_name, _env_optional_int(env, env_name)) + for field_name, env_name in _STR_LIST_ENV_FIELDS: + _set_if_found(values, field_name, _env_optional_str_list(env, env_name)) + for field_name, env_name, choices in _CHOICE_ENV_FIELDS: + _set_if_found( + values, + field_name, + _env_optional_choice(env, env_name, choices), + ) + _set_if_found( + values, + "moe_deepep_num_sms", + _env_default_or_even_positive_int( + env, + "ART_MEGATRON_MOE_DEEPEP_NUM_SMS", + ), + ) + _set_if_found( + values, "expert_tensor_parallel_size", _env_expert_tensor_parallel_size(env) + ) + return cls(**values) + + def is_set(self, field_name: str) -> bool: + return field_name in self.model_fields_set + + +def _set_if_found( + values: dict[str, Any], + field_name: str, + parsed: tuple[bool, Any], +) -> None: + found, value = parsed + if found: + values[field_name] = value + + +def _env_bool(env: Mapping[str, str], name: str) -> tuple[bool, bool | None]: + raw = env.get(name) + if raw is None: + return False, None value = raw.strip().lower() - if value in {"1", "true", "yes", "on"}: - return True - if value in {"0", "false", "no", "off"}: - return False + if value in _TRUE_ENV_VALUES: + return True, True + if value in _FALSE_ENV_VALUES: + return True, False raise ValueError(f"{name} must be a boolean-like value, got {raw!r}") -def _env_override_str(name: str) -> tuple[bool, str | None]: - raw = os.environ.get(name) +def _env_optional_str( + env: Mapping[str, str], + name: str, +) -> tuple[bool, str | None]: + raw = env.get(name) if raw is None: return False, None value = raw.strip() - if not value or value.lower() in {"none", "null", "off", "disable", "disabled"}: + if value.lower() in _NONE_ENV_VALUES: return True, None return True, value -def _env_override_int(name: str) -> tuple[bool, int | None]: - found, value = _env_override_str(name) +def _env_optional_int( + env: Mapping[str, str], + name: str, +) -> tuple[bool, int | None]: + found, value = _env_optional_str(env, name) if not found or value is None: return found, None return True, int(value) -def _env_override_str_list(name: str) -> tuple[bool, list[str] | None]: - found, value = _env_override_str(name) +def _env_default_or_even_positive_int( + env: Mapping[str, str], + name: str, +) -> tuple[bool, int | None]: + raw = env.get(name) + if raw is None: + return False, None + value = raw.strip().lower() + if value == "default": + return True, None + try: + parsed = int(raw.strip()) + except ValueError as exc: + raise ValueError( + f"{name} must be 'default' or a positive, even integer, got {raw!r}" + ) from exc + if parsed <= 0 or parsed % 2 != 0: + raise ValueError( + f"{name} must be 'default' or a positive, even integer, got {raw!r}" + ) + return True, parsed + + +def _env_optional_str_list( + env: Mapping[str, str], + name: str, +) -> tuple[bool, list[str] | None]: + found, value = _env_optional_str(env, name) if not found or value is None: return found, None parts = [part.strip() for part in value.split(",")] return True, [part for part in parts if part] -def _env_override_recompute_granularity( +def _env_optional_choice( + env: Mapping[str, str], name: str, -) -> tuple[bool, Literal["full", "selective"] | None]: - found, value = _env_override_str(name) + choices: set[str], +) -> tuple[bool, str | None]: + found, value = _env_optional_str(env, name) if not found or value is None: return found, None - if value not in {"full", "selective"}: - raise ValueError(f"{name} must be one of 'full' or 'selective', got {value!r}") - return True, cast(Literal["full", "selective"], value) + if value not in choices: + expected = ", ".join(repr(choice) for choice in sorted(choices)) + raise ValueError(f"{name} must be one of {expected}, got {value!r}") + return True, value -def _env_override_recompute_method( - name: str, -) -> tuple[bool, Literal["uniform", "block"] | None]: - found, value = _env_override_str(name) - if not found or value is None: - return found, None - if value not in {"uniform", "block"}: - raise ValueError(f"{name} must be one of 'uniform' or 'block', got {value!r}") - return True, cast(Literal["uniform", "block"], value) +def _env_expert_tensor_parallel_size( + env: Mapping[str, str], +) -> tuple[bool, int | None]: + found, value = _env_optional_int(env, "ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE") + if found: + return found, value + return _env_optional_int(env, "ART_MEGATRON_EXPERT_TENSOR_MODEL_PARALLEL_SIZE") def _resolve_default_deepep_num_sms(provider: GPTModelProvider) -> int: @@ -92,8 +338,8 @@ def _resolve_default_deepep_num_sms(provider: GPTModelProvider) -> int: def _apply_default_parallel_topology(provider: GPTModelProvider) -> None: visible_gpu_count = max(torch.cuda.device_count(), 1) - provider.tensor_model_parallel_size = visible_gpu_count - provider.context_parallel_size = 1 + provider.tensor_model_parallel_size = 1 + provider.context_parallel_size = visible_gpu_count provider.pipeline_model_parallel_size = 1 provider.expert_model_parallel_size = ( visible_gpu_count @@ -111,114 +357,126 @@ def _apply_art_training_runtime_prepare_defaults(provider: GPTModelProvider) -> _apply_default_parallel_topology(provider) -def _apply_art_training_runtime_finalize_defaults(provider: GPTModelProvider) -> None: - if provider.expert_model_parallel_size <= 1: +def _apply_art_training_runtime_finalize_defaults( + provider: GPTModelProvider, + runtime_env: _ProviderRuntimeEnv | None = None, +) -> None: + if int(provider.expert_model_parallel_size or 1) <= 1: return - # use DeepEP for MoE expert comm. comm can be the same amount of time as actual MLP - # compute, so these are very beneficial - apply_flex_dispatcher_backend(provider, moe_flex_dispatcher_backend="deepep") - - -def _apply_runtime_env_overrides(provider: GPTModelProvider) -> None: - overlap = _env_flag("ART_MEGATRON_OVERLAP_MOE_EXPERT_PARALLEL_COMM") - if overlap is not None: - provider.overlap_moe_expert_parallel_comm = overlap - - delay_wgrad = _env_flag("ART_MEGATRON_DELAY_WGRAD_COMPUTE") - if delay_wgrad is not None: - provider.delay_wgrad_compute = delay_wgrad - if delay_wgrad: - provider.overlap_moe_expert_parallel_comm = True - - early_attn_release = _env_flag("ART_MEGATRON_EP_OVERLAP_EARLY_ATTN_MEMORY_RELEASE") - if early_attn_release is not None: - provider.ep_overlap_early_attn_memory_release = early_attn_release - - found, deepep_num_sms = _env_override_int("ART_MEGATRON_MOE_DEEPEP_NUM_SMS") - if found and deepep_num_sms is not None: - provider.moe_deepep_num_sms = deepep_num_sms - if "ART_MEGATRON_MOE_DEEPEP_NUM_SMS" not in os.environ: - provider.moe_deepep_num_sms = _resolve_default_deepep_num_sms(provider) + runtime_env = ( + _ProviderRuntimeEnv.from_environ() if runtime_env is None else runtime_env + ) + backend = ( + runtime_env.moe_flex_dispatcher_backend + if runtime_env.is_set("moe_flex_dispatcher_backend") + else "deepep" + ) + if backend is None: + return + # Expert communication is comparable to expert MLP compute, so the ART + # runtime uses Megatron's optimized flex dispatcher instead of all-to-all. + apply_flex_dispatcher_backend(provider, moe_flex_dispatcher_backend=backend) - moe_apply_probs_on_input = _env_flag("ART_MEGATRON_MOE_APPLY_PROBS_ON_INPUT") - if moe_apply_probs_on_input is not None: - provider.moe_apply_probs_on_input = moe_apply_probs_on_input - bias_activation_fusion = _env_flag("ART_MEGATRON_BIAS_ACTIVATION_FUSION") - if bias_activation_fusion is not None: - provider.bias_activation_fusion = bias_activation_fusion +def _normalize_recompute_settings(provider: GPTModelProvider) -> None: + if provider.recompute_granularity is None: + provider.recompute_method = None + provider.recompute_num_layers = None + provider.recompute_modules = [] - fine_grained_activation_offloading = _env_flag( - "ART_MEGATRON_FINE_GRAINED_ACTIVATION_OFFLOADING" - ) - if fine_grained_activation_offloading is not None: - provider.fine_grained_activation_offloading = fine_grained_activation_offloading - offload_modules_found, offload_modules = _env_override_str_list( - "ART_MEGATRON_OFFLOAD_MODULES" +def _apply_runtime_env_overrides( + provider: GPTModelProvider, + runtime_env: _ProviderRuntimeEnv | None = None, +) -> None: + runtime_env = ( + _ProviderRuntimeEnv.from_environ() if runtime_env is None else runtime_env ) - if offload_modules_found: - provider.offload_modules = [] if offload_modules is None else offload_modules - - found, tensor_model_parallel_size = _env_override_int( - "ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE" + _apply_provider_attr_if_value( + provider, + runtime_env, + "overlap_moe_expert_parallel_comm", ) - if found and tensor_model_parallel_size is not None: - provider.tensor_model_parallel_size = tensor_model_parallel_size - - found, expert_model_parallel_size = _env_override_int( - "ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE" + if runtime_env.delay_wgrad_compute is not None: + provider.delay_wgrad_compute = runtime_env.delay_wgrad_compute + if runtime_env.delay_wgrad_compute: + provider.overlap_moe_expert_parallel_comm = True + _apply_provider_attr_if_value( + provider, + runtime_env, + "ep_overlap_early_attn_memory_release", ) - if found and expert_model_parallel_size is not None: - provider.expert_model_parallel_size = expert_model_parallel_size - found, expert_tensor_parallel_size = _env_override_int( - "ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE" - ) - if not found: - found, expert_tensor_parallel_size = _env_override_int( - "ART_MEGATRON_EXPERT_TENSOR_MODEL_PARALLEL_SIZE" + if runtime_env.is_set("moe_deepep_num_sms"): + provider.moe_deepep_num_sms = ( + _resolve_default_deepep_num_sms(provider) + if runtime_env.moe_deepep_num_sms is None + else runtime_env.moe_deepep_num_sms ) - if found and expert_tensor_parallel_size is not None: - provider.expert_tensor_parallel_size = expert_tensor_parallel_size - - recompute_granularity_found, recompute_granularity = ( - _env_override_recompute_granularity("ART_MEGATRON_RECOMPUTE_GRANULARITY") - ) - if recompute_granularity_found: - provider.recompute_granularity = recompute_granularity - - recompute_method_found, recompute_method = _env_override_recompute_method( - "ART_MEGATRON_RECOMPUTE_METHOD" - ) - if recompute_method_found: - provider.recompute_method = recompute_method + else: + provider.moe_deepep_num_sms = _resolve_default_deepep_num_sms(provider) - recompute_num_layers_found, recompute_num_layers = _env_override_int( - "ART_MEGATRON_RECOMPUTE_NUM_LAYERS" + _apply_provider_attr_if_value(provider, runtime_env, "moe_apply_probs_on_input") + _apply_provider_attr_if_value(provider, runtime_env, "bias_activation_fusion") + _apply_provider_attr_if_value( + provider, + runtime_env, + "fine_grained_activation_offloading", ) - if recompute_num_layers_found: - provider.recompute_num_layers = recompute_num_layers + if runtime_env.is_set("offload_modules"): + provider.offload_modules = ( + [] if runtime_env.offload_modules is None else runtime_env.offload_modules + ) - recompute_modules_found, recompute_modules = _env_override_str_list( - "ART_MEGATRON_RECOMPUTE_MODULES" + _apply_provider_attr_if_value(provider, runtime_env, "tensor_model_parallel_size") + _apply_provider_attr_if_value(provider, runtime_env, "context_parallel_size") + _apply_provider_attr_if_value(provider, runtime_env, "pipeline_model_parallel_size") + _apply_provider_attr_if_set( + provider, + runtime_env, + "virtual_pipeline_model_parallel_size", ) - if recompute_modules_found: - provider.recompute_modules = recompute_modules - - shared_expert_overlap = _env_flag("ART_MEGATRON_MOE_SHARED_EXPERT_OVERLAP") - if shared_expert_overlap is not None: - provider.moe_shared_expert_overlap = shared_expert_overlap - - if provider.overlap_moe_expert_parallel_comm: - # EP overlap is incompatible with full recompute in Megatron, so treat - # overlap as the authoritative request even if a launcher exported the - # usual recompute defaults. Selective recompute is still allowed. - provider.moe_shared_expert_overlap = False - provider.recompute_method = None - provider.recompute_num_layers = None - if provider.recompute_granularity != "selective": - provider.recompute_granularity = None + _apply_provider_attr_if_value(provider, runtime_env, "expert_model_parallel_size") + _apply_provider_attr_if_value(provider, runtime_env, "expert_tensor_parallel_size") + _apply_provider_attr_if_set(provider, runtime_env, "recompute_granularity") + _apply_provider_attr_if_set(provider, runtime_env, "recompute_method") + _apply_provider_attr_if_set(provider, runtime_env, "recompute_num_layers") + _apply_provider_attr_if_set(provider, runtime_env, "recompute_modules") + _apply_provider_attr_if_value(provider, runtime_env, "moe_shared_expert_overlap") + _enforce_ep_overlap_recompute_contract(provider) + _normalize_recompute_settings(provider) + + +def _apply_provider_attr_if_value( + provider: GPTModelProvider, + runtime_env: _ProviderRuntimeEnv, + field_name: str, +) -> None: + value = getattr(runtime_env, field_name) + if value is not None: + setattr(provider, field_name, value) + + +def _apply_provider_attr_if_set( + provider: GPTModelProvider, + runtime_env: _ProviderRuntimeEnv, + field_name: str, +) -> None: + if runtime_env.is_set(field_name): + setattr(provider, field_name, getattr(runtime_env, field_name)) + + +def _enforce_ep_overlap_recompute_contract(provider: GPTModelProvider) -> None: + if not provider.overlap_moe_expert_parallel_comm: + return + # EP overlap is incompatible with full recompute in Megatron, so treat + # overlap as the authoritative request even if a launcher exported the + # usual recompute defaults. Selective recompute is still allowed. + provider.moe_shared_expert_overlap = False + provider.recompute_method = None + provider.recompute_num_layers = None + if provider.recompute_granularity != "selective": + provider.recompute_granularity = None def _install_art_training_flex_attention(provider: GPTModelProvider) -> None: @@ -228,7 +486,7 @@ def _flex_attention_layer_spec( config: GPTModelProvider, vp_stage: int | None = None ) -> object: layer_spec = resolve_layer_spec(base_layer_spec, config, vp_stage) - patch_layer_spec_tree(layer_spec, FlexDotProductAttention) + patch_art_flex_attention(layer_spec, config) return layer_spec provider.transformer_layer_spec = cast(Any, _flex_attention_layer_spec) @@ -244,6 +502,7 @@ def _build_provider_bundle( model, allow_unvalidated_arch=allow_unvalidated_arch, ) + ensure_model_support_bridge_registered_for_spec(spec) handler = get_model_support_handler_for_spec(spec) bridge = AutoBridge.from_hf_pretrained( model, @@ -265,6 +524,7 @@ def prepare_provider_bundle( torch_dtype: torch.dtype = torch.bfloat16, allow_unvalidated_arch: bool = False, ) -> ProviderBundle: + runtime_env = _ProviderRuntimeEnv.from_environ() bundle = _build_provider_bundle( model, torch_dtype=torch_dtype, @@ -285,7 +545,7 @@ def prepare_provider_bundle( provider.cross_entropy_fusion_impl = "te" _apply_art_training_runtime_prepare_defaults(provider) bundle.handler.configure_provider_for_runtime(provider) - _apply_runtime_env_overrides(provider) + _apply_runtime_env_overrides(provider, runtime_env) provider.sequence_parallel = provider.tensor_model_parallel_size > 1 _install_art_training_flex_attention(provider) bundle.handler.patch_provider(provider, bundle.bridge) @@ -293,12 +553,70 @@ def prepare_provider_bundle( def finalize_provider_bundle(provider_bundle: ProviderBundle) -> ProviderBundle: - provider = provider_bundle.provider - _apply_art_training_runtime_finalize_defaults(provider) - provider.finalize() + runtime_env = _ProviderRuntimeEnv.from_environ() + provider = cast(GPTModelProvider, provider_bundle.provider) + _apply_art_training_runtime_finalize_defaults(provider, runtime_env) + _finalize_provider_with_art_overrides(provider) + _normalize_recompute_settings(provider) return provider_bundle +def _finalize_provider_with_art_overrides(provider: GPTModelProvider) -> None: + if not _is_art_gdn_context_parallel_provider(provider): + provider.finalize() + return + _validate_art_gdn_context_parallel_provider(provider) + variant = provider.experimental_attention_variant + provider.experimental_attention_variant = None + try: + provider.finalize() + finally: + provider.experimental_attention_variant = variant + + +def _is_art_gdn_context_parallel_provider(provider: GPTModelProvider) -> bool: + return ( + getattr(provider, "experimental_attention_variant", None) == "gated_delta_net" + and int(getattr(provider, "context_parallel_size", 1) or 1) > 1 + ) + + +def _validate_art_gdn_context_parallel_provider(provider: GPTModelProvider) -> None: + required = ( + "linear_attention_freq", + "linear_conv_kernel_dim", + "linear_key_head_dim", + "linear_value_head_dim", + "linear_num_key_heads", + "linear_num_value_heads", + ) + missing = [name for name in required if getattr(provider, name, None) is None] + if missing: + raise ValueError( + "GatedDeltaNet context parallel provider is missing required fields: " + + ", ".join(missing) + ) + raw_linear_num_key_heads = provider.linear_num_key_heads + raw_linear_num_value_heads = provider.linear_num_value_heads + assert raw_linear_num_key_heads is not None + assert raw_linear_num_value_heads is not None + linear_num_key_heads = int(raw_linear_num_key_heads) + linear_num_value_heads = int(raw_linear_num_value_heads) + tensor_model_parallel_size = int(provider.tensor_model_parallel_size) + if linear_num_value_heads % linear_num_key_heads != 0: + raise ValueError( + "linear_num_value_heads must be a multiple of linear_num_key_heads." + ) + if linear_num_key_heads % tensor_model_parallel_size != 0: + raise ValueError( + "linear_num_key_heads must be a multiple of tensor_model_parallel_size." + ) + if linear_num_value_heads % tensor_model_parallel_size != 0: + raise ValueError( + "linear_num_value_heads must be a multiple of tensor_model_parallel_size." + ) + + def get_provider_bundle( model: str, *, diff --git a/src/art/megatron/provider_common.py b/src/art/megatron/provider_common.py deleted file mode 100644 index 308680fc2..000000000 --- a/src/art/megatron/provider_common.py +++ /dev/null @@ -1,55 +0,0 @@ -import copy -import inspect -from typing import Any - -from megatron.bridge import AutoBridge -from megatron.bridge.models.gpt_provider import GPTModelProvider -from megatron.core.transformer.spec_utils import ModuleSpec -from pydantic import BaseModel, ConfigDict, SkipValidation - -from art.megatron.model_support.spec import ModelSupportHandler, ModelSupportSpec - - -class ProviderBundle(BaseModel): - model_config = ConfigDict(arbitrary_types_allowed=True) - - provider: SkipValidation[GPTModelProvider] - bridge: SkipValidation[AutoBridge] - handler: SkipValidation[ModelSupportHandler] - spec: ModelSupportSpec - - -def resolve_layer_spec( - base_layer_spec: Any, - config: Any, - vp_stage: int | None = None, -) -> Any: - if isinstance(base_layer_spec, ModuleSpec): - return copy.deepcopy(base_layer_spec) - kwargs = ( - {"vp_stage": vp_stage} - if vp_stage in inspect.signature(base_layer_spec).parameters - else {} - ) - return base_layer_spec(config, **kwargs) - - -def patch_core_attention(layer_spec: object, core_attention: object) -> None: - submodules = getattr(layer_spec, "submodules", None) - self_attention = getattr(submodules, "self_attention", None) - attention_submodules = getattr(self_attention, "submodules", None) - if attention_submodules is None or not hasattr( - attention_submodules, - "core_attention", - ): - return - attention_submodules.core_attention = core_attention - - -def patch_layer_spec_tree(layer_spec: object, core_attention: object) -> None: - layer_specs = getattr(layer_spec, "layer_specs", None) - if layer_specs is None: - patch_core_attention(layer_spec, core_attention) - return - for block_layer_spec in layer_specs: - patch_core_attention(block_layer_spec, core_attention) diff --git a/src/art/megatron/routing_replay.py b/src/art/megatron/routing_replay.py index 849ce467a..2e4db5c73 100644 --- a/src/art/megatron/routing_replay.py +++ b/src/art/megatron/routing_replay.py @@ -8,6 +8,7 @@ from pathlib import Path import random import re +import types from typing import TYPE_CHECKING, Any, Protocol from pydantic import BaseModel, ConfigDict, model_validator @@ -26,6 +27,11 @@ _ROUTER_LAYER_PATTERN = re.compile(r"decoder\.layers\.(?P\d+)\.mlp\.router$") _TRACE_CHUNK_PREFIX_PATTERN = re.compile(r"^chunk(?P\d+)\.(?P.+)$") logger = logging.getLogger(__name__) +_ACTIVE_ROUTING_REPLAY_CONTROLLER: Any | None = None + + +def _active_routing_replay_controller() -> Any | None: + return _ACTIVE_ROUTING_REPLAY_CONTROLLER def _to_tensor_cpu_contiguous( @@ -87,22 +93,36 @@ class RouterCallRoute(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) expert_indices: torch.Tensor + expert_probs: torch.Tensor | None = None expert_mask: torch.Tensor num_experts: int sample_index: int | None = None micro_slot: int | None = None + rank_token_counts: tuple[int, ...] | None = None @model_validator(mode="after") def _validate(self) -> "RouterCallRoute": self.expert_indices = _to_tensor_cpu_contiguous( self.expert_indices, dtype=torch.int32 ) + if self.expert_probs is not None: + self.expert_probs = _to_tensor_cpu_contiguous( + self.expert_probs, dtype=torch.float32 + ) self.expert_mask = _to_tensor_cpu_contiguous(self.expert_mask, dtype=torch.bool) if self.expert_indices.ndim != 2: raise RuntimeError( "expert_indices must have shape [num_tokens, topk], got " f"{tuple(self.expert_indices.shape)}" ) + if ( + self.expert_probs is not None + and self.expert_probs.shape != self.expert_indices.shape + ): + raise RuntimeError( + "expert_probs shape must match expert_indices shape, got " + f"{tuple(self.expert_probs.shape)} vs {tuple(self.expert_indices.shape)}" + ) if self.expert_mask.shape != self.expert_indices.shape: raise RuntimeError( "expert_mask shape must match expert_indices shape, got " @@ -129,6 +149,18 @@ def _validate(self) -> "RouterCallRoute": self.sample_index = int(self.sample_index) if self.micro_slot is not None: self.micro_slot = int(self.micro_slot) + if self.rank_token_counts is not None: + counts = tuple(int(count) for count in self.rank_token_counts) + if any(count < 0 for count in counts): + raise RuntimeError( + f"rank_token_counts must be non-negative, got {counts}" + ) + if sum(counts) != int(self.expert_indices.shape[0]): + raise RuntimeError( + "rank_token_counts must sum to route token count: " + f"counts={counts}, tokens={int(self.expert_indices.shape[0])}" + ) + self.rank_token_counts = counts return self @property @@ -176,17 +208,49 @@ def _validate(self) -> "StepRoutes": ): raise RuntimeError("global_token_uids must be unique per step") expected_tokens = int(self.global_token_uids.numel()) + token_count_by_call_key: dict[tuple[str, int], int] = {} for router_key, step_router in self.routers.items(): for call_index, route in step_router.calls.items(): - if route.num_global_tokens != expected_tokens: + call_key = self._router_call_key(route) + if call_key is None: + if route.num_global_tokens != expected_tokens: + raise RuntimeError( + "Route token count must match step global_token_uids " + "when no per-micro metadata is present: " + f"router='{router_key}', call={call_index}, " + f"route_tokens={route.num_global_tokens}, " + f"global_token_uids={expected_tokens}" + ) + continue + if route.num_global_tokens > expected_tokens: raise RuntimeError( - "Route token count must match step global_token_uids: " + "Route token count exceeds step global_token_uids span: " f"router='{router_key}', call={call_index}, " - f"route_tokens={route.num_global_tokens}, " + f"call_key={call_key}, route_tokens={route.num_global_tokens}, " f"global_token_uids={expected_tokens}" ) + previous_token_count = token_count_by_call_key.get(call_key) + if ( + previous_token_count is not None + and previous_token_count != route.num_global_tokens + ): + raise RuntimeError( + "Route token count must be consistent for the same micro: " + f"router='{router_key}', call={call_index}, " + f"call_key={call_key}, expected={previous_token_count}, " + f"got={route.num_global_tokens}" + ) + token_count_by_call_key[call_key] = route.num_global_tokens return self + @staticmethod + def _router_call_key(route: RouterCallRoute) -> tuple[str, int] | None: + if route.sample_index is not None: + return ("sample", int(route.sample_index)) + if route.micro_slot is not None: + return ("dummy_micro_slot", int(route.micro_slot)) + return None + class MoeRoutingReplayBundle(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) @@ -267,6 +331,9 @@ def from_dir(cls, bundle_dir: str | Path) -> "MoeRoutingReplayBundle": indices_key = _build_tensor_key( router_key, call_index, "expert_indices" ) + probs_key = _build_tensor_key( + router_key, call_index, "expert_probs" + ) mask_key = _build_tensor_key(router_key, call_index, "expert_mask") missing_keys = [ key @@ -279,10 +346,12 @@ def from_dir(cls, bundle_dir: str | Path) -> "MoeRoutingReplayBundle": ) calls[call_index] = RouterCallRoute( expert_indices=step_tensors[indices_key], + expert_probs=step_tensors.get(probs_key), expert_mask=step_tensors[mask_key], num_experts=int(call_info["num_experts"]), sample_index=call_info.get("sample_index"), micro_slot=call_info.get("micro_slot"), + rank_token_counts=call_info.get("rank_token_counts"), ) routers[router_key] = StepRouterRoutes(calls=calls) steps[step_index] = StepRoutes( @@ -316,6 +385,10 @@ def to_dir(self, bundle_dir: str | Path) -> None: step_tensors[ _build_tensor_key(router_key, call_index, "expert_indices") ] = route.expert_indices + if route.expert_probs is not None: + step_tensors[ + _build_tensor_key(router_key, call_index, "expert_probs") + ] = route.expert_probs step_tensors[ _build_tensor_key(router_key, call_index, "expert_mask") ] = route.expert_mask @@ -324,6 +397,10 @@ def to_dir(self, bundle_dir: str | Path) -> None: call_info["sample_index"] = int(route.sample_index) if route.micro_slot is not None: call_info["micro_slot"] = int(route.micro_slot) + if route.rank_token_counts is not None: + call_info["rank_token_counts"] = [ + int(count) for count in route.rank_token_counts + ] calls_manifest[str(call_index)] = call_info routers_manifest[router_key] = calls_manifest save_file(step_tensors, str(base_dir / step_name)) @@ -596,10 +673,20 @@ def __init__( self._router_last_call_indices: dict[str, int] = {} self._router_last_call_keys: dict[str, tuple[str, int] | None] = {} self._router_reuse_counts: dict[str, int] = {} + self._global_uid_to_row_index: dict[int, int] = {} + self._global_uid_dense_start: int | None = None + self._global_uid_count: int = 0 self._local_router_keys: set[str] = set() self._router_bindings: dict[str, dict[str, Any]] = {} - self._preloaded_targets: dict[tuple[str, int], torch.Tensor] = {} - self._target_buffers: dict[str, torch.Tensor] = {} + self._prepared_uid_sets: dict[str, torch.Tensor] = {} + self._prepared_targets: dict[tuple[str, str, int], torch.Tensor] = {} + self._router_prepared_target_keys: dict[str, tuple[str, int]] = {} + self._target_buffers: dict[tuple[str, str, int], torch.Tensor] = {} + self._host_target_staging: list[torch.Tensor] = [] + self._target_copy_stream: torch.cuda.Stream | None = None + self._target_copy_event: torch.cuda.Event | None = None + self._target_copy_waited: bool = True + self._active_token_uid_key: str | None = None def _target_device(self) -> torch.device: if self._device is not None: @@ -609,6 +696,7 @@ def _target_device(self) -> torch.device: return torch.device("cpu") def install_router_patches(self, model_chunks: list[Any]) -> None: + global _ACTIVE_ROUTING_REPLAY_CONTROLLER if self._router_bindings: return for chunk_index, chunk in enumerate(model_chunks): @@ -645,20 +733,53 @@ def install_router_patches(self, model_chunks: list[Any]) -> None: "RouterReplay instance is already patched: " f"router_key='{router_key}'" ) + if getattr(module, "_art_routing_replay_target_patched", False): + raise RuntimeError( + "Router module routing method is already patched: " + f"router_key='{router_key}'" + ) sequence_parallel = bool(getattr(config, "sequence_parallel", False)) context_parallel_size = int(getattr(config, "context_parallel_size", 1)) topk = int(getattr(module, "topk")) + original_routing = module.routing + + def _routing_with_replay_target( + router_module: Any, + *args: Any, + _controller: MoeRoutingReplayController = self, + _router_key: str = router_key, + _original_routing: Any = original_routing, + **kwargs: Any, + ) -> Any: + del router_module + _controller._prepare_native_target_for_router(_router_key) + return _original_routing(*args, **kwargs) + + module.routing = types.MethodType(_routing_with_replay_target, module) + setattr(module, "_art_routing_replay_target_patched", True) self._router_bindings[router_key] = { "module": module, + "original_routing": original_routing, "router_replay": router_replay, "sequence_parallel": sequence_parallel, "context_parallel_size": context_parallel_size, "topk": topk, } self._local_router_keys.add(router_key) + _ACTIVE_ROUTING_REPLAY_CONTROLLER = self def remove_router_patches(self) -> None: + global _ACTIVE_ROUTING_REPLAY_CONTROLLER + if _ACTIVE_ROUTING_REPLAY_CONTROLLER is self: + _ACTIVE_ROUTING_REPLAY_CONTROLLER = None + for binding in self._router_bindings.values(): + module = binding["module"] + original_routing = binding.get("original_routing") + if original_routing is not None: + module.routing = original_routing + if hasattr(module, "_art_routing_replay_target_patched"): + delattr(module, "_art_routing_replay_target_patched") self._router_bindings.clear() self._local_router_keys.clear() self._target_buffers.clear() @@ -675,24 +796,110 @@ def begin_micro(self, sample_index: int | None, micro_order: int) -> None: "Routing replay expected exactly one router call per local " f"microbatch for router='{router_key}', got {call_indices}" ) - call_index = self._next_route_call_index(router_key) - if call_index != call_indices[0]: - raise RuntimeError( - "Routing replay cursor mismatch while preparing native replay: " - f"router='{router_key}', expected={call_indices[0]}, " - f"actual={call_index}" - ) - target = self._target_for_router_call( - router_key=router_key, - call_index=call_index, + + def set_local_input_token_uids( + self, + local_token_uids: torch.Tensor | None, + ) -> None: + self.prepare_micro_targets({"attention": local_token_uids}) + + def prepare_micro_targets( + self, + token_uid_sets: dict[str, torch.Tensor | None], + *, + active_token_uid_key: str = "attention", + ) -> None: + if self._active_step_routes is None or self._active_micro_order is None: + raise RuntimeError( + "Routing replay target staging requires set_step and begin_micro" + ) + self._reset_staged_micro_targets() + prepared_uid_sets = { + key: self._normalize_token_uids(value) + for key, value in token_uid_sets.items() + if value is not None + } + if not prepared_uid_sets: + raise RuntimeError("Routing replay requires at least one token UID set") + if active_token_uid_key not in prepared_uid_sets: + raise RuntimeError( + "Routing replay active token UID key was not prepared: " + f"key='{active_token_uid_key}', prepared={sorted(prepared_uid_sets)}" ) - router_replay = self._router_bindings[router_key]["router_replay"] - router_replay.set_target_indices( - self._copy_into_stable_target_buffer(router_key, target) + self._prepared_uid_sets = prepared_uid_sets + if not self._local_router_keys: + self._active_token_uid_key = active_token_uid_key + return + for token_uid_key, token_uids in prepared_uid_sets.items(): + for router_key in sorted(self._local_router_keys): + call_indices = self._active_micro_call_indices(router_key) + if len(call_indices) != 1: + raise RuntimeError( + "Routing replay expected exactly one active router call while " + f"staging targets for router='{router_key}', got {call_indices}" + ) + call_index = call_indices[0] + binding = self._router_bindings[router_key] + router_token_uids = self._token_uids_for_router_binding( + token_uids, + sequence_parallel=bool(binding["sequence_parallel"]), + ) + target_cpu = self._explicit_target_for_router_call( + router_key=router_key, + call_index=call_index, + explicit_uids=router_token_uids, + ) + self._stage_prepared_target( + target_key=(token_uid_key, router_key, call_index), + target_cpu=target_cpu, + ) + self._record_target_copy_event() + self.set_active_token_uid_key(active_token_uid_key) + + def set_active_token_uid_key(self, token_uid_key: str) -> None: + if not self._local_router_keys: + self._active_token_uid_key = token_uid_key + return + prepared_keys = { + key for key, _router_key, _call_index in self._prepared_targets.keys() + } + if token_uid_key not in prepared_keys: + raise RuntimeError( + "Routing replay token UID key was not staged for this micro: " + f"key='{token_uid_key}', staged={sorted(prepared_keys)}" ) - router_replay.set_router_replay_action( - _router_replay_classes()[1].REPLAY_FORWARD + self._active_token_uid_key = token_uid_key + + @staticmethod + def _normalize_token_uids(local_token_uids: torch.Tensor) -> torch.Tensor: + if local_token_uids.device.type != "cpu": + raise RuntimeError( + "Routing replay token UIDs must be CPU metadata. Passing CUDA token " + "UIDs would force a host/device synchronization in the model path." ) + return local_token_uids.detach().to(dtype=torch.int64).contiguous().reshape(-1) + + def local_token_uids_for_active_dispatch( + self, + *, + num_local_tokens: int, + sequence_parallel: bool, + ) -> torch.Tensor | None: + if self._active_token_uid_key is None: + return None + token_uids = self._prepared_uid_sets.get(self._active_token_uid_key) + if token_uids is None: + return None + local_uids = self._token_uids_for_router_binding( + token_uids, + sequence_parallel=sequence_parallel, + ) + if int(local_uids.numel()) == int(num_local_tokens): + return local_uids.contiguous() + compact_uids = local_uids[local_uids >= 0] + if int(compact_uids.numel()) == int(num_local_tokens): + return compact_uids.contiguous() + return None def set_step( self, @@ -715,13 +922,24 @@ def set_step( ) self._active_micro_order = None self._active_step_routes = step_routes - self._preloaded_targets = {} + self._reset_staged_micro_targets() self._router_call_cursors = {} self._router_call_sequences = {} self._router_last_call_indices = {} self._router_last_call_keys = {} self._router_reuse_counts = {} - + self._global_uid_count = int(step_routes.global_token_uids.numel()) + self._global_uid_dense_start = self._dense_global_uid_start( + step_routes.global_token_uids + ) + self._global_uid_to_row_index = ( + {} + if self._global_uid_dense_start is not None + else { + int(uid.item()): row_index + for row_index, uid in enumerate(step_routes.global_token_uids) + } + ) for router_key in sorted(self._local_router_keys): if router_key not in step_routes.routers: raise RuntimeError( @@ -749,8 +967,6 @@ def set_step( sample_index=sample_index, global_grad_accumulation_sequences=global_grad_accumulation_sequences, ) - for call_index in self._router_call_sequences[router_key]: - self._preload_target(router_key, call_index) RouterReplay, RouterReplayAction = _router_replay_classes() RouterReplay.clear_global_indices() RouterReplay.set_global_router_replay_action(RouterReplayAction.REPLAY_FORWARD) @@ -791,7 +1007,19 @@ def _reset_step_state(self) -> None: self._router_last_call_indices = {} self._router_last_call_keys = {} self._router_reuse_counts = {} - self._preloaded_targets = {} + self._reset_staged_micro_targets() + self._global_uid_to_row_index = {} + self._global_uid_dense_start = None + self._global_uid_count = 0 + + def _reset_staged_micro_targets(self) -> None: + self._prepared_uid_sets = {} + self._prepared_targets = {} + self._router_prepared_target_keys = {} + self._host_target_staging = [] + self._target_copy_event = None + self._target_copy_waited = True + self._active_token_uid_key = None @staticmethod def _clear_native_router_replay_state() -> None: @@ -799,6 +1027,18 @@ def _clear_native_router_replay_state() -> None: RouterReplay.clear_global_indices() RouterReplay.clear_global_router_replay_action() + @staticmethod + def _dense_global_uid_start(global_token_uids: torch.Tensor) -> int | None: + num_uids = int(global_token_uids.numel()) + if num_uids == 0: + return None + start = int(global_token_uids[0].item()) + if num_uids == 1: + return start + if bool((global_token_uids[1:] == global_token_uids[:-1] + 1).all().item()): + return start + return None + def _build_call_sequence( self, *, @@ -970,6 +1210,15 @@ def _active_micro_call_indices(self, router_key: str) -> list[int]: first_index = call_sequence[cursor] if active_call_key is None: return [first_index] + next_key = self._router_call_key(router_calls[first_index]) + last_index = self._router_last_call_indices.get(router_key) + last_key = self._router_last_call_keys.get(router_key) + if ( + last_index is not None + and last_key == active_call_key + and next_key != active_call_key + ): + return [last_index] indices: list[int] = [] for call_index in call_sequence[cursor:]: if self._router_call_key(router_calls[call_index]) != active_call_key: @@ -1026,40 +1275,41 @@ def _next_route_call_index(self, router_key: str) -> int: ) return call_index - def _preload_target(self, router_key: str, call_index: int) -> None: - key = (router_key, call_index) - if key in self._preloaded_targets: + def _prepare_native_target_for_router(self, router_key: str) -> None: + if ( + self._active_step_routes is None + or self._active_micro_order is None + or self._active_token_uid_key is None + ): + raise RuntimeError( + "Routing replay router call occurred before staged targets were ready: " + f"router='{router_key}'" + ) + call_indices = self._active_micro_call_indices(router_key) + if len(call_indices) != 1: + raise RuntimeError( + "Routing replay expected exactly one active router call while " + f"preparing native replay for router='{router_key}', got {call_indices}" + ) + call_index = self._next_route_call_index(router_key) + if call_index != call_indices[0]: + raise RuntimeError( + "Routing replay cursor mismatch while preparing native replay: " + f"router='{router_key}', expected={call_indices[0]}, " + f"actual={call_index}" + ) + target_key = (self._active_token_uid_key, call_index) + if self._router_prepared_target_keys.get(router_key) == target_key: return - if self._active_step_routes is None: - raise RuntimeError("Routing replay target preload called before set_step") - route = self._active_step_routes.routers[router_key].calls[call_index] - binding = self._router_bindings[router_key] - target = route.expert_indices.to( - device=self._target_device(), - dtype=torch.long, - non_blocking=True, - ) - target = self._slice_target_for_local_rank( - target, - sequence_parallel=bool(binding["sequence_parallel"]), - context_parallel_size=int(binding["context_parallel_size"]), - ).contiguous() - self._preloaded_targets[key] = target - - def _target_for_router_call( - self, - *, - router_key: str, - call_index: int, - ) -> torch.Tensor: - key = (router_key, call_index) - if key not in self._preloaded_targets: + self.wait_for_staged_targets() + staged_key = (self._active_token_uid_key, router_key, call_index) + target = self._prepared_targets.get(staged_key) + if target is None: raise RuntimeError( - "Routing replay target was not preloaded before router execution: " + "Routing replay target was not staged before router execution: " f"step={self._active_step_index}, router='{router_key}', " - f"call={call_index}. begin_micro must be called before forward." + f"call={call_index}, token_uid_key='{self._active_token_uid_key}'" ) - target = self._preloaded_targets[key] topk = int(self._router_bindings[router_key]["topk"]) if int(target.shape[1]) != topk: raise RuntimeError( @@ -1067,46 +1317,153 @@ def _target_for_router_call( f"router='{router_key}', call={call_index}, " f"target_topk={int(target.shape[1])}, router_topk={topk}" ) - return target + router_replay = self._router_bindings[router_key]["router_replay"] + router_replay.set_target_indices(target) + router_replay.set_router_replay_action( + _router_replay_classes()[1].REPLAY_FORWARD + ) + self._router_prepared_target_keys[router_key] = target_key - def _copy_into_stable_target_buffer( - self, router_key: str, target: torch.Tensor + def _explicit_target_for_router_call( + self, + *, + router_key: str, + call_index: int, + explicit_uids: torch.Tensor, ) -> torch.Tensor: - buffer = self._target_buffers.get(router_key) - if ( - buffer is None - or buffer.shape != target.shape - or buffer.device != target.device - ): - buffer = torch.empty_like(target) - self._target_buffers[router_key] = buffer - buffer.copy_(target, non_blocking=True) - return buffer + if self._active_step_routes is None: + raise RuntimeError("Routing replay explicit target used before set_step") + route = self._active_step_routes.routers[router_key].calls[call_index] + local_uids = explicit_uids.reshape(-1).contiguous() + target_cpu = torch.empty( + (int(local_uids.numel()), route.max_topk), + dtype=torch.long, + ) + valid_positions = torch.nonzero(local_uids >= 0, as_tuple=False).reshape(-1) + if int(valid_positions.numel()) > 0: + valid_uids = local_uids[valid_positions] + row_indices = self._row_indices_for_explicit_uids( + valid_uids=valid_uids, + router_key=router_key, + call_index=call_index, + ) + target_cpu[valid_positions] = route.expert_indices.index_select( + 0, + row_indices, + ).to(dtype=torch.long) + invalid_positions = torch.nonzero(local_uids < 0, as_tuple=False).reshape(-1) + if int(invalid_positions.numel()) > 0: + target_cpu[invalid_positions] = _synthetic_replay_rows( + row_positions=invalid_positions, + num_experts=route.num_experts, + topk=route.max_topk, + dtype=torch.long, + seed=(int(self._active_step_index or 0) + 1) * 1_000_003 + + (call_index + 1) * 97_003, + ) + return target_cpu.contiguous() + + def _row_indices_for_explicit_uids( + self, + *, + valid_uids: torch.Tensor, + router_key: str, + call_index: int, + ) -> torch.Tensor: + if self._global_uid_dense_start is not None: + row_indices = valid_uids.to(dtype=torch.long) - self._global_uid_dense_start + out_of_range = (row_indices < 0) | (row_indices >= self._global_uid_count) + if bool(out_of_range.any().item()): + bad_uid = int(valid_uids[out_of_range][0].item()) + raise RuntimeError( + "Explicit routing replay token uid is outside the active dense " + f"step span: step={self._active_step_index}, " + f"router='{router_key}', call={call_index}, uid={bad_uid}" + ) + return row_indices + try: + row_indices = [ + self._global_uid_to_row_index[int(uid)] for uid in valid_uids.tolist() + ] + except KeyError as exc: + raise RuntimeError( + "Explicit routing replay token uid is missing from the active " + f"step map: step={self._active_step_index}, " + f"router='{router_key}', call={call_index}, uid={exc.args[0]}" + ) from exc + return torch.tensor(row_indices, dtype=torch.long) @staticmethod - def _slice_target_for_local_rank( - target: torch.Tensor, + def _token_uids_for_router_binding( + token_uids: torch.Tensor, *, sequence_parallel: bool, - context_parallel_size: int, ) -> torch.Tensor: - candidate = target - if context_parallel_size > 1: - from megatron.core import parallel_state as ps - from megatron.core.utils import get_batch_on_this_cp_rank + if not sequence_parallel: + return token_uids + from megatron.core import parallel_state as ps + + tp_size = int(ps.get_tensor_model_parallel_world_size()) + if tp_size <= 1: + return token_uids + tp_rank = int(ps.get_tensor_model_parallel_rank()) + token_count = int(token_uids.numel()) + local_count = (token_count + tp_size - 1) // tp_size + start = tp_rank * local_count + end = min(start + local_count, token_count) + local_uids = token_uids.new_full((local_count,), -1) + if start < token_count: + real_uids = token_uids[start:end] + local_uids[: int(real_uids.numel())] = real_uids + return local_uids + + def _stage_prepared_target( + self, + *, + target_key: tuple[str, str, int], + target_cpu: torch.Tensor, + ) -> None: + target_cpu = target_cpu.to(dtype=torch.long).contiguous() + device = self._target_device() + if device.type != "cuda": + self._prepared_targets[target_key] = target_cpu + return + if self._target_copy_stream is None: + self._target_copy_stream = torch.cuda.Stream(device=device) + host_target = ( + target_cpu if target_cpu.is_pinned() else target_cpu.pin_memory() + ).contiguous() + self._host_target_staging.append(host_target) + buffer = self._target_buffers.get(target_key) + if ( + buffer is None + or buffer.shape != host_target.shape + or buffer.device != device + or buffer.dtype != torch.long + ): + buffer = torch.empty( + tuple(host_target.shape), + device=device, + dtype=torch.long, + ) + self._target_buffers[target_key] = buffer + with torch.cuda.stream(self._target_copy_stream): + buffer.copy_(host_target, non_blocking=True) + buffer.record_stream(self._target_copy_stream) + self._prepared_targets[target_key] = buffer + self._target_copy_waited = False + + def _record_target_copy_event(self) -> None: + if self._target_copy_stream is None or self._target_copy_waited: + return + self._target_copy_event = torch.cuda.Event() + with torch.cuda.stream(self._target_copy_stream): + self._target_copy_event.record() - if int(ps.get_context_parallel_world_size()) > 1: - candidate = get_batch_on_this_cp_rank( - {"tokens": candidate.view(1, *candidate.shape)} - )["tokens"].reshape(-1, int(candidate.shape[1])) - if sequence_parallel: - from megatron.core import parallel_state as ps - - tp_size = int(ps.get_tensor_model_parallel_world_size()) - tp_rank = int(ps.get_tensor_model_parallel_rank()) if tp_size > 1 else 0 - total_rows = int(candidate.shape[0]) - if tp_size > 1 and total_rows % tp_size == 0: - rows_per_rank = total_rows // tp_size - start = tp_rank * rows_per_rank - candidate = candidate[start : start + rows_per_rank] - return candidate + def wait_for_staged_targets(self) -> None: + if self._target_copy_event is None or self._target_copy_waited: + return + torch.cuda.current_stream(self._target_device()).wait_event( + self._target_copy_event + ) + self._target_copy_waited = True diff --git a/src/art/megatron/runtime/bridge_runtime.py b/src/art/megatron/runtime/bridge_runtime.py index 4412278f0..a5296587f 100644 --- a/src/art/megatron/runtime/bridge_runtime.py +++ b/src/art/megatron/runtime/bridge_runtime.py @@ -22,6 +22,31 @@ import torch +class ExpertTensorSlice: + __slots__ = ("global_start", "global_stop", "tensor") + + def __init__( + self, + tensor: torch.Tensor, + *, + global_start: int, + global_stop: int, + ) -> None: + self.tensor = tensor + self.global_start = int(global_start) + self.global_stop = int(global_stop) + + def get(self, global_expert: int) -> torch.Tensor: + global_expert = int(global_expert) + if not self.global_start <= global_expert < self.global_stop: + raise RuntimeError( + "expert slice cache miss for global expert " + f"{global_expert}; cached range is " + f"[{self.global_start}, {self.global_stop})" + ) + return self.tensor[global_expert - self.global_start] + + def _pin_cpu_tensor(tensor: torch.Tensor) -> torch.Tensor: if tensor.device.type != "cpu" or not torch.cuda.is_available(): return tensor @@ -43,6 +68,8 @@ def _iter_hf_param_names(hf_param: Any) -> Iterable[str]: def _needs_local_hf_prefetch(task: Any) -> bool: if task is None or task.megatron_module is None: return False + if _needs_expert_slice_prefetch(task): + return False mapping = task.mapping # ART Qwen3.5 expert mappings slice the full HF expert tensor before # delegating to the inner TP mapping, so every ETP rank needs the source. @@ -59,21 +86,91 @@ def _needs_local_hf_prefetch(task: Any) -> bool: return int(getattr(mapping, "tp_rank", 0)) == 0 +def _needs_expert_slice_prefetch(task: Any) -> bool: + mapping = task.mapping + return ( + int(getattr(mapping, "ep_size", 1)) > 1 + and bool(getattr(mapping, "is_expert", False)) + and bool(getattr(mapping, "is_grouped_export", False)) + and isinstance(getattr(mapping, "hf_param", None), str) + ) + + +def _expert_slice_range(task: Any) -> tuple[int, int]: + mapping = task.mapping + config = getattr(task.megatron_module, "config", None) + num_experts = int(getattr(config, "num_moe_experts", 0) or 0) + ep_size = int(getattr(mapping, "ep_size", 1)) + ep_rank = int(getattr(mapping, "ep_rank", 0)) + if num_experts <= 0 or ep_size <= 1 or num_experts % ep_size != 0: + raise RuntimeError( + "cannot slice fused expert HF weights with " + f"num_experts={num_experts}, ep_size={ep_size}" + ) + experts_per_rank = num_experts // ep_size + start = ep_rank * experts_per_rank + return start, start + experts_per_rank + + +def _load_hf_tensor_slice( + hf_state_dict: Mapping[str, torch.Tensor], + key: str, + *, + start: int, + stop: int, +) -> torch.Tensor: + source = getattr(hf_state_dict, "source", None) + if source is None or not hasattr(source, "key_to_filename_map"): + raise RuntimeError( + "fused expert EP loading requires a safetensors-backed HF state " + f"dict for key {key!r}" + ) + key_to_filename = source.key_to_filename_map + if key not in key_to_filename: + raise KeyError(f"HF tensor key {key!r} not found in safetensors index") + from safetensors import safe_open + + file_path = source.path / key_to_filename[key] + with safe_open(file_path, framework="pt", device="cpu") as handle: + tensor_slice = handle.get_slice(key) + shape = tuple(int(dim) for dim in tensor_slice.get_shape()) + if not shape or start < 0 or stop > shape[0] or start >= stop: + raise RuntimeError( + f"invalid expert slice [{start}, {stop}) for {key!r} with shape {shape}" + ) + index = (slice(start, stop),) + (slice(None),) * (len(shape) - 1) + return tensor_slice[index] + + def load_unique_hf_keys_once( tasks: Iterable[Any], hf_state_dict: Mapping[str, torch.Tensor], -) -> dict[str, torch.Tensor]: +) -> dict[str, torch.Tensor | ExpertTensorSlice]: + task_list = list(tasks) keys = sorted( { key - for task in tasks + for task in task_list if _needs_local_hf_prefetch(task) for key in _iter_hf_param_names(task.mapping.hf_param) } ) - if not keys: - return {} - if hasattr(hf_state_dict, "__getitem__"): + expert_slice_ranges: dict[str, tuple[int, int]] = {} + for task in task_list: + if task is None or task.megatron_module is None: + continue + if not _needs_expert_slice_prefetch(task): + continue + start, stop = _expert_slice_range(task) + key = cast(str, task.mapping.hf_param) + previous = expert_slice_ranges.get(key) + expert_slice_ranges[key] = ( + (start, stop) + if previous is None + else (min(previous[0], start), max(previous[1], stop)) + ) + cache: dict[str, torch.Tensor | ExpertTensorSlice] = {} + if keys and hasattr(hf_state_dict, "__getitem__"): hf_state_dict_getter = cast(Any, hf_state_dict) loaded = ( hf_state_dict_getter[keys] @@ -82,23 +179,39 @@ def load_unique_hf_keys_once( ) else: loaded = {key: hf_state_dict[key] for key in keys} - return { - key: _pin_cpu_tensor(value) - for key, value in cast(Mapping[str, torch.Tensor], loaded).items() - } + cache.update( + { + key: _pin_cpu_tensor(value) + for key, value in cast(Mapping[str, torch.Tensor], loaded).items() + } + ) + for key, (start, stop) in expert_slice_ranges.items(): + cache[key] = ExpertTensorSlice( + _pin_cpu_tensor( + _load_hf_tensor_slice( + hf_state_dict, + key, + start=start, + stop=stop, + ) + ), + global_start=start, + global_stop=stop, + ) + return cache -class _CachedStateLookup(Mapping[str, torch.Tensor]): +class _CachedStateLookup(Mapping[str, torch.Tensor | ExpertTensorSlice]): def __init__( self, *, - cache: Mapping[str, torch.Tensor], + cache: Mapping[str, torch.Tensor | ExpertTensorSlice], source: Mapping[str, torch.Tensor], ) -> None: self._cache = cache self._source = source - def __getitem__(self, key: str) -> torch.Tensor: + def __getitem__(self, key: str) -> torch.Tensor | ExpertTensorSlice: if key in self._cache: return self._cache[key] return _pin_cpu_tensor(self._source[key]) @@ -338,7 +451,7 @@ def _optimized_load_weights_hf_to_megatron( if task is None or task.megatron_module is None: continue hf_weights = self.maybe_modify_loaded_hf_weight( - task.mapping.hf_param, cached_state + task.mapping.hf_param, cast(Mapping[str, torch.Tensor], cached_state) ) converted_weights = task.mapping.hf_to_megatron( hf_weights, task.megatron_module @@ -378,6 +491,9 @@ def _optimized_load_weights_hf_to_megatron( def install_art_bridge_runtime_patches() -> None: from megatron.bridge.models import model_provider as model_provider_module + _patch_router_gating_linear_empty_input() + _patch_bias_swiglu_empty_input() + _patch_moe_unpermute_empty_input() if not getattr( model_provider_module.get_model, "__art_meta_materialization__", False ): @@ -405,3 +521,132 @@ def install_art_bridge_runtime_patches() -> None: "load_weights_hf_to_megatron", _optimized_load_weights_hf_to_megatron, ) + + +def _patch_router_gating_linear_empty_input() -> None: + from megatron.core.transformer.moe import moe_utils, router + + if getattr(moe_utils.router_gating_linear, "__art_empty_safe__", False): + return + + original_router_gating_linear = moe_utils.router_gating_linear + + def _router_gating_linear_empty_safe( + inp: torch.Tensor, + weight: torch.Tensor, + bias: torch.Tensor | None, + router_dtype: torch.dtype, + ) -> torch.Tensor: + if int(inp.numel()) != 0: + return original_router_gating_linear(inp, weight, bias, router_dtype) + zero = inp.to(router_dtype).sum() * 0.0 + weight.to(router_dtype).sum() * 0.0 + if bias is not None: + zero = zero + bias.to(router_dtype).sum() * 0.0 + return zero.expand(*inp.shape[:-1], int(weight.shape[0])) + + setattr(_router_gating_linear_empty_safe, "__art_empty_safe__", True) + setattr(moe_utils, "router_gating_linear", _router_gating_linear_empty_safe) + setattr(router, "router_gating_linear", _router_gating_linear_empty_safe) + + +def _patch_bias_swiglu_empty_input() -> None: + from megatron.core.fusions import fused_bias_swiglu + from megatron.core.transformer import mlp + from megatron.core.transformer.moe import experts, shared_experts + + if getattr(fused_bias_swiglu.bias_swiglu_impl, "__art_empty_safe__", False): + return + + original_bias_swiglu_impl = fused_bias_swiglu.bias_swiglu_impl + original_weighted_bias_swiglu_impl = fused_bias_swiglu.weighted_bias_swiglu_impl + + def _empty_swiglu_output( + input: torch.Tensor, + bias: torch.Tensor | None = None, + weights: torch.Tensor | None = None, + ) -> torch.Tensor: + output_shape = (*input.shape[:-1], int(input.shape[-1]) // 2) + zero = input.sum() * 0.0 + if bias is not None: + zero = zero + bias.to(dtype=input.dtype).sum() * 0.0 + if weights is not None: + zero = zero + weights.to(dtype=input.dtype).sum() * 0.0 + return zero.expand(output_shape).clone() + + def _bias_swiglu_empty_safe( + input: torch.Tensor, + bias: torch.Tensor | None, + fp8_input_store: bool = False, + cpu_offload_input: bool = False, + ) -> torch.Tensor: + if int(input.numel()) != 0: + return original_bias_swiglu_impl( + input, bias, fp8_input_store, cpu_offload_input + ) + return _empty_swiglu_output(input, bias=bias) + + def _weighted_bias_swiglu_empty_safe( + input: torch.Tensor, + bias: torch.Tensor | None, + weights: torch.Tensor, + fp8_input_store: bool = False, + ) -> torch.Tensor: + if int(input.numel()) != 0: + return original_weighted_bias_swiglu_impl( + input, bias, weights, fp8_input_store + ) + return _empty_swiglu_output(input, bias=bias, weights=weights) + + setattr(_bias_swiglu_empty_safe, "__art_empty_safe__", True) + setattr(_weighted_bias_swiglu_empty_safe, "__art_empty_safe__", True) + setattr(fused_bias_swiglu, "bias_swiglu_impl", _bias_swiglu_empty_safe) + setattr( + fused_bias_swiglu, + "weighted_bias_swiglu_impl", + _weighted_bias_swiglu_empty_safe, + ) + setattr(mlp, "bias_swiglu_impl", _bias_swiglu_empty_safe) + setattr(mlp, "weighted_bias_swiglu_impl", _weighted_bias_swiglu_empty_safe) + setattr(experts, "weighted_bias_swiglu_impl", _weighted_bias_swiglu_empty_safe) + setattr(shared_experts, "bias_swiglu_impl", _bias_swiglu_empty_safe) + + +def _patch_moe_unpermute_empty_input() -> None: + from megatron.core.transformer.moe import moe_utils, token_dispatcher + + if getattr(moe_utils.unpermute, "__art_empty_safe__", False): + return + + original_unpermute = moe_utils.unpermute + + def _unpermute_empty_safe( + permuted_tokens: torch.Tensor, + sorted_indices: torch.Tensor, + restore_shape: torch.Size, + probs: torch.Tensor | None = None, + routing_map: torch.Tensor | None = None, + fused: bool = False, + drop_and_pad: bool = False, + pad_offsets: torch.Tensor | None = None, + ) -> torch.Tensor: + if int(permuted_tokens.numel()) != 0: + return original_unpermute( + permuted_tokens, + sorted_indices, + restore_shape, + probs=probs, + routing_map=routing_map, + fused=fused, + drop_and_pad=drop_and_pad, + pad_offsets=pad_offsets, + ) + zero = ( + permuted_tokens.sum() * 0.0 + sorted_indices.sum().to(permuted_tokens) * 0.0 + ) + if probs is not None: + zero = zero + probs.to(dtype=permuted_tokens.dtype).sum() * 0.0 + return zero.expand(tuple(int(dim) for dim in restore_shape)).clone() + + setattr(_unpermute_empty_safe, "__art_empty_safe__", True) + setattr(moe_utils, "unpermute", _unpermute_empty_safe) + setattr(token_dispatcher, "unpermute", _unpermute_empty_safe) diff --git a/src/art/megatron/runtime/client.py b/src/art/megatron/runtime/client.py index 25b683911..99dd16c14 100644 --- a/src/art/megatron/runtime/client.py +++ b/src/art/megatron/runtime/client.py @@ -4,9 +4,7 @@ import os from typing import Any, AsyncIterator -from art.megatron.weights.merge import merge_lora_adapter - -from .jobs import DEFAULT_JOBS_DIR, MegatronJob, MegatronSyncJob, dump_megatron_job +from .jobs import DEFAULT_JOBS_DIR, MegatronJob, dump_megatron_job DEFAULT_TRAINING_LOG_DIR = "/tmp/megatron_training_logs" @@ -35,7 +33,6 @@ async def stream_megatron_job( job: MegatronJob, *, job_path: str, - merge_output_path: str | None = None, process: Any | None = None, process_log_path: str | None = None, poll_interval: float = 0.1, @@ -60,12 +57,6 @@ async def stream_megatron_job( if not (line := line.strip()): continue if line == "all done": - if not isinstance(job, MegatronSyncJob): - merge_lora_adapter( - job.lora_path, - output_dir=merge_output_path, - allow_unvalidated_arch=job.allow_unvalidated_arch, - ) return num_lines += 1 yield json.loads(line) diff --git a/src/art/megatron/service.py b/src/art/megatron/service.py index dae9a26fb..cd14be046 100644 --- a/src/art/megatron/service.py +++ b/src/art/megatron/service.py @@ -1,13 +1,12 @@ import asyncio +from collections.abc import Mapping from dataclasses import dataclass, field import gc import importlib -import json import os from pathlib import Path import shutil import socket -import subprocess import sys from typing import Any, AsyncIterator, Literal, TypedDict, cast @@ -20,22 +19,18 @@ from ..local.checkpoints import get_last_checkpoint_dir from ..preprocessing.pack import DiskPackedTensors from ..preprocessing.tokenize import SFTBatch -from ..utils.convert_moe_lora import convert_checkpoint_if_needed +from ..types import MegatronTopologyConfig from ..utils.get_model_step import get_step_from_dir from ..utils.lifecycle import ( ChildProcessSupervisor, ServiceLifecycle, managed_process_cmd, terminate_asyncio_process_group, - terminate_popen_process_group, ) from ..utils.output_dirs import get_step_checkpoint_dir from ..vllm_runtime import ( + ManagedVllmRuntime, VllmRuntimeLaunchConfig, - build_vllm_runtime_server_cmd, - get_vllm_runtime_nccl_so_path, - get_vllm_runtime_working_dir, - wait_for_vllm_runtime, ) from .lora import LORA_ALPHA, default_lora_rank_for_handler from .model_support.lora_disk import normalize_lora_checkpoint_to_vllm @@ -60,6 +55,7 @@ safetensors = importlib.import_module("safetensors") safe_open = safetensors.safe_open +OFFLOAD_BETWEEN_JOBS_ENV = "ART_MEGATRON_OFFLOAD_BETWEEN_JOBS" def gc_and_empty_cuda_cache(n: int = 3) -> None: @@ -76,20 +72,21 @@ class _RuntimeRequestKwargs(TypedDict, total=False): def create_identity_lora( base_model: str, lora_path: str, - lora_config: dev.LoRAConfig | None = None, + rank: int | None = None, + lora_alpha: int = LORA_ALPHA, random_state: int | None = None, allow_unvalidated_arch: bool = False, ) -> None: """Create an identity LoRA adapter for a Megatron model. - For MoE models, this targets fused expert parameters and converts them to - per-expert format. The conversion swaps lora_A/lora_B, producing A=zeros and - B=Kaiming — which is critical for stable training when alpha/rank is large. + For MoE models, this targets fused expert parameters and lets the model + support handler normalize the saved PEFT tensors to vLLM layout. Args: base_model: HuggingFace model identifier. lora_path: Directory to save the adapter files. - lora_config: Normalized ART LoRA configuration. + rank: LoRA rank. Defaults to rank 1 for MoE models and rank 8 for dense models. + lora_alpha: LoRA alpha scaling factor. """ from unittest.mock import patch @@ -101,16 +98,13 @@ def create_identity_lora( if random_state is not None: torch.manual_seed(random_state) - resolved_lora_config = lora_config or dev.LoRAConfig() - target_modules = resolved_lora_config.get( - "target_modules", default_target_modules(base_model) - ) + target_modules = default_target_modules(base_model) handler = get_model_support_handler( base_model, allow_unvalidated_arch=allow_unvalidated_arch, ) - rank = resolved_lora_config.get("rank", default_lora_rank_for_handler(handler)) - alpha = resolved_lora_config.get("alpha", LORA_ALPHA) + if rank is None: + rank = default_lora_rank_for_handler(handler) base_config = AutoConfig.from_pretrained(base_model, trust_remote_code=True) model_config = handler.identity_lora_model_config(base_config) with init_empty_weights(): @@ -119,10 +113,10 @@ def create_identity_lora( ) model.name_or_path = base_model - peft_lora_config = LoraConfig( + lora_config = LoraConfig( base_model_name_or_path=base_model, r=rank, - lora_alpha=alpha, + lora_alpha=lora_alpha, target_modules=[], target_parameters=handler.identity_lora_target_parameters( model, @@ -143,16 +137,15 @@ def _skip_meta_to( return orig_to(module, *args, **kwargs) with patch.object(torch.nn.Module, "to", _skip_meta_to): - peft_model = get_peft_model(model, peft_lora_config) + peft_model = get_peft_model(model, lora_config) os.makedirs(lora_path, exist_ok=True) peft_model.save_pretrained(lora_path) - convert_checkpoint_if_needed(lora_path) final_config = LoraConfig( base_model_name_or_path=base_model, r=rank, - lora_alpha=alpha, + lora_alpha=lora_alpha, target_modules=target_modules, bias="none", ).to_dict() @@ -179,14 +172,13 @@ class MegatronService: _megatron_process: asyncio.subprocess.Process | None = None _megatron_log_file: Any = None _megatron_log_path: str | None = None - _vllm_process: subprocess.Popen[Any] | None = None - _vllm_log_file: Any = None - _vllm_log_path: str | None = None - _vllm_host: str = "127.0.0.1" - _vllm_port: int = 0 - _vllm_api_key: str | None = None - _vllm_nccl_so_path: str | None = None + _vllm_runtime: ManagedVllmRuntime = field( + default_factory=ManagedVllmRuntime, + init=False, + repr=False, + ) _merged_weight_transfer_init_info: MergedWeightTransferInitInfo | None = None + _active_megatron_topology: MegatronTopologyConfig | None = None _lifecycle: ServiceLifecycle = field( default_factory=ServiceLifecycle, init=False, @@ -221,19 +213,34 @@ def rollout_weights_mode(self) -> Literal["lora", "merged"]: @property def _vllm_base_url(self) -> str: - return f"http://{self._vllm_host}:{self._vllm_port}" + return self._vllm_runtime.base_url - def _megatron_random_state(self) -> int | None: - random_state = self._lora_config.get("random_state") or self.config.get( - "init_args", {} - ).get("random_state") - if random_state is not None: - return int(random_state) - return None + @property + def _vllm_host(self) -> str: + return self._vllm_runtime.host + + @property + def _vllm_port(self) -> int: + return self._vllm_runtime.port + + @_vllm_port.setter + def _vllm_port(self, port: int) -> None: + self._vllm_runtime.port = port @property - def _lora_config(self) -> dev.LoRAConfig: - return cast(dev.BackendModelConfig, self.config).get("lora_config", {}) + def _vllm_api_key(self) -> str | None: + return self._vllm_runtime.api_key + + @property + def _vllm_nccl_so_path(self) -> str | None: + return self._vllm_runtime.nccl_so_path + + def _megatron_random_state(self) -> int | None: + for config_key in ("peft_args", "init_args"): + random_state = self.config.get(config_key, {}).get("random_state") + if random_state is not None: + return int(random_state) + return None @property def _allow_unvalidated_arch(self) -> bool: @@ -315,6 +322,40 @@ def _allocate_master_port(self) -> int: sock.bind(("", 0)) return int(sock.getsockname()[1]) + @staticmethod + def _resolve_megatron_topology( + raw_topology: Mapping[str, int | None] | MegatronTopologyConfig | None, + ) -> MegatronTopologyConfig | None: + if raw_topology is None: + return None + if isinstance(raw_topology, MegatronTopologyConfig): + return raw_topology + return MegatronTopologyConfig.model_validate(raw_topology) + + @staticmethod + def _megatron_topology_env(topology: MegatronTopologyConfig) -> dict[str, str]: + env = { + "ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE": str(topology.tp), + "ART_MEGATRON_CONTEXT_PARALLEL_SIZE": str(topology.cp), + "ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE": str(topology.ep), + "ART_MEGATRON_PIPELINE_MODEL_PARALLEL_SIZE": str(topology.pp), + "ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE": str(topology.etp), + } + if topology.vpp is not None: + env["ART_MEGATRON_VIRTUAL_PIPELINE_MODEL_PARALLEL_SIZE"] = str(topology.vpp) + return env + + @staticmethod + def _megatron_topology_env_names() -> tuple[str, ...]: + return ( + "ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE", + "ART_MEGATRON_CONTEXT_PARALLEL_SIZE", + "ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE", + "ART_MEGATRON_PIPELINE_MODEL_PARALLEL_SIZE", + "ART_MEGATRON_VIRTUAL_PIPELINE_MODEL_PARALLEL_SIZE", + "ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE", + ) + def _install_parent_signal_cleanup(self) -> None: self._lifecycle.install_parent_cleanup(self.close) @@ -386,14 +427,11 @@ def _default_lora_adapter_config(self) -> LoraConfig: self.base_model, allow_unvalidated_arch=self._allow_unvalidated_arch, ) - lora_config = self._lora_config return LoraConfig( base_model_name_or_path=self.base_model, - r=lora_config.get("rank", default_lora_rank_for_handler(handler)), - lora_alpha=lora_config.get("alpha", LORA_ALPHA), - target_modules=lora_config.get( - "target_modules", default_target_modules(self.base_model) - ), + r=default_lora_rank_for_handler(handler), + lora_alpha=LORA_ALPHA, + target_modules=default_target_modules(self.base_model), bias="none", ) @@ -413,7 +451,6 @@ def _create_identity_lora(self, lora_path: str) -> None: create_identity_lora( self.base_model, lora_path, - lora_config=self._lora_config, random_state=self._megatron_random_state(), allow_unvalidated_arch=self._allow_unvalidated_arch, ) @@ -502,91 +539,25 @@ async def _start_vllm_subprocess( port: int, config: dev.OpenAIServerConfig | None, ) -> tuple[str, int]: - import httpx - self._raise_if_child_failed() server_args = self._runtime_server_args(config) - api_key = server_args.get("api_key") - self._vllm_api_key = api_key if isinstance(api_key, str) else None - self._vllm_nccl_so_path = ( - str(get_vllm_runtime_nccl_so_path()) - if self.rollout_weights_mode == "merged" - else None - ) - cmd = build_vllm_runtime_server_cmd( - VllmRuntimeLaunchConfig( + return await self._vllm_runtime.start( + launch_config=VllmRuntimeLaunchConfig( base_model=self.base_model, port=port, - host=self._vllm_host, + host=self._vllm_runtime.host, cuda_visible_devices=self._runtime_cuda_visible_devices(), lora_path=lora_path, served_model_name=f"{self.model_name}@{self._latest_step}", rollout_weights_mode=self.rollout_weights_mode, engine_args=self._runtime_engine_args(config), server_args=server_args, - ) - ) - - log_dir = os.path.join(self.output_dir, "logs") - os.makedirs(log_dir, exist_ok=True) - self._vllm_log_path = os.path.join(log_dir, "vllm-runtime.log") - self._vllm_log_file = open(self._vllm_log_path, "w", buffering=1) - self._vllm_process = subprocess.Popen( - managed_process_cmd(cmd), - cwd=str(get_vllm_runtime_working_dir()), - env=os.environ.copy(), - stdout=self._vllm_log_file, - stderr=subprocess.STDOUT, - bufsize=1, - start_new_session=True, - ) - self._install_parent_signal_cleanup() - self._vllm_port = port - - timeout = float(os.environ.get("ART_DEDICATED_VLLM_TIMEOUT", 1200)) - async with httpx.AsyncClient() as client: - try: - await wait_for_vllm_runtime( - process=self._vllm_process, - host=self._vllm_host, - port=self._vllm_port, - timeout=timeout, - ) - except TimeoutError as exc: - self._stop_vllm_subprocess() - raise TimeoutError( - f"vLLM subprocess did not become ready within {timeout}s. " - f"Check logs at {log_dir}/vllm-runtime.log" - ) from exc - except RuntimeError as exc: - returncode = self._vllm_process.returncode - self._stop_vllm_subprocess() - raise RuntimeError( - f"vLLM subprocess exited with code {returncode}. " - f"Check logs at {log_dir}/vllm-runtime.log" - ) from exc - - try: - response = await client.get( - f"{self._vllm_base_url}/v1/models", - **self._runtime_request_kwargs(), - timeout=5.0, - ) - response.raise_for_status() - except httpx.HTTPError as exc: - self._stop_vllm_subprocess() - raise RuntimeError( - "vLLM passed /health but /v1/models was not reachable. " - f"Check logs at {log_dir}/vllm-runtime.log" - ) from exc - assert self._vllm_process is not None - assert self._vllm_log_path is not None - self._child_processes.watch_popen( - "vLLM runtime", - self._vllm_process, - log_path=self._vllm_log_path, + ), + output_dir=self.output_dir, + child_processes=self._child_processes, + install_parent_cleanup=self._install_parent_signal_cleanup, + cleanup_on_error=self._stop_vllm_subprocess, ) - return self._vllm_host, self._vllm_port async def _reload_adapter(self, checkpoint_path: str, step: int) -> None: import httpx @@ -637,9 +608,10 @@ async def _sync_dedicated_merged_weights( *, lora_path: str, step: int, + megatron_topology: MegatronTopologyConfig | None = None, ) -> None: self._raise_if_child_failed() - await self._ensure_megatron_running() + await self._ensure_megatron_running(megatron_topology=megatron_topology) await self._init_merged_weight_transfer() self._clear_pending_jobs() job_path, log_path = self._create_megatron_job_paths() @@ -705,13 +677,19 @@ def _validate_megatron_dependencies(self) -> None: "training." ) from exc - async def _ensure_megatron_running(self) -> None: + async def _ensure_megatron_running( + self, + *, + megatron_topology: MegatronTopologyConfig | None = None, + ) -> None: """Lazily start Megatron training process if not running.""" self._raise_if_child_failed() if self._megatron_process is not None: if self._megatron_process.returncode is None: + assert self._active_megatron_topology == megatron_topology return self._megatron_process = None + self._active_megatron_topology = None self._validate_megatron_dependencies() @@ -728,13 +706,13 @@ async def _ensure_megatron_running(self) -> None: num_gpus = torch.cuda.device_count() jobs_dir, _training_log_dir, wake_lock_path = self._megatron_runtime_paths() env["MODEL_IDENTIFIER"] = self.base_model - env["ART_MEGATRON_LORA_CONFIG"] = json.dumps(self._lora_config) if self._allow_unvalidated_arch: env["ART_MEGATRON_ALLOW_UNVALIDATED_ARCH"] = "1" if self._model_uses_expert_replay(): env["ART_MEGATRON_ENABLE_MOE_ROUTING_REPLAY"] = "1" env["ART_MEGATRON_JOBS_DIR"] = jobs_dir env["ART_MEGATRON_WAKE_LOCK_PATH"] = wake_lock_path + env[OFFLOAD_BETWEEN_JOBS_ENV] = "0" if self.is_dedicated else "1" master_addr = env.get("MASTER_ADDR", "127.0.0.1") master_port = str(self._allocate_master_port()) env["MASTER_ADDR"] = master_addr @@ -742,6 +720,10 @@ async def _ensure_megatron_running(self) -> None: random_state = self._megatron_random_state() if random_state is not None: env["ART_MEGATRON_RANDOM_STATE"] = str(random_state) + if megatron_topology is not None: + for env_name in self._megatron_topology_env_names(): + env.pop(env_name, None) + env.update(self._megatron_topology_env(megatron_topology)) command = [ sys.executable, @@ -778,6 +760,7 @@ async def _ensure_megatron_running(self) -> None: self._megatron_process, log_path=megatron_log_path, ) + self._active_megatron_topology = megatron_topology def _clear_pending_jobs(self) -> None: jobs_dir, _training_log_dir, _wake_lock_path = self._megatron_runtime_paths() @@ -802,10 +785,14 @@ def _resolve_training_lora_path(self) -> str: self._ensure_lora_adapter_config(lora_path) return lora_path - async def _prepare_for_training(self) -> str: + async def _prepare_for_training( + self, + *, + megatron_topology: MegatronTopologyConfig | None = None, + ) -> str: self._raise_if_child_failed() self._validate_megatron_dependencies() - await self._ensure_megatron_running() + await self._ensure_megatron_running(megatron_topology=megatron_topology) await self._sleep_runtime() gc_and_empty_cuda_cache() @@ -813,20 +800,33 @@ async def _prepare_for_training(self) -> str: self._clear_pending_jobs() return lora_path - async def _publish_training_checkpoint( + def _publish_staged_training_checkpoint( self, *, - lora_path: str, - ) -> None: - next_step = self._latest_step + 1 - new_checkpoint_dir = get_step_checkpoint_dir(self.output_dir, next_step) - os.makedirs(new_checkpoint_dir, exist_ok=True) - shutil.copy( - f"{lora_path}/adapter_model.safetensors", - f"{new_checkpoint_dir}/adapter_model.safetensors", - ) - self._ensure_lora_adapter_config(new_checkpoint_dir, source_path=lora_path) + staging_lora_path: str, + step: int, + ) -> str: + self._ensure_lora_adapter_config(staging_lora_path) + if not self._adapter_exists_and_loads(staging_lora_path): + raise RuntimeError( + f"Megatron training did not publish LoRA adapter: {staging_lora_path}" + ) + checkpoint_dir = get_step_checkpoint_dir(self.output_dir, step) + if os.path.exists(checkpoint_dir): + raise RuntimeError( + f"Refusing to publish Megatron checkpoint over existing directory: " + f"{checkpoint_dir}" + ) + Path(checkpoint_dir).parent.mkdir(parents=True, exist_ok=True) + Path(staging_lora_path).rename(checkpoint_dir) + return checkpoint_dir + async def _wake_and_reload_training_checkpoint( + self, + *, + checkpoint_dir: str, + step: int, + ) -> None: _jobs_dir, _training_log_dir, wake_lock_path = self._megatron_runtime_paths() try: with open(wake_lock_path, "w") as lock_file: @@ -836,7 +836,7 @@ async def _publish_training_checkpoint( if os.path.exists(wake_lock_path): os.remove(wake_lock_path) - await self._reload_adapter(new_checkpoint_dir, next_step) + await self._reload_adapter(checkpoint_dir, step) async def start_openai_server( self, config: dev.OpenAIServerConfig | None @@ -859,6 +859,9 @@ async def start_openai_server( await self._sync_dedicated_merged_weights( lora_path=lora_path, step=self._latest_step, + megatron_topology=self._resolve_megatron_topology( + self.config.get("megatron_topology") + ), ) except BaseException: await self.aclose() @@ -882,12 +885,19 @@ async def train( "moe_routing_replay_bundle is only supported for in-process/runtime APIs; " "MegatronService subprocess jobs must use moe_routing_replay_path." ) + megatron_topology = self._resolve_megatron_topology( + cast( + Mapping[str, int | None] | MegatronTopologyConfig | None, + _config.get( + "megatron_topology", self.config.get("megatron_topology") + ), + ) + ) if self.is_dedicated: - await self._ensure_megatron_running() + await self._ensure_megatron_running(megatron_topology=megatron_topology) lora_path = self._resolve_active_lora_path() self._clear_pending_jobs() next_step = self._latest_step + 1 - new_checkpoint_dir = get_step_checkpoint_dir(self.output_dir, next_step) staging_lora_path = self._prepare_training_lora_dir( lora_path, next_step, @@ -935,31 +945,32 @@ async def train( async for result in stream_megatron_job( job, job_path=job_path, - merge_output_path=new_checkpoint_dir, process=self._megatron_process, process_log_path=self._megatron_log_path, ): yield {key: float(value) for key, value in result.items()} - self._ensure_lora_adapter_config( - new_checkpoint_dir, source_path=staging_lora_path + new_checkpoint_dir = self._publish_staged_training_checkpoint( + staging_lora_path=staging_lora_path, + step=next_step, ) - if not self._adapter_exists_and_loads(new_checkpoint_dir): - raise RuntimeError( - "Megatron training did not publish LoRA adapter: " - f"{new_checkpoint_dir}" - ) if self.rollout_weights_mode == "merged": self._latest_step = next_step else: await self._reload_adapter(new_checkpoint_dir, next_step) - shutil.rmtree(staging_lora_path, ignore_errors=True) return - lora_path = await self._prepare_for_training() + lora_path = await self._prepare_for_training( + megatron_topology=megatron_topology + ) + next_step = self._latest_step + 1 + staging_lora_path = self._prepare_training_lora_dir( + lora_path, + next_step, + ) job_path, log_path = self._create_megatron_job_paths() job = MegatronTrainingJob( - lora_path=lora_path, + lora_path=staging_lora_path, allow_unvalidated_arch=self._allow_unvalidated_arch, optimizer_state_path=self._get_optimizer_state_path("rl"), disk_packed_tensors=disk_packed_tensors, @@ -981,7 +992,14 @@ async def train( ): yield {key: float(value) for key, value in result.items()} - await self._publish_training_checkpoint(lora_path=lora_path) + new_checkpoint_dir = self._publish_staged_training_checkpoint( + staging_lora_path=staging_lora_path, + step=next_step, + ) + await self._wake_and_reload_training_checkpoint( + checkpoint_dir=new_checkpoint_dir, + step=next_step, + ) except BaseException: await self.aclose() raise @@ -998,14 +1016,21 @@ async def train_sft( raise NotImplementedError( "train_sft is not yet supported in dedicated mode" ) - lora_path = await self._prepare_for_training() + lora_path = await self._prepare_for_training( + megatron_topology=config.megatron_topology + ) + next_step = self._latest_step + 1 + staging_lora_path = self._prepare_training_lora_dir( + lora_path, + next_step, + ) serialized_batches = materialize_sft_batches(batches) job_path, log_path = self._create_megatron_job_paths() grad_accumulation_sequences = ( config.batch_size if isinstance(config.batch_size, int) else None ) job = MegatronSFTTrainingJob( - lora_path=lora_path, + lora_path=staging_lora_path, allow_unvalidated_arch=self._allow_unvalidated_arch, optimizer_state_path=self._get_optimizer_state_path("sft"), sft_data_dir=serialized_batches.sft_data_dir, @@ -1028,7 +1053,14 @@ async def train_sft( "loss/grad_norm": float(result["grad_norm"]), } - await self._publish_training_checkpoint(lora_path=lora_path) + new_checkpoint_dir = self._publish_staged_training_checkpoint( + staging_lora_path=staging_lora_path, + step=next_step, + ) + await self._wake_and_reload_training_checkpoint( + checkpoint_dir=new_checkpoint_dir, + step=next_step, + ) except BaseException: await self.aclose() raise @@ -1037,14 +1069,7 @@ async def aclose(self) -> None: self.close() def _stop_vllm_subprocess(self) -> None: - if self._vllm_process is not None: - terminate_popen_process_group(self._vllm_process) - self._vllm_process = None - if self._vllm_log_file is not None: - self._vllm_log_file.close() - self._vllm_log_file = None - self._vllm_log_path = None - self._vllm_nccl_so_path = None + self._vllm_runtime.close() self._merged_weight_transfer_init_info = None self._loaded_adapter_steps.clear() @@ -1054,9 +1079,11 @@ def _stop_megatron_process(self) -> None: self._megatron_log_file.close() self._megatron_log_file = None self._megatron_log_path = None + self._active_megatron_topology = None return terminate_asyncio_process_group(self._megatron_process) self._megatron_process = None + self._active_megatron_topology = None if self._megatron_log_file is not None: self._megatron_log_file.close() self._megatron_log_file = None diff --git a/src/art/megatron/shared_prefix_state.py b/src/art/megatron/shared_prefix_state.py new file mode 100644 index 000000000..0b1be535d --- /dev/null +++ b/src/art/megatron/shared_prefix_state.py @@ -0,0 +1,195 @@ +"""Shared-prefix packed-sequence state for ART attention and GDN integration.""" + +from __future__ import annotations + +import gc +from typing import Any + +from pydantic import Field +import torch +from torch import Tensor +from torch.nn.attention.flex_attention import create_block_mask + +from art.megatron.context_parallel.layout_index import TokenLayoutIndex +from art.megatron.flex_attn.attention import ( + SharedPrefixAttentionState as FlexSharedPrefixAttentionState, +) +from art.megatron.flex_attn.compiled import flash_sparse_block_size_for_head_dim +from art.megatron.gdn.gdn_shared_prefix import ( + GdnPackedExecutionSpec, + GdnRankExecutionPlan, + build_gdn_rank_execution_plan, + move_gdn_rank_execution_plan_to_device, + parse_gdn_shared_prefix_segments, +) + + +class SharedPrefixAttentionState(FlexSharedPrefixAttentionState): + """Shared-prefix sparsity and optional GDN execution metadata.""" + + group_ids: Tensor + parent_ids: Tensor + gdn_execution_spec: GdnPackedExecutionSpec | None = None + gdn_execution_plan: GdnRankExecutionPlan | None = None + gdn_hidden_layout: str = "attention" + gdn_input_layout: str | None = None + gdn_output_layout: str | None = None + gdn_attention_original_shape: tuple[int, int, int] | None = None + gdn_attention_original_shapes: dict[int, tuple[int, int, int]] = Field( + default_factory=dict + ) + gdn_attention_token_uids: Tensor | None = None + gdn_active_module: Any | None = None + + +_compiled_create_block_mask = torch.compile(create_block_mask, backend="aot_eager") + + +def create_shared_prefix_state( + group_ids: Tensor, + parent_ids: Tensor, + *, + build_gdn_execution_spec: bool = False, + attention_token_layout_index: TokenLayoutIndex | None = None, + attention_head_dim: int | None = None, + attention_value_head_dim: int | None = None, +) -> SharedPrefixAttentionState: + """Build shared-prefix attention mask state plus optional reusable GDN plan.""" + + def _shared_prefix_mask( + batch_idx: Tensor, + head_idx: Tensor, + query_idx: Tensor, + kv_idx: Tensor, + ) -> Tensor: + del head_idx + same_group = group_ids[batch_idx, query_idx] == group_ids[batch_idx, kv_idx] + parent_prefix = parent_ids[batch_idx, query_idx] == group_ids[batch_idx, kv_idx] + return (query_idx >= kv_idx) & (same_group | parent_prefix) + + block_mask = _compiled_create_block_mask( + _shared_prefix_mask, + group_ids.shape[0], + None, + group_ids.shape[1], + group_ids.shape[1], + device=group_ids.device, + BLOCK_SIZE=_shared_prefix_block_size( + group_ids.device, + attention_head_dim=attention_head_dim, + attention_value_head_dim=attention_value_head_dim, + ), + ) + cp_rank, cp_size, cp_group = _gdn_cp_rank_size_group() + gdn_execution_spec = _build_gdn_execution_spec_once( + group_ids, + parent_ids, + build=build_gdn_execution_spec, + cp_rank=cp_rank, + cp_size=cp_size, + cp_group=cp_group, + ) + return SharedPrefixAttentionState( + block_mask=block_mask, + group_ids=group_ids, + parent_ids=parent_ids, + gdn_execution_spec=gdn_execution_spec, + gdn_execution_plan=_build_gdn_execution_plan_once( + gdn_execution_spec, + device=group_ids.device, + cp_rank=cp_rank, + cp_size=cp_size, + cp_group=cp_group, + attention_token_layout_index=attention_token_layout_index, + ), + ) + + +def _shared_prefix_block_size( + device: torch.device, + *, + attention_head_dim: int | None, + attention_value_head_dim: int | None, +) -> tuple[int, int]: + if attention_head_dim is None: + return (128, 128) + return flash_sparse_block_size_for_head_dim( + head_dim=int(attention_head_dim), + head_dim_v=int( + attention_head_dim + if attention_value_head_dim is None + else attention_value_head_dim + ), + device=device, + ) + + +def _build_gdn_execution_spec_once( + group_ids: Tensor, + parent_ids: Tensor, + *, + build: bool, + cp_rank: int, + cp_size: int, + cp_group: Any | None, +) -> GdnPackedExecutionSpec | None: + if not build: + return None + if cp_size == 1: + return parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + if ( + not torch.distributed.is_available() or not torch.distributed.is_initialized() # ty: ignore[possibly-missing-attribute] + ): + return parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + return parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + + +def _build_gdn_execution_plan_once( + spec: GdnPackedExecutionSpec | None, + *, + device: torch.device, + cp_rank: int, + cp_size: int, + cp_group: Any | None, + attention_token_layout_index: TokenLayoutIndex | None, +) -> GdnRankExecutionPlan | None: + if spec is None: + return None + planner_device = torch.device("cpu") if device.type == "cuda" else device + del cp_group + gc_was_enabled = gc.isenabled() + if gc_was_enabled: + gc.disable() + try: + plan = build_gdn_rank_execution_plan( + spec, + device=planner_device, + cp_rank=cp_rank, + cp_size=cp_size, + attention_token_layout_index=attention_token_layout_index, + ) + finally: + if gc_was_enabled: + gc.enable() + return move_gdn_rank_execution_plan_to_device(plan, device) + + +def _gdn_cp_rank_size_group() -> tuple[int, int, Any | None]: + try: + from megatron.core import parallel_state as ps + + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + return ( + int(ps.get_context_parallel_rank()), + int(ps.get_context_parallel_world_size()), + ps.get_context_parallel_group(), + ) + except Exception: + pass + return 0, 1, None diff --git a/src/art/megatron/train.py b/src/art/megatron/train.py index f99eec3c5..134dec74f 100644 --- a/src/art/megatron/train.py +++ b/src/art/megatron/train.py @@ -2,6 +2,9 @@ from art.megatron.runtime.runtime_env import configure_megatron_runtime_env configure_megatron_runtime_env() +from art.megatron.runtime.bridge_runtime import install_art_bridge_runtime_patches + +install_art_bridge_runtime_patches() # isort: on """Megatron training runtime and public worker API. @@ -9,12 +12,9 @@ Public cross-repo API consumed by serverless-training: - build_training_runtime - run_megatron_worker_loop -- merge_lora_adapter """ -from collections.abc import Iterator, Sequence import gc -import importlib import json import math import os @@ -23,33 +23,32 @@ import time from typing import Any, Callable, Literal, cast -from megatron.bridge import AutoBridge -from megatron.bridge.models.gpt_provider import GPTModelProvider from megatron.core import parallel_state as ps from megatron.core.distributed import DistributedDataParallelConfig -from megatron.core.optimizer import ( - MegatronOptimizer, - OptimizerConfig, - get_megatron_optimizer, -) +from megatron.core.optimizer import OptimizerConfig, get_megatron_optimizer from megatron.core.transformer.module import MegatronModule -from megatron.core.transformer.transformer_layer import TransformerLayer -from pydantic import BaseModel, ConfigDict, SkipValidation, field_validator +from pydantic import BaseModel, ConfigDict, field_validator import torch from torch._inductor.runtime.cache_dir_utils import cache_dir as inductor_cache_dir from art import dev, types -from art.loss import loss_fn, shift_tensor -from art.megatron.runtime.bridge_runtime import install_art_bridge_runtime_patches - -install_art_bridge_runtime_patches() - -from art.megatron.compile_workarounds import install_torch_compile_workarounds -from art.megatron.flex_attention import create_shared_prefix_attention_state +from art.loss import Loss, LossInputs, loss_fn, shift_tensor +from art.megatron.context_parallel.types import ( + DispatchedPackedTensors, + ParallelTopology, + PreparedMegatronBatch, +) from art.megatron.lora import apply_lora_adapters -from art.megatron.model_support.spec import ModelSupportHandler, ModelSupportSpec -from art.megatron.provider import finalize_provider_bundle, prepare_provider_bundle -from art.megatron.provider_common import ProviderBundle +from art.megatron.megatron_patches import install_fast_frozen_output_backward +from art.megatron.model_support.lora_disk import ( + load_adapter_config, + load_lora_tensors_for_megatron, +) +from art.megatron.provider import ( + ProviderBundle, + finalize_provider_bundle, + prepare_provider_bundle, +) from art.megatron.routing_replay import ( MoeRoutingReplayBundle, MoeRoutingReplayController, @@ -66,19 +65,52 @@ MergedWeightTransferSpec, load_megatron_job, ) -from art.megatron.training.finalize_grads import finalize_model_grads_extended +from art.megatron.training.compile import ( + configure_training_compile, +) +from art.megatron.training.finalize_grads import ( + finalize_model_grads_extended, + flush_param_grads_to_main_grads, +) +from art.megatron.training.microbatches import ( + CpBatchLookaheadState, + PreparedSFTMicroInputs, + _causal_attention_state, + _clone_packed_tensors, + _clone_sft_tensors, + _count_sft_trainable_tokens, + _count_trainable_tokens, + _empty_new_logprobs_from_logits, + _local_trainable_sft_token_count_tensor, + _local_trainable_token_count_tensor, + _next_micro_lookahead, + _prepare_current_rl_micro, + _prepare_current_sft_micro, + _prepare_dense_sft_micro, + _prepare_next_rl_cp_micro, + _prepare_next_sft_cp_micro, + _select_next_step_first_micro, + _zero_contribution_inputs, + _zero_contribution_sft_inputs, + build_micro_sample_indices, + resolve_global_grad_accumulation_sequences, + select_indexed_inputs, + select_micro_inputs, + select_sft_micro_inputs, +) from art.megatron.training.model_chunks import ( ModelChunks, as_megatron_api_chunks, validate_model_chunks, ) -from art.megatron.training.offload import ( - OffloadState, - offload_to_cpu, - reload_to_gpu, -) from art.megatron.training.sft_batches import load_sft_batch_from_disk -from art.megatron.weights.merge import load_lora_adapter_state_dict, merge_lora_adapter +from art.megatron.training.trace import ( + attach_trace_token_uids, + context_parallel_trace_token_uids_enabled, + prepare_replay_local_input_token_uids, +) +from art.megatron.training.weight_offload import WeightOffloadManager +from art.megatron.weights.lora_publish import save_vllm_lora_from_model from art.megatron.weights.merged_weight_export import ( sync_merged_weights_to_vllm, ) @@ -87,12 +119,6 @@ PackedTensors, packed_tensors_from_dir, ) -from art.weight_transfer import TrainerNcclCommunicator - -safetensors = importlib.import_module("safetensors") -safetensors_torch = importlib.import_module("safetensors.torch") -safe_open = safetensors.safe_open -save_file = safetensors_torch.save_file DEFAULT_MODEL_IDENTIFIER = "Qwen/Qwen3-30B-A3B-Instruct-2507" _optimizer_stats_printed = False @@ -105,7 +131,6 @@ "run_megatron_rl_job", "run_megatron_sft_job", "finalize_megatron_job", - "merge_lora_adapter", ] @@ -113,14 +138,15 @@ class TrainingRuntime(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) provider_bundle: ProviderBundle - provider: SkipValidation[GPTModelProvider] - model: SkipValidation[ModelChunks] - optimizer: SkipValidation[MegatronOptimizer | None] + provider: Any + model: ModelChunks + optimizer: Any | None optimizer_config: OptimizerConfig + transformer_layers_compiled: bool = False rank: int world_size: int moe_routing_replay_controller: MoeRoutingReplayController | None = None - merged_weight_transfer_group: SkipValidation[TrainerNcclCommunicator | None] = None + merged_weight_transfer_group: Any | None = None merged_weight_transfer_init_info: MergedWeightTransferInitInfo | None = None @field_validator("model") @@ -130,15 +156,15 @@ def _validate_model(cls, value: ModelChunks) -> ModelChunks: return value @property - def bridge(self) -> AutoBridge: + def bridge(self) -> Any: return self.provider_bundle.bridge @property - def model_support_handler(self) -> ModelSupportHandler: + def model_support_handler(self) -> Any: return self.provider_bundle.handler @property - def model_support_spec(self) -> ModelSupportSpec: + def model_support_spec(self) -> Any: return self.provider_bundle.spec @@ -166,15 +192,14 @@ def freeze_model(model_chunks: list[MegatronModule]) -> list[MegatronModule]: def _register_trainable_parameter_mode( - provider: GPTModelProvider, + provider: Any, *, trainable_parameter_mode: Literal["lora", "base_model"], - lora_config: dev.LoRAConfig | None, ) -> None: if trainable_parameter_mode == "lora": provider.register_pre_wrap_hook(freeze_model) provider.register_pre_wrap_hook( - lambda chunks: apply_lora_adapters(chunks, provider, lora_config) + lambda chunks: apply_lora_adapters(chunks, provider) ) return if trainable_parameter_mode == "base_model": @@ -185,41 +210,7 @@ def _register_trainable_parameter_mode( ) -def _frozen_linear_grad_input( - grad_output: torch.Tensor, - weight: torch.Tensor, -) -> torch.Tensor: - if grad_output.dim() <= 2 or weight.dim() != 2: - return grad_output.matmul(weight) - grad_output_2d = grad_output.reshape(-1, int(grad_output.shape[-1])) - grad_input_2d = grad_output_2d.matmul(weight) - return grad_input_2d.reshape(*grad_output.shape[:-1], int(weight.shape[-1])) - - -def _install_fast_frozen_output_backward() -> None: - from megatron.core.tensor_parallel.layers import LinearWithFrozenWeight - - if getattr(LinearWithFrozenWeight.backward, "__art_fast_output_backward__", False): - return - - def _fast_backward( - ctx: Any, - grad_output: torch.Tensor, - ) -> tuple[torch.Tensor, None, None, None, None]: - (weight,) = ctx.saved_tensors - grad_input = _frozen_linear_grad_input(grad_output, weight) - if ctx.allreduce_dgrad: - torch.distributed.all_reduce( # ty: ignore[possibly-missing-attribute] - grad_input, - group=ctx.tp_group, - ) - return grad_input, None, None, None, None - - setattr(_fast_backward, "__art_fast_output_backward__", True) - cast(Any, LinearWithFrozenWeight).backward = staticmethod(_fast_backward) - - -def _eager_initialize_optimizer_state(optimizer: MegatronOptimizer) -> None: +def _eager_initialize_optimizer_state(optimizer: Any) -> None: chained_optimizers = getattr(optimizer, "chained_optimizers", None) if chained_optimizers is not None: for child_optimizer in chained_optimizers: @@ -231,14 +222,6 @@ def _eager_initialize_optimizer_state(optimizer: MegatronOptimizer) -> None: init_state_fn(inner_optimizer, getattr(optimizer, "config", None)) -def _compile_enabled() -> bool: - return os.environ.get("ART_DISABLE_MEGATRON_COMPILE", "0") in { - "0", - "false", - "False", - } - - def _default_optimizer_config() -> OptimizerConfig: return OptimizerConfig( bf16=True, @@ -252,7 +235,7 @@ def _default_optimizer_config() -> OptimizerConfig: def _maybe_print_optimizer_stats( - optimizer: MegatronOptimizer, + optimizer: Any, model: ModelChunks, ) -> None: global _optimizer_stats_printed @@ -278,7 +261,7 @@ def _maybe_print_optimizer_stats( def _build_optimizer( model: ModelChunks, optimizer_config: OptimizerConfig, -) -> MegatronOptimizer: +) -> Any: optimizer = get_megatron_optimizer( config=optimizer_config, model_chunks=as_megatron_api_chunks(model), @@ -287,13 +270,6 @@ def _build_optimizer( return optimizer -def clear_moe_routing_replay(runtime: TrainingRuntime) -> None: - """Disable any active MoE routing replay bundle on a long-lived runtime.""" - if runtime.moe_routing_replay_controller is not None: - runtime.moe_routing_replay_controller.remove_router_patches() - runtime.moe_routing_replay_controller = None - - def configure_moe_routing_replay( runtime: TrainingRuntime, *, @@ -301,20 +277,24 @@ def configure_moe_routing_replay( replay_bundle: MoeRoutingReplayBundle | None = None, strict: bool = True, ) -> None: - """Replace the active MoE routing replay bundle, or clear it if no bundle is provided.""" + if runtime.moe_routing_replay_controller is not None: + runtime.moe_routing_replay_controller.remove_router_patches() + runtime.moe_routing_replay_controller = None + if replay_bundle is not None and replay_bundle_path is not None: raise RuntimeError( "Provide either replay_bundle_path or replay_bundle, not both" ) if replay_bundle is None and replay_bundle_path is None: - clear_moe_routing_replay(runtime) return if replay_bundle is None: - assert replay_bundle_path is not None + if replay_bundle_path is None: + raise RuntimeError( + "replay_bundle_path is required when replay_bundle is None" + ) replay_bundle = MoeRoutingReplayBundle.from_dir(replay_bundle_path) - clear_moe_routing_replay(runtime) controller = MoeRoutingReplayController( bundle=replay_bundle, strict=strict, @@ -338,7 +318,7 @@ def _moe_routing_replay_requested( } -def _enable_native_moe_routing_replay(provider: GPTModelProvider) -> None: +def _enable_native_moe_routing_replay(provider: Any) -> None: if bool(getattr(provider, "moe_router_fusion", False)): raise RuntimeError( "MoE routing replay requires provider.moe_router_fusion=False because " @@ -355,7 +335,7 @@ def build_training_runtime( model_identifier: str | None = None, provider_torch_dtype: torch.dtype = torch.bfloat16, provider_bundle_configure: Callable[[ProviderBundle], None] | None = None, - provider_configure: Callable[[GPTModelProvider], None] | None = None, + provider_configure: Callable[[Any], None] | None = None, optimizer_config: OptimizerConfig | None = None, moe_routing_replay_path: str | None = None, moe_routing_replay_bundle: MoeRoutingReplayBundle | None = None, @@ -363,8 +343,7 @@ def build_training_runtime( print_env: bool = True, build_optimizer: bool = True, trainable_parameter_mode: Literal["lora", "base_model"] = "lora", - lora_config: dev.LoRAConfig | None = None, - allow_unvalidated_arch: bool = False, + allow_unvalidated_arch: bool | None = None, ) -> TrainingRuntime: if random_state := os.environ.get("ART_MEGATRON_RANDOM_STATE"): seed = int(random_state) @@ -372,12 +351,17 @@ def build_training_runtime( torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) - _install_fast_frozen_output_backward() + install_fast_frozen_output_backward() provider_bundle = prepare_provider_bundle( model_identifier or os.environ.get("MODEL_IDENTIFIER", DEFAULT_MODEL_IDENTIFIER), torch_dtype=provider_torch_dtype, - allow_unvalidated_arch=allow_unvalidated_arch, + allow_unvalidated_arch=( + os.environ.get("ART_MEGATRON_ALLOW_UNVALIDATED_ARCH", "").strip().lower() + in {"1", "true", "yes", "on"} + if allow_unvalidated_arch is None + else allow_unvalidated_arch + ), ) if provider_bundle_configure is not None: provider_bundle_configure(provider_bundle) @@ -393,7 +377,6 @@ def build_training_runtime( _register_trainable_parameter_mode( provider, trainable_parameter_mode=trainable_parameter_mode, - lora_config=lora_config, ) model = cast( @@ -422,13 +405,11 @@ def build_training_runtime( print("TRITON_CACHE_DIR:", os.environ["TRITON_CACHE_DIR"]) provider_bundle.handler.install_preprocess_patch(model) - compile_workaround_config = provider_bundle.handler.compile_workaround_config( - provider + transformer_layers_compiled = configure_training_compile( + model=model, + provider=provider, + provider_bundle=provider_bundle, ) - if _compile_enabled() and not compile_workaround_config.disable_compile: - install_torch_compile_workarounds(compile_workaround_config) - for chunk in model: - _compile_transformer_layers(chunk) optimizer_config = optimizer_config or _default_optimizer_config() optimizer = _build_optimizer(model, optimizer_config) if build_optimizer else None @@ -439,6 +420,7 @@ def build_training_runtime( model=model, optimizer=optimizer, optimizer_config=optimizer_config, + transformer_layers_compiled=transformer_layers_compiled, rank=rank, world_size=world_size, ) @@ -480,10 +462,12 @@ def run_megatron_worker_loop( print0(runtime.rank, "Loaded job from", job_path) print0(runtime.rank, "Job:", job) + job_completed = False try: _run_megatron_job(runtime, job) + job_completed = True finally: - if after_job is not None: + if job_completed and after_job is not None: after_job() finalize_megatron_job( @@ -502,7 +486,11 @@ def run_megatron_rl_job( adapter_model = None template = None zero_template = None + cp_lookahead_state = None + next_step_first_micro = None + step_result = None + job_completed = False try: configure_moe_routing_replay( runtime, @@ -514,7 +502,6 @@ def run_megatron_rl_job( lora_path=job.lora_path, optimizer_state_path=job.optimizer_state_path, ) - assert runtime.optimizer is not None print0( runtime.rank, @@ -529,6 +516,8 @@ def run_megatron_rl_job( job.config.grad_accumulation_sequences ) num_steps = math.ceil(num_sequences / global_grad_accumulation_sequences) + topology = _infer_parallel_topology(runtime.model) + cp_lookahead_state = CpBatchLookaheadState() if int(topology.cp) > 1 else None for step_index in range(num_steps): micro_indices = build_micro_sample_indices( step_index=step_index, @@ -540,8 +529,21 @@ def run_megatron_rl_job( micro_indices, zero_template, ) + next_step_first_micro = ( + _select_next_step_first_micro( + packed_tensors=packed_tensors, + zero_template=zero_template, + step_index=step_index, + num_steps=num_steps, + num_sequences=num_sequences, + global_grad_accumulation_sequences=global_grad_accumulation_sequences, + ) + if cp_lookahead_state is not None + else None + ) step_result = run_training_step( model_chunks=runtime.model, + provider=runtime.provider, model_support_handler=runtime.model_support_handler, optimizer=runtime.optimizer, learning_rate=job.config.learning_rate, @@ -552,6 +554,8 @@ def run_megatron_rl_job( step_index=step_index, sample_index=micro_indices, moe_routing_replay_controller=runtime.moe_routing_replay_controller, + cp_lookahead_state=cp_lookahead_state, + next_step_first_micro=next_step_first_micro, ) print0( runtime.rank, @@ -578,8 +582,9 @@ def run_megatron_rl_job( lora_path=job.lora_path, optimizer_state_path=job.optimizer_state_path, ) + job_completed = True finally: - clear_moe_routing_replay(runtime) + configure_moe_routing_replay(runtime) if packed_tensors is not None: del packed_tensors if adapter_model is not None: @@ -590,30 +595,16 @@ def run_megatron_rl_job( del zero_template if "micro_inputs" in locals(): del micro_inputs - gc.collect() - torch.cuda.empty_cache() - - -def _flush_param_grads_to_main_grads(model_chunks: ModelChunks) -> None: - """Fallback for direct SFT jobs when DDP post-hooks leave grads in param.grad. - - Megatron's distributed optimizer reads gradients from `main_grad`, which is - normally populated by DDP backward post-hooks. Some direct ART runtimes can - reach finalize/step with gradients still in `param.grad`, so copy them over - using the same guard Megatron uses in its hook implementation. - """ - for chunk in model_chunks: - for param in chunk.parameters(): - if not param.requires_grad or param.grad is None: - continue - if not hasattr(param, "main_grad"): - continue - main_grad = cast(torch.Tensor, getattr(param, "main_grad")) - if not getattr(param, "grad_added_to_main_grad", False) or getattr( - param, "zero_out_wgrad", False - ): - main_grad.add_(param.grad.to(dtype=main_grad.dtype)) - param.grad = None + if next_step_first_micro is not None: + del next_step_first_micro + if step_result is not None: + del step_result + if cp_lookahead_state is not None: + cp_lookahead_state.pending_prepared_micro = None + del cp_lookahead_state + if job_completed: + gc.collect() + torch.cuda.empty_cache() def run_megatron_sft_job( @@ -622,8 +613,9 @@ def run_megatron_sft_job( ) -> None: adapter_model = None + job_completed = False try: - clear_moe_routing_replay(runtime) + configure_moe_routing_replay(runtime) adapter_model = _load_lora_and_optimizer( runtime, lora_path=job.lora_path, @@ -645,50 +637,54 @@ def run_megatron_sft_job( batch_dir = os.path.join(job.sft_data_dir, f"batch_{batch_idx:06d}") batch_metadata, trajectory_tensors = load_sft_batch_from_disk(batch_dir) num_trajectories = int(batch_metadata["num_trajectories"]) - num_dropped_trajectories = int( - batch_metadata.get("num_dropped_trajectories", 0) - ) + if not trajectory_tensors: + raise RuntimeError(f"SFT batch {batch_idx} is empty") if num_trajectories != len(trajectory_tensors): raise RuntimeError( "SFT batch metadata does not match trajectory count: " f"{num_trajectories} != {len(trajectory_tensors)}" ) - global_tokens = sum( - _sft_actual_len(inputs) for inputs in trajectory_tensors - ) - global_trainable_tokens = sum( - _count_sft_trainable_tokens(inputs) for inputs in trajectory_tensors + global_tokens = max( + int(batch_metadata.get("num_tokens", 0)), + 1, ) - if trajectory_tensors: - template = _clone_sft_tensors(trajectory_tensors[0]) - zero_template = _zero_contribution_sft_inputs(template) - micro_indices = build_micro_sample_indices( - step_index=0, - num_sequences=num_trajectories, - global_grad_accumulation_sequences=grad_accumulation_sequences, - ) - micro_inputs = select_sft_micro_inputs( - trajectory_tensors, - micro_indices, - zero_template, + if "num_tokens" not in batch_metadata: + global_tokens = max( + sum( + int(inputs["attention_mask"].sum().item()) + for inputs in trajectory_tensors + ), + 1, ) - step_result = run_megatron_sft_step( - model_chunks=runtime.model, - model_support_handler=runtime.model_support_handler, - optimizer=runtime.optimizer, - learning_rate=job.learning_rates[batch_idx], - inputs=micro_inputs, - step_index=batch_idx, - sample_index=micro_indices, - global_grad_accumulation_sequences=grad_accumulation_sequences, - moe_routing_replay_controller=runtime.moe_routing_replay_controller, - ) - loss = step_result.reduced_loss.item() - grad_norm = float(step_result.grad_norm) - else: - loss = 0.0 - grad_norm = 0.0 + global_trainable_tokens = max( + int(batch_metadata["num_trainable_tokens"]), + 1, + ) + template = _clone_sft_tensors(trajectory_tensors[0]) + zero_template = _zero_contribution_sft_inputs(template) + micro_indices = build_micro_sample_indices( + step_index=0, + num_sequences=num_trajectories, + global_grad_accumulation_sequences=grad_accumulation_sequences, + ) + micro_inputs = select_sft_micro_inputs( + trajectory_tensors, + micro_indices, + zero_template, + ) + step_result = run_megatron_sft_step( + model_chunks=runtime.model, + provider=runtime.provider, + model_support_handler=runtime.model_support_handler, + optimizer=runtime.optimizer, + learning_rate=job.learning_rates[batch_idx], + inputs=micro_inputs, + step_index=batch_idx, + sample_index=micro_indices, + global_grad_accumulation_sequences=grad_accumulation_sequences, + moe_routing_replay_controller=runtime.moe_routing_replay_controller, + ) batch_time = time.perf_counter() - batch_start_time tokens_per_second = global_tokens / batch_time if batch_time > 0 else 0.0 completed_batches = batch_idx + 1 @@ -710,11 +706,10 @@ def run_megatron_sft_job( with open(job.log_path, "a+", encoding="utf-8") as log_file: log_msg = json.dumps( { - "loss": loss, + "loss": step_result.reduced_loss.item(), "learning_rate": job.learning_rates[batch_idx], - "grad_norm": grad_norm, + "grad_norm": float(step_result.grad_norm), "num_trajectories": float(num_trajectories), - "num_dropped_trajectories": float(num_dropped_trajectories), "num_tokens": float(global_tokens), "num_trainable_tokens": float(global_trainable_tokens), "tokens_per_second": tokens_per_second, @@ -729,11 +724,13 @@ def run_megatron_sft_job( lora_path=job.lora_path, optimizer_state_path=job.optimizer_state_path, ) + job_completed = True finally: if adapter_model is not None: del adapter_model - gc.collect() - torch.cuda.empty_cache() + if job_completed: + gc.collect() + torch.cuda.empty_cache() def _load_megatron_job(job_path: str, *, supports_sft: bool) -> MegatronJob: @@ -820,11 +817,11 @@ def _load_adapter_into_model( lora_path: str, rank: int, *, - handler: ModelSupportHandler | None = None, - optimizer: MegatronOptimizer | None = None, + handler: Any | None = None, + optimizer: Any | None = None, ) -> dict[str, torch.Tensor]: print0(rank, "Loading adapter model from", lora_path) - adapter_model = load_lora_adapter_state_dict(lora_path, handler=handler) + adapter_model = load_lora_tensors_for_megatron(lora_path, handler=handler) load_adapter_into_model(model_chunks, adapter_model, optimizer) return adapter_model @@ -837,25 +834,20 @@ def _save_lora_and_optimizer( optimizer_state_path: str, ) -> None: assert runtime.optimizer is not None - sharded_state_dict, sharded_state_manifest = collect_sharded_lora_state( - runtime.model, - adapter_model, - ) - shard_path = os.path.join( - lora_path, - f"adapter_model-{runtime.rank + 1:02d}-of-{runtime.world_size:02d}.safetensors", - ) - manifest_path = os.path.join( - lora_path, - f"adapter_manifest-{runtime.rank + 1:02d}-of-{runtime.world_size:02d}.json", + save_vllm_lora_from_model( + model=runtime.model, + adapter_model=adapter_model, + handler=runtime.model_support_handler, + adapter_config=load_adapter_config(lora_path), + output_dir=lora_path, + rank=runtime.rank, + world_size=runtime.world_size, ) - print("Saving adapter shard to", shard_path) - os.makedirs(lora_path, exist_ok=True) - save_file(sharded_state_dict, shard_path) - print("Saving adapter shard manifest to", manifest_path) - with open(manifest_path, "w", encoding="utf-8") as manifest_file: - json.dump(sharded_state_manifest, manifest_file, sort_keys=True) + _save_optimizer(runtime, optimizer_state_path=optimizer_state_path) + +def _save_optimizer(runtime: TrainingRuntime, *, optimizer_state_path: str) -> None: + assert runtime.optimizer is not None optimizer_shard_path = os.path.join( optimizer_state_path, f"{runtime.rank + 1:02d}-of-{runtime.world_size:02d}.pt", @@ -888,233 +880,24 @@ def _placeholder_attention_mask(device: torch.device) -> torch.Tensor: return torch.zeros((1, 1, 1, 1), dtype=torch.bool, device=device) -def _causal_attention_state(seq_len: int, device: torch.device) -> Any: - group_ids = torch.zeros((1, seq_len), dtype=torch.int64, device=device) - parent_ids = torch.zeros_like(group_ids) - return create_shared_prefix_attention_state( - group_ids=group_ids, - parent_ids=parent_ids, - ) - - -def _set_child_module( - parent: torch.nn.Module, - name: str, - child: torch.nn.Module, -) -> None: - if isinstance(parent, torch.nn.ModuleList | torch.nn.Sequential): - parent[int(name)] = child - return - setattr(parent, name, child) - - -def _compile_transformer_layers(module: torch.nn.Module) -> None: - for name, child in list(module.named_children()): - if isinstance(child, TransformerLayer): - compiled_child = cast(torch.nn.Module, torch.compile(child)) - _set_child_module(parent=module, name=name, child=compiled_child) - continue - _compile_transformer_layers(child) - - -def iter_modules(model_chunks: Sequence[torch.nn.Module]) -> Iterator[torch.nn.Module]: - for chunk in model_chunks: - for module in chunk.modules(): - yield module - - def load_adapter_into_model( - model_chunks: Sequence[torch.nn.Module], + model_chunks: ModelChunks, adapter_model: dict[str, torch.Tensor], optimizer: Any | None = None, ) -> None: with torch.no_grad(): - for module in iter_modules(model_chunks): - if hasattr(module, "load_lora"): - module.load_lora(adapter_model) # type: ignore[attr-defined] + for chunk in model_chunks: + for module in chunk.modules(): + if hasattr(module, "load_lora"): + module.load_lora(adapter_model) # type: ignore[attr-defined] if optimizer is None: return optimizer.reload_model_params() -def collect_sharded_lora_state( - model_chunks: ModelChunks, - adapter_model: dict[str, torch.Tensor], -) -> tuple[dict[str, torch.Tensor], dict[str, dict[str, Any]]]: - sharded_state_dict: dict[str, torch.Tensor] = {} - sharded_state_manifest: dict[str, dict[str, Any]] = {} - for module in iter_modules(model_chunks): - if hasattr(module, "sharded_lora_state_dict"): - module_sharded_lora_state_dict: dict[str, torch.Tensor] = ( - module.sharded_lora_state_dict() # type: ignore[attr-defined] - ) - for key, value in module_sharded_lora_state_dict.items(): - target_dtype = ( - adapter_model[key].dtype if key in adapter_model else value.dtype - ) - sharded_state_dict[key] = value.to(target_dtype).contiguous() - if hasattr(module, "sharded_lora_manifest"): - module_sharded_lora_manifest: dict[str, dict[str, Any]] = ( - module.sharded_lora_manifest() # type: ignore[attr-defined] - ) - sharded_state_manifest.update(module_sharded_lora_manifest) - return sharded_state_dict, sharded_state_manifest - - -@torch.no_grad() -def select_indexed_inputs(packed_tensors: PackedTensors, index: int) -> PackedTensors: - return cast( - PackedTensors, - { - **{ - key: value[index : index + 1] - for key, value in packed_tensors.items() - if isinstance(value, torch.Tensor) - }, - "pixel_values": [None], - "image_grid_thw": [None], - "moe_routing_replay": None, - }, - ) - - -@torch.no_grad() -def _clone_packed_tensors(inputs: PackedTensors) -> PackedTensors: - return cast( - PackedTensors, - { - **{ - key: value.clone() - for key, value in inputs.items() - if isinstance(value, torch.Tensor) - }, - "pixel_values": [None], - "image_grid_thw": [None], - "moe_routing_replay": None, - }, - ) - - -@torch.no_grad() -def _zero_contribution_inputs(template: PackedTensors) -> PackedTensors: - dummy = _clone_packed_tensors(template) - dummy["assistant_mask"].zero_() - return dummy - - -@torch.no_grad() -def _clone_sft_tensors( - inputs: dict[str, torch.Tensor], -) -> dict[str, torch.Tensor]: - return {key: value.clone() for key, value in inputs.items()} - - -@torch.no_grad() -def _zero_contribution_sft_inputs( - template: dict[str, torch.Tensor], -) -> dict[str, torch.Tensor]: - dummy = _clone_sft_tensors(template) - dummy["labels"].fill_(-100) - return dummy - - -def resolve_global_grad_accumulation_sequences( - global_grad_accumulation_sequences: int | None, -) -> int: - dp_world_size = ps.get_data_parallel_world_size() - if global_grad_accumulation_sequences is None: - return dp_world_size - return global_grad_accumulation_sequences - - -def resolve_local_grad_accumulation_sequences( - global_grad_accumulation_sequences: int | None, -) -> int: - resolved_global_grad_accumulation_sequences = ( - resolve_global_grad_accumulation_sequences( - global_grad_accumulation_sequences=global_grad_accumulation_sequences - ) - ) - dp_world_size = ps.get_data_parallel_world_size() - if ( - resolved_global_grad_accumulation_sequences <= 0 - or resolved_global_grad_accumulation_sequences % dp_world_size != 0 - ): - raise RuntimeError( - "Invalid global grad accumulation / DP world size combination: " - f"global_grad_accumulation_sequences={resolved_global_grad_accumulation_sequences}, " - f"dp_world_size={dp_world_size}" - ) - return resolved_global_grad_accumulation_sequences // dp_world_size - - -def build_micro_sample_indices( - step_index: int, - num_sequences: int, - global_grad_accumulation_sequences: int | None, -) -> list[int | None]: - dp_rank = ps.get_data_parallel_rank() - resolved_global_grad_accumulation_sequences = ( - resolve_global_grad_accumulation_sequences( - global_grad_accumulation_sequences=global_grad_accumulation_sequences - ) - ) - dp_world_size = ps.get_data_parallel_world_size() - local_grad_accumulation_sequences = resolve_local_grad_accumulation_sequences( - global_grad_accumulation_sequences=resolved_global_grad_accumulation_sequences, - ) - base_global_sample_index = step_index * resolved_global_grad_accumulation_sequences - global_step_indices: list[int | None] = [] - for offset in range(resolved_global_grad_accumulation_sequences): - global_sample_index = base_global_sample_index + offset - global_step_indices.append( - global_sample_index if global_sample_index < num_sequences else None - ) - return [ - global_step_indices[offset * dp_world_size + dp_rank] - for offset in range(local_grad_accumulation_sequences) - ] - - -def select_micro_inputs( - packed_tensors: PackedTensors, - sample_indices: list[int | None], - zero_template: PackedTensors, -) -> list[PackedTensors]: - return [ - ( - _clone_packed_tensors(zero_template) - if sample_index is None - else select_indexed_inputs(packed_tensors, sample_index) - ) - for sample_index in sample_indices - ] - - -def select_sft_micro_inputs( - trajectory_tensors: list[dict[str, torch.Tensor]], - sample_indices: list[int | None], - zero_template: dict[str, torch.Tensor], -) -> list[dict[str, torch.Tensor]]: - return [ - ( - _clone_sft_tensors(zero_template) - if sample_index is None - else _clone_sft_tensors(trajectory_tensors[sample_index]) - ) - for sample_index in sample_indices - ] - - -def _move_inputs_to_device(inputs: PackedTensors, device: torch.device) -> None: - for key, value in inputs.items(): - if isinstance(value, torch.Tensor): - inputs[key] = value.to(device) # type: ignore[index] - - def _optimizer_step( - optimizer: MegatronOptimizer, + optimizer: Any, learning_rate: float, ) -> tuple[bool, float, int | None]: for param_group in optimizer.param_groups: @@ -1140,59 +923,30 @@ def _reduce_loss( return reduced_loss -def _count_trainable_tokens(inputs: PackedTensors) -> float: - assistant_mask = shift_tensor(inputs["assistant_mask"], False) - return float(assistant_mask.sum().item()) - - -def _local_trainable_token_count_tensor( - micro_inputs: list[PackedTensors], - device: torch.device, -) -> torch.Tensor: - local_token_total = sum(_count_trainable_tokens(micro) for micro in micro_inputs) - return torch.tensor([local_token_total], device=device, dtype=torch.float32) - - -def _sft_actual_len(inputs: dict[str, torch.Tensor]) -> int: - attention_mask = inputs["attention_mask"].reshape(-1) - return max(int(attention_mask.sum().item()), 1) - - -def _count_sft_trainable_tokens(inputs: dict[str, torch.Tensor]) -> float: - actual_len = _sft_actual_len(inputs) - labels = inputs["labels"].reshape(-1)[:actual_len].unsqueeze(0) - shifted_labels = shift_tensor(labels, -100) - return float((shifted_labels != -100).sum().item()) +def _unwrap_model_config(model_chunks: ModelChunks) -> Any | None: + module: Any = model_chunks[0] + while hasattr(module, "module"): + module = module.module + return getattr(module, "config", None) -def _local_trainable_sft_token_count_tensor( - micro_inputs: list[dict[str, torch.Tensor]], - device: torch.device, -) -> torch.Tensor: - local_token_total = sum( - _count_sft_trainable_tokens(micro) for micro in micro_inputs +def _infer_parallel_topology(model_chunks: ModelChunks) -> ParallelTopology: + model_config = _unwrap_model_config(model_chunks) + return ParallelTopology( + tp=ps.get_tensor_model_parallel_world_size(), + cp=ps.get_context_parallel_world_size(), + dp=ps.get_data_parallel_world_size(), + pp=ps.get_pipeline_model_parallel_world_size(), + sp=bool(getattr(model_config, "sequence_parallel", False)), ) - return torch.tensor([local_token_total], device=device, dtype=torch.float32) - - -def _prepare_sft_micro_inputs( - inputs: dict[str, torch.Tensor], - device: torch.device, -) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int]: - actual_len = _sft_actual_len(inputs) - input_ids = inputs["input_ids"].reshape(-1)[:actual_len].unsqueeze(0).to(device) - labels = inputs["labels"].reshape(-1)[:actual_len].unsqueeze(0).to(device) - position_ids = torch.arange(actual_len, device=device).unsqueeze(0) - shifted_labels = shift_tensor(labels, -100) - mask = shifted_labels != -100 - return input_ids, position_ids, shifted_labels, mask, actual_len def run_megatron_sft_step( *, model_chunks: ModelChunks, - model_support_handler: ModelSupportHandler, - optimizer: MegatronOptimizer, + provider: Any, + model_support_handler: Any, + optimizer: Any, learning_rate: float, inputs: dict[str, torch.Tensor] | list[dict[str, torch.Tensor]], step_index: int, @@ -1210,7 +964,7 @@ def run_megatron_sft_step( "sample_index list length must match number of micro inputs: " f"{len(sample_index)} != {len(micro_inputs)}" ) - micro_sample_indices: list[int | None] = sample_index + micro_sample_indices = sample_index else: assert len(micro_inputs) == 1 micro_sample_indices = [sample_index] @@ -1227,13 +981,19 @@ def run_megatron_sft_step( global_grad_accumulation_sequences=resolved_global_grad_accumulation_sequences, ) + topology = _infer_parallel_topology(model_chunks) device = next(model_chunks[0].parameters()).device + trace_token_uids = context_parallel_trace_token_uids_enabled( + topology, + moe_routing_replay_controller, + ) for chunk in model_chunks: - chunk.zero_grad_buffer() # type: ignore[call-non-callable] + chunk.zero_grad_buffer() # ty: ignore[call-non-callable] raw_loss_sum: torch.Tensor | None = None - num_tokens = _local_trainable_sft_token_count_tensor(micro_inputs, device=device) + loss_inputs_for_count: list[dict[str, torch.Tensor] | PreparedSFTMicroInputs] = [] + pending_prepared_micro: PreparedMegatronBatch | None = None for micro_order, micro in enumerate(micro_inputs): if moe_routing_replay_controller is not None: @@ -1241,31 +1001,58 @@ def run_megatron_sft_step( micro_sample_indices[micro_order], micro_order, ) - input_ids, position_ids, shifted_labels, mask, seq_len = ( - _prepare_sft_micro_inputs(micro, device) + prepared_micro, pending_prepared_micro = _prepare_current_sft_micro( + micro, + device=device, + topology=topology, + provider=provider, + model_support_handler=model_support_handler, + trace_token_uids=trace_token_uids, + pending_prepared_micro=pending_prepared_micro, ) - per_token_loss: torch.Tensor = model_chunks[0]( - input_ids=input_ids, - position_ids=position_ids, - attention_mask=_placeholder_attention_mask(device), - labels=shifted_labels, - **model_support_handler.get_forward_kwargs( - model_chunks[0], - attention_bias=_causal_attention_state(seq_len, device), - ), + prepare_replay_local_input_token_uids( + moe_routing_replay_controller, + prepared_micro.local_token_uids, + prepared_micro.attention_state, + ) + with attach_trace_token_uids(model_chunks, prepared_micro.local_token_uids): + per_token_loss: torch.Tensor = model_chunks[0]( + input_ids=prepared_micro.input_ids, + position_ids=prepared_micro.position_ids, + attention_mask=_placeholder_attention_mask(device), + labels=prepared_micro.labels, + packed_seq_params=prepared_micro.packed_seq_params, + **model_support_handler.get_forward_kwargs( + model_chunks[0], + attention_bias=prepared_micro.attention_state, + ), + ) + masked_loss = ( + per_token_loss[prepared_micro.loss_mask].sum() + per_token_loss.sum() * 0.0 ) - masked_loss = per_token_loss[mask].sum() masked_loss.backward() + pending_prepared_micro = _prepare_next_sft_cp_micro( + _next_micro_lookahead(micro_inputs, micro_order), + device=device, + topology=topology, + model_support_handler=model_support_handler, + trace_token_uids=trace_token_uids, + ) detached_micro_loss = masked_loss.detach() if raw_loss_sum is None: raw_loss_sum = detached_micro_loss else: raw_loss_sum = raw_loss_sum + detached_micro_loss + loss_inputs_for_count.append(prepared_micro) if raw_loss_sum is None: raise RuntimeError("run_megatron_sft_step did not produce outputs") - _flush_param_grads_to_main_grads(model_chunks) + num_tokens = _local_trainable_sft_token_count_tensor( + loss_inputs_for_count, + device=device, + ) + flush_param_grads_to_main_grads(model_chunks) finalize_model_grads_extended( as_megatron_api_chunks(model_chunks), num_tokens=num_tokens ) @@ -1296,8 +1083,9 @@ def run_megatron_sft_step( def run_training_step( *, model_chunks: ModelChunks, - model_support_handler: ModelSupportHandler, - optimizer: MegatronOptimizer, + provider: Any, + model_support_handler: Any, + optimizer: Any, learning_rate: float, inputs: PackedTensors | list[PackedTensors], config: types.TrainConfig, @@ -1306,6 +1094,8 @@ def run_training_step( sample_index: int | list[int | None], ref_logprobs: torch.Tensor | None = None, moe_routing_replay_controller: MoeRoutingReplayController | None = None, + cp_lookahead_state: CpBatchLookaheadState | None = None, + next_step_first_micro: PackedTensors | None = None, ) -> TrainStepResult: micro_inputs = inputs if isinstance(inputs, list) else [inputs] if not micro_inputs: @@ -1317,7 +1107,7 @@ def run_training_step( "sample_index list length must match number of micro inputs: " f"{len(sample_index)} != {len(micro_inputs)}" ) - micro_sample_indices: list[int | None] = sample_index + micro_sample_indices = sample_index else: assert len(micro_inputs) == 1 micro_sample_indices = [sample_index] @@ -1335,67 +1125,125 @@ def run_training_step( ) device = next(model_chunks[0].parameters()).device + topology = _infer_parallel_topology(model_chunks) + trace_token_uids = context_parallel_trace_token_uids_enabled( + topology, + moe_routing_replay_controller, + ) + pending_prepared_micro = ( + cp_lookahead_state.pending_prepared_micro + if cp_lookahead_state is not None and int(topology.cp) > 1 + else None + ) + if cp_lookahead_state is not None and int(topology.cp) <= 1: + cp_lookahead_state.pending_prepared_micro = None for chunk in model_chunks: - chunk.zero_grad_buffer() # type: ignore[call-non-callable] + chunk.zero_grad_buffer() # ty: ignore[call-non-callable] micro_count = len(micro_inputs) raw_loss_sum: torch.Tensor | None = None - token_count = _local_trainable_token_count_tensor(micro_inputs, device=device) - probs_corr_sum = 0.0 - new_logprobs_list: list[torch.Tensor] = [] + loss_inputs_for_count: list[LossInputs | DispatchedPackedTensors] = [] + probs_corr_total: torch.Tensor | None = None + new_logprobs_gpu: list[torch.Tensor] = [] - for micro_order, micro in enumerate(micro_inputs): + def begin_micro(micro_order: int) -> None: if moe_routing_replay_controller is not None: moe_routing_replay_controller.begin_micro( micro_sample_indices[micro_order], micro_order, ) - _move_inputs_to_device(micro, device) - attention_state = create_shared_prefix_attention_state( - group_ids=micro["group_ids"], - parent_ids=micro["parent_ids"], + + for micro_order in range(micro_count): + begin_micro(micro_order) + prepared_micro, pending_prepared_micro = _prepare_current_rl_micro( + micro_inputs[micro_order], + device=device, + topology=topology, + provider=provider, + model_support_handler=model_support_handler, + ref_logprobs=ref_logprobs, + trace_token_uids=trace_token_uids, + pending_prepared_micro=pending_prepared_micro, ) - attention_mask = torch.zeros((1, 1, 1, 1), dtype=torch.bool, device=device) - shifted_labels = shift_tensor(micro["tokens"], -100) - shifted_assistant_mask = shift_tensor(micro["assistant_mask"], False) - shifted_labels = torch.where( - shifted_assistant_mask, - shifted_labels, - torch.full_like(shifted_labels, -100), + prepare_replay_local_input_token_uids( + moe_routing_replay_controller, + prepared_micro.local_token_uids, + prepared_micro.attention_state, ) - new_logprobs = -model_chunks[0]( - input_ids=micro["tokens"], - position_ids=micro["input_pos"], - attention_mask=attention_mask, - labels=shifted_labels, + model_forward_kwargs = dict( + input_ids=prepared_micro.model_tokens, + position_ids=prepared_micro.model_input_pos, + attention_mask=_placeholder_attention_mask(device), + packed_seq_params=prepared_micro.packed_seq_params, **model_support_handler.get_forward_kwargs( model_chunks[0], - attention_bias=attention_state, + attention_bias=prepared_micro.attention_state, ), ) + with attach_trace_token_uids(model_chunks, prepared_micro.local_token_uids): + if int(prepared_micro.model_tokens.numel()) == 0: + logits = model_chunks[0](**model_forward_kwargs, labels=None) + new_logprobs = _empty_new_logprobs_from_logits( + logits, prepared_micro.model_labels + ) + else: + new_logprobs = -model_chunks[0]( + **model_forward_kwargs, + labels=prepared_micro.model_labels, + ) loss_info = loss_fn( - micro, # type: ignore[invalid-argument-type] - new_logprobs, - ref_logprobs, - None, - experimental_config, + prepared_micro.loss_inputs, + new_logprobs=new_logprobs, + ref_logprobs=prepared_micro.ref_logprobs, + entropies=None, + experimental_config=experimental_config, reduction="sum", ) - micro_loss = loss_info.policy_loss + micro_loss = loss_info.policy_loss + new_logprobs.sum() * 0.0 if not micro_loss.requires_grad: + assistant_tokens = _count_trainable_tokens(prepared_micro.loss_inputs) + nonzero_weights = int( + torch.count_nonzero( + prepared_micro.loss_inputs.align_inputs().weights + ).item() + ) + nonzero_advantages = int( + torch.count_nonzero( + prepared_micro.loss_inputs.align_inputs().advantages + ).item() + ) raise RuntimeError( "RL micro_loss is detached before backward: " f"new_logprobs.requires_grad={new_logprobs.requires_grad}, " f"policy_loss_sum_requires_grad={loss_info.policy_loss_sum.requires_grad}, " - f"assistant_tokens={int(shift_tensor(micro['assistant_mask'], False).sum().item())}, " - f"nonzero_weights={int(torch.count_nonzero(shift_tensor(micro['weights'], 0.0)).item())}, " - f"nonzero_advantages={int(torch.count_nonzero(shift_tensor(micro['advantages'], 0.0)).item())}" + f"assistant_tokens={assistant_tokens}, " + f"nonzero_weights={nonzero_weights}, " + f"nonzero_advantages={nonzero_advantages}" ) micro_loss.backward() - probs_corr_sum += float(loss_info.probs_corr.item()) + loss_inputs_for_count.append(prepared_micro.loss_inputs) + del model_forward_kwargs + del prepared_micro + pending_prepared_micro = _prepare_next_rl_cp_micro( + _next_micro_lookahead( + micro_inputs, + micro_order, + next_step_first_micro, + ), + device=device, + topology=topology, + model_support_handler=model_support_handler, + trace_token_uids=trace_token_uids, + ref_logprobs=ref_logprobs, + ) + detached_probs_corr = loss_info.probs_corr.detach() + if probs_corr_total is None: + probs_corr_total = detached_probs_corr + else: + probs_corr_total = probs_corr_total + detached_probs_corr detached_micro_loss = micro_loss.detach() if raw_loss_sum is None: raw_loss_sum = detached_micro_loss @@ -1403,17 +1251,21 @@ def run_training_step( raw_loss_sum = raw_loss_sum + detached_micro_loss del loss_info del micro_loss - del attention_mask - del attention_state - new_logprobs_list.append( - new_logprobs.detach().to(device="cpu", non_blocking=True) - ) + new_logprobs_gpu.append(new_logprobs.detach()) del new_logprobs if raw_loss_sum is None: raise RuntimeError("run_training_step did not produce outputs") + if probs_corr_total is None: + raise RuntimeError("run_training_step did not accumulate probs_corr") + if cp_lookahead_state is not None: + cp_lookahead_state.pending_prepared_micro = pending_prepared_micro torch.cuda.empty_cache() + token_count = _local_trainable_token_count_tensor( + loss_inputs_for_count, + device=device, + ) finalize_model_grads_extended( as_megatron_api_chunks(model_chunks), num_tokens=token_count, @@ -1434,8 +1286,10 @@ def run_training_step( return TrainStepResult( reduced_loss=reduced_loss, - probs_corr=probs_corr_sum / micro_count, - new_logprobs=new_logprobs_list, + probs_corr=float((probs_corr_total / micro_count).item()), + new_logprobs=[ + tensor.to(device="cpu", non_blocking=True) for tensor in new_logprobs_gpu + ], update_successful=update_successful, grad_norm=grad_norm, num_zeros_in_grad=num_zeros_in_grad, @@ -1464,8 +1318,24 @@ def _sync_merged_weights_to_vllm( ) +def _close_merged_weight_transfer_group(runtime: TrainingRuntime) -> None: + weight_transfer_group = runtime.merged_weight_transfer_group + runtime.merged_weight_transfer_group = None + runtime.merged_weight_transfer_init_info = None + if weight_transfer_group is None: + return + close = getattr(weight_transfer_group, "close", None) + if close is not None: + close() + + def _run_service_loop(runtime: TrainingRuntime) -> None: - offload_state = OffloadState() + weight_offload = WeightOffloadManager.from_env( + model=runtime.model, + rank=runtime.rank, + compile_enabled=runtime.transformer_layers_compiled, + ) + weight_offload.install() wake_lock_path = os.environ.get( "ART_MEGATRON_WAKE_LOCK_PATH", DEFAULT_VLLM_WAKE_LOCK_PATH ) @@ -1475,35 +1345,29 @@ def wait_until_ready() -> None: time.sleep(0.2) def before_job() -> None: - reload_to_gpu(runtime.model, runtime.rank, offload_state) + weight_offload.before_job() def after_job() -> None: runtime.optimizer = None - gc.collect() - torch.cuda.empty_cache() - offload_to_cpu(runtime.model, runtime.rank, offload_state) + weight_offload.after_job() - after_job() - run_megatron_worker_loop( - runtime, - supports_sft=True, - wait_until_ready=wait_until_ready, - before_job=before_job, - after_job=after_job, - ) + try: + after_job() + run_megatron_worker_loop( + runtime, + supports_sft=True, + wait_until_ready=wait_until_ready, + before_job=before_job, + after_job=after_job, + ) + finally: + _close_merged_weight_transfer_group(runtime) def main() -> None: runtime = build_training_runtime( model_identifier=os.environ.get("MODEL_IDENTIFIER", DEFAULT_MODEL_IDENTIFIER), build_optimizer=False, - lora_config=cast( - dev.LoRAConfig, json.loads(os.environ.get("ART_MEGATRON_LORA_CONFIG", "{}")) - ), - allow_unvalidated_arch=os.environ.get( - "ART_MEGATRON_ALLOW_UNVALIDATED_ARCH", "" - ).lower() - in {"1", "true", "yes", "on"}, ) _run_service_loop(runtime) diff --git a/src/art/megatron/training/compile.py b/src/art/megatron/training/compile.py new file mode 100644 index 000000000..531d6b30b --- /dev/null +++ b/src/art/megatron/training/compile.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import os +from typing import Any, cast + +from megatron.core.transformer.transformer_layer import TransformerLayer +import torch + +from art.megatron.compile_workarounds import install_torch_compile_workarounds +from art.megatron.provider import ProviderBundle +from art.megatron.training.model_chunks import ModelChunks + + +def compile_enabled() -> bool: + return os.environ.get("ART_DISABLE_MEGATRON_COMPILE", "0") in { + "0", + "false", + "False", + } + + +def _set_child_module( + parent: torch.nn.Module, + name: str, + child: torch.nn.Module, +) -> None: + if isinstance(parent, torch.nn.ModuleList | torch.nn.Sequential): + parent[int(name)] = child + return + setattr(parent, name, child) + + +def _compile_transformer_layers(module: torch.nn.Module) -> None: + for name, child in list(module.named_children()): + if isinstance(child, TransformerLayer): + physical_forward = getattr(child, "_art_gdn_island_physical_forward", None) + if callable(physical_forward): + setattr( + child, + "_art_gdn_island_physical_forward", + torch.compile(physical_forward), + ) + continue + compiled_child = cast(torch.nn.Module, torch.compile(child)) + _set_child_module(parent=module, name=name, child=compiled_child) + continue + _compile_transformer_layers(child) + + +def configure_training_compile( + *, + model: ModelChunks, + provider: Any, + provider_bundle: ProviderBundle, +) -> bool: + compile_workaround_config = provider_bundle.handler.compile_workaround_config( + provider + ) + enabled = compile_enabled() + flags = ( + compile_workaround_config.flags + if enabled and not compile_workaround_config.disable_compile + else compile_workaround_config.unconditional_flags + ) + if flags: + install_torch_compile_workarounds( + compile_workaround_config.model_copy(update={"flags": flags}) + ) + transformer_layers_compiled = ( + enabled and not compile_workaround_config.disable_compile + ) + if transformer_layers_compiled: + for chunk in model: + _compile_transformer_layers(chunk) + return transformer_layers_compiled diff --git a/src/art/megatron/training/finalize_grads.py b/src/art/megatron/training/finalize_grads.py index 2a770fea0..cde0e7b06 100644 --- a/src/art/megatron/training/finalize_grads.py +++ b/src/art/megatron/training/finalize_grads.py @@ -60,6 +60,28 @@ def _resolve_reduce_op(op: GradSyncOp) -> Any: raise RuntimeError(f"Unknown grad sync op: {op}") +def flush_param_grads_to_main_grads(model_chunks: Iterable[torch.nn.Module]) -> None: + """Fallback for direct jobs when DDP post-hooks leave grads in param.grad. + + Megatron's distributed optimizer reads gradients from `main_grad`, which is + normally populated by DDP backward post-hooks. Some direct ART runtimes can + reach finalize/step with gradients still in `param.grad`, so copy them over + using the same guard Megatron uses in its hook implementation. + """ + for chunk in model_chunks: + for param in chunk.parameters(): + if not param.requires_grad or param.grad is None: + continue + if not hasattr(param, "main_grad"): + continue + main_grad = cast(torch.Tensor, param.main_grad) + if not getattr(param, "grad_added_to_main_grad", False) or getattr( + param, "zero_out_wgrad", False + ): + main_grad.add_(param.grad.to(dtype=main_grad.dtype)) + param.grad = None + + def finalize_model_grads_extended( model: list[MegatronModule], num_tokens: torch.Tensor | None = None, diff --git a/src/art/megatron/training/microbatches.py b/src/art/megatron/training/microbatches.py new file mode 100644 index 000000000..9beee69a6 --- /dev/null +++ b/src/art/megatron/training/microbatches.py @@ -0,0 +1,621 @@ +from __future__ import annotations + +from collections.abc import Sequence +from typing import Any + +from megatron.core import parallel_state as ps +from pydantic import BaseModel, ConfigDict +import torch + +from art.loss import LossInputs, shift_tensor +from art.megatron.context_parallel.runtime import prepare_cp_micro +from art.megatron.context_parallel.types import ( + ContextParallelConfig, + DispatchedPackedTensors, + ParallelTopology, + PreparedMegatronBatch, +) +from art.megatron.shared_prefix_state import create_shared_prefix_state +from art.megatron.training.trace import ( + packed_sequence_token_uids, + sft_sequence_token_uids, +) +from art.preprocessing.pack import PackedTensors + + +class CpBatchLookaheadState(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + pending_prepared_micro: PreparedMegatronBatch | None = None + + +class PreparedRLMicroInputs(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + model_tokens: torch.Tensor + model_input_pos: torch.Tensor + model_labels: torch.Tensor + attention_state: Any + packed_seq_params: Any | None = None + loss_inputs: LossInputs | DispatchedPackedTensors + ref_logprobs: torch.Tensor | None = None + local_token_uids: torch.Tensor | None = None + + +class PreparedSFTMicroInputs(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + input_ids: torch.Tensor + position_ids: torch.Tensor + labels: torch.Tensor + loss_mask: torch.Tensor + attention_state: Any + packed_seq_params: Any | None = None + local_token_uids: torch.Tensor | None = None + + +@torch.no_grad() +def select_indexed_inputs(packed_tensors: PackedTensors, index: int) -> PackedTensors: + def selected_tensor(value: torch.Tensor) -> torch.Tensor: + selected = value[index : index + 1] + # File-backed slices keep the mmap alive and can make job cleanup fail. + if getattr(selected.untyped_storage(), "filename", None): + return selected.clone() + return selected + + return PackedTensors( # type: ignore[call-arg] + **{ + key: selected_tensor(value) + for key, value in packed_tensors.items() + if isinstance(value, torch.Tensor) + }, + pixel_values=[None], + image_grid_thw=[None], + ) + + +@torch.no_grad() +def _clone_packed_tensors(inputs: PackedTensors) -> PackedTensors: + return PackedTensors( # type: ignore[call-arg] + **{ + key: value.clone() + for key, value in inputs.items() + if isinstance(value, torch.Tensor) + }, + pixel_values=[None], + image_grid_thw=[None], + ) + + +@torch.no_grad() +def _zero_contribution_inputs(template: PackedTensors) -> PackedTensors: + dummy = _clone_packed_tensors(template) + dummy["assistant_mask"].zero_() + return dummy + + +@torch.no_grad() +def _clone_sft_tensors( + inputs: dict[str, torch.Tensor], +) -> dict[str, torch.Tensor]: + return {key: value.clone() for key, value in inputs.items()} + + +@torch.no_grad() +def _zero_contribution_sft_inputs( + template: dict[str, torch.Tensor], +) -> dict[str, torch.Tensor]: + dummy = _clone_sft_tensors(template) + dummy["labels"].fill_(-100) + return dummy + + +def resolve_global_grad_accumulation_sequences( + global_grad_accumulation_sequences: int | None, +) -> int: + dp_world_size = ps.get_data_parallel_world_size() + if global_grad_accumulation_sequences is None: + return dp_world_size + return global_grad_accumulation_sequences + + +def resolve_local_grad_accumulation_sequences( + global_grad_accumulation_sequences: int | None, +) -> int: + resolved_global_grad_accumulation_sequences = ( + resolve_global_grad_accumulation_sequences( + global_grad_accumulation_sequences=global_grad_accumulation_sequences + ) + ) + dp_world_size = ps.get_data_parallel_world_size() + if ( + resolved_global_grad_accumulation_sequences <= 0 + or resolved_global_grad_accumulation_sequences % dp_world_size != 0 + ): + raise RuntimeError( + "Invalid global grad accumulation / DP world size combination: " + f"global_grad_accumulation_sequences={resolved_global_grad_accumulation_sequences}, " + f"dp_world_size={dp_world_size}" + ) + return resolved_global_grad_accumulation_sequences // dp_world_size + + +def build_micro_sample_indices( + step_index: int, + num_sequences: int, + global_grad_accumulation_sequences: int | None, +) -> list[int | None]: + dp_rank = ps.get_data_parallel_rank() + resolved_global_grad_accumulation_sequences = ( + resolve_global_grad_accumulation_sequences( + global_grad_accumulation_sequences=global_grad_accumulation_sequences + ) + ) + dp_world_size = ps.get_data_parallel_world_size() + local_grad_accumulation_sequences = resolve_local_grad_accumulation_sequences( + global_grad_accumulation_sequences=resolved_global_grad_accumulation_sequences, + ) + base_global_sample_index = step_index * resolved_global_grad_accumulation_sequences + global_step_indices: list[int | None] = [] + for offset in range(resolved_global_grad_accumulation_sequences): + global_sample_index = base_global_sample_index + offset + global_step_indices.append( + global_sample_index if global_sample_index < num_sequences else None + ) + return [ + global_step_indices[offset * dp_world_size + dp_rank] + for offset in range(local_grad_accumulation_sequences) + ] + + +def select_micro_inputs( + packed_tensors: PackedTensors, + sample_indices: list[int | None], + zero_template: PackedTensors, +) -> list[PackedTensors]: + return [ + _clone_packed_tensors(zero_template) + if sample_index is None + else select_indexed_inputs(packed_tensors, sample_index) + for sample_index in sample_indices + ] + + +def select_sft_micro_inputs( + trajectory_tensors: list[dict[str, torch.Tensor]], + sample_indices: list[int | None], + zero_template: dict[str, torch.Tensor], +) -> list[dict[str, torch.Tensor]]: + return [ + _clone_sft_tensors(zero_template) + if sample_index is None + else _clone_sft_tensors(trajectory_tensors[sample_index]) + for sample_index in sample_indices + ] + + +def _select_next_step_first_micro( + *, + packed_tensors: PackedTensors, + zero_template: PackedTensors, + step_index: int, + num_steps: int, + num_sequences: int, + global_grad_accumulation_sequences: int, +) -> PackedTensors | None: + next_step_index = step_index + 1 + if next_step_index >= num_steps: + return None + next_micro_indices = build_micro_sample_indices( + step_index=next_step_index, + num_sequences=num_sequences, + global_grad_accumulation_sequences=global_grad_accumulation_sequences, + ) + return select_micro_inputs( + packed_tensors, + [next_micro_indices[0]], + zero_template, + )[0] + + +def _move_inputs_to_device(inputs: PackedTensors, device: torch.device) -> None: + for key, value in inputs.items(): + if isinstance(value, torch.Tensor): + inputs[key] = value.to(device) # type: ignore[index] + + +def _count_trainable_tokens(inputs: LossInputs | DispatchedPackedTensors) -> float: + assistant_mask = inputs.align_inputs().assistant_mask + return float(assistant_mask.sum().item()) + + +def _local_trainable_token_count_tensor( + micro_inputs: list[LossInputs | DispatchedPackedTensors], + device: torch.device, +) -> torch.Tensor: + local_token_total = sum(_count_trainable_tokens(micro) for micro in micro_inputs) + return torch.tensor([local_token_total], device=device, dtype=torch.float32) + + +def _causal_attention_state( + seq_len: int, + device: torch.device, + *, + build_gdn_execution_spec: bool, + attention_head_dim: int | None = None, + attention_value_head_dim: int | None = None, +) -> Any: + group_ids = torch.zeros((1, seq_len), dtype=torch.int64, device=device) + parent_ids = torch.zeros_like(group_ids) + return create_shared_prefix_state( + group_ids=group_ids, + parent_ids=parent_ids, + build_gdn_execution_spec=build_gdn_execution_spec, + attention_head_dim=attention_head_dim, + attention_value_head_dim=attention_value_head_dim, + ) + + +def _next_micro_lookahead( + micro_inputs: list[Any], + micro_order: int, + trailing_micro: Any | None = None, +) -> Any | None: + next_micro_order = micro_order + 1 + if next_micro_order < len(micro_inputs): + return micro_inputs[next_micro_order] + return trailing_micro + + +def _prepare_dense_rl_micro( + micro: PackedTensors, + *, + device: torch.device, + provider: Any, + model_support_handler: Any, + ref_logprobs: torch.Tensor | None, +) -> PreparedRLMicroInputs: + _move_inputs_to_device(micro, device) + shifted_labels = shift_tensor(micro["tokens"], -100) + shifted_assistant_mask = shift_tensor(micro["assistant_mask"], False) + shifted_labels = torch.where( + shifted_assistant_mask, + shifted_labels, + torch.full_like(shifted_labels, -100), + ) + return PreparedRLMicroInputs( + model_tokens=micro["tokens"], + model_input_pos=micro["input_pos"], + model_labels=shifted_labels, + attention_state=create_shared_prefix_state( + group_ids=micro["group_ids"], + parent_ids=micro["parent_ids"], + build_gdn_execution_spec=bool( + getattr(model_support_handler, "build_gdn_execution_spec", False) + ), + attention_head_dim=getattr(provider, "kv_channels", None), + attention_value_head_dim=getattr(provider, "kv_channels", None), + ), + loss_inputs=LossInputs(inputs=micro), + ref_logprobs=ref_logprobs, + local_token_uids=packed_sequence_token_uids(micro, device=device), + ) + + +def _prepare_rl_cp_micro_full( + micro: PackedTensors, + *, + device: torch.device, + topology: ParallelTopology, + model_support_handler: Any, + trace_token_uids: bool, + ref_logprobs: torch.Tensor | None, +) -> PreparedMegatronBatch: + """Prepare RL CP inputs without moving planning metadata to CUDA first. + + CP lookahead relies on the CPU running this after backward has enqueued GPU + work. Moving the full packed micro to CUDA before planning forces later D2H + metadata reads and collapses that overlap. + """ + return prepare_cp_micro( + micro=micro, + topology=topology, + config=ContextParallelConfig(), + cp_group=ps.get_context_parallel_group(check_initialized=False), + cp_rank=ps.get_context_parallel_rank(), + build_gdn_execution_spec=bool( + getattr(model_support_handler, "build_gdn_execution_spec", False) + ), + trace_token_uids=trace_token_uids, + target_device=device, + ref_logprobs=ref_logprobs, + ) + + +def _prepared_rl_micro_from_cp_batch( + prepared: PreparedMegatronBatch, + *, + ref_logprobs: torch.Tensor | None, +) -> PreparedRLMicroInputs: + return PreparedRLMicroInputs( + model_tokens=prepared.tensors.tokens, + model_input_pos=prepared.tensors.input_pos, + model_labels=prepared.tensors.labels, + attention_state=prepared.attention_state, + packed_seq_params=prepared.packed_seq_params, + loss_inputs=prepared.tensors, + ref_logprobs=prepared.tensors.ref_logprobs + if ref_logprobs is not None + else None, + local_token_uids=prepared.tensors.token_uids, + ) + + +def _empty_new_logprobs_from_logits( + logits: torch.Tensor, labels: torch.Tensor +) -> torch.Tensor: + if int(labels.numel()) != 0: + raise ValueError("empty-logprob path requires empty local labels") + if logits.ndim < 3 or int(logits.shape[-1]) == 0: + raise ValueError( + f"expected empty local logits [B, S, V], got {tuple(logits.shape)}" + ) + candidate = logits[..., 0] + if tuple(candidate.shape) == tuple(labels.shape): + return candidate + candidate = candidate.transpose(0, 1).contiguous() + if tuple(candidate.shape) != tuple(labels.shape): + raise ValueError( + "empty local logits shape must match labels after removing vocab dim, " + f"got logits={tuple(logits.shape)} labels={tuple(labels.shape)}" + ) + return candidate + + +def _prepare_current_rl_micro( + micro: PackedTensors, + *, + device: torch.device, + topology: ParallelTopology, + provider: Any, + model_support_handler: Any, + ref_logprobs: torch.Tensor | None, + trace_token_uids: bool, + pending_prepared_micro: PreparedMegatronBatch | None, +) -> tuple[PreparedRLMicroInputs, PreparedMegatronBatch | None]: + if int(topology.cp) <= 1: + return ( + _prepare_dense_rl_micro( + micro, + device=device, + provider=provider, + model_support_handler=model_support_handler, + ref_logprobs=ref_logprobs, + ), + pending_prepared_micro, + ) + prepared = pending_prepared_micro + if prepared is None: + prepared = _prepare_rl_cp_micro_full( + micro, + device=device, + topology=topology, + model_support_handler=model_support_handler, + trace_token_uids=trace_token_uids, + ref_logprobs=ref_logprobs, + ) + return _prepared_rl_micro_from_cp_batch(prepared, ref_logprobs=ref_logprobs), None + + +def _prepare_next_rl_cp_micro( + next_micro: PackedTensors | None, + *, + device: torch.device, + topology: ParallelTopology, + model_support_handler: Any, + trace_token_uids: bool, + ref_logprobs: torch.Tensor | None = None, +) -> PreparedMegatronBatch | None: + if next_micro is None or int(topology.cp) <= 1: + return None + return _prepare_rl_cp_micro_full( + next_micro, + device=device, + topology=topology, + model_support_handler=model_support_handler, + trace_token_uids=trace_token_uids, + ref_logprobs=ref_logprobs, + ) + + +def _count_sft_trainable_tokens( + inputs: dict[str, torch.Tensor] | PreparedSFTMicroInputs, +) -> float: + if isinstance(inputs, PreparedSFTMicroInputs): + return float(inputs.loss_mask.sum().item()) + attention_mask = inputs["attention_mask"].reshape(-1) + actual_len = int(attention_mask.sum().item()) + labels = inputs["labels"].reshape(-1)[:actual_len].unsqueeze(0) + shifted_labels = shift_tensor(labels, -100) + return float((shifted_labels != -100).sum().item()) + + +def _local_trainable_sft_token_count_tensor( + micro_inputs: Sequence[dict[str, torch.Tensor] | PreparedSFTMicroInputs], + device: torch.device, +) -> torch.Tensor: + local_token_total = sum( + _count_sft_trainable_tokens(micro) for micro in micro_inputs + ) + return torch.tensor([local_token_total], device=device, dtype=torch.float32) + + +def _prepare_dense_sft_micro( + micro: dict[str, torch.Tensor], + *, + device: torch.device, + provider: Any, + model_support_handler: Any, +) -> PreparedSFTMicroInputs: + attention_mask = micro["attention_mask"].reshape(-1) + seq_len = max(int(attention_mask.sum().item()), 1) + input_ids = micro["input_ids"].reshape(-1)[:seq_len].unsqueeze(0).to(device) + labels = micro["labels"].reshape(-1)[:seq_len].unsqueeze(0).to(device) + position_ids = torch.arange(seq_len, device=device).unsqueeze(0) + shifted_labels = shift_tensor(labels, -100) + loss_mask = shifted_labels != -100 + return PreparedSFTMicroInputs( + input_ids=input_ids, + position_ids=position_ids, + labels=shifted_labels, + loss_mask=loss_mask, + attention_state=_causal_attention_state( + seq_len, + device, + build_gdn_execution_spec=bool( + getattr(model_support_handler, "build_gdn_execution_spec", False) + ), + attention_head_dim=getattr(provider, "kv_channels", None), + attention_value_head_dim=getattr(provider, "kv_channels", None), + ), + local_token_uids=sft_sequence_token_uids(micro, device=device)[ + :, : int(input_ids.shape[1]) + ], + ) + + +def _sft_inputs_to_sparse_packed_tensors( + inputs: dict[str, torch.Tensor], + *, + device: torch.device, +) -> PackedTensors: + input_ids = inputs["input_ids"].reshape(-1) + attention_mask = inputs["attention_mask"].reshape(-1) + labels = inputs["labels"].reshape(-1) + actual_len = max(int(attention_mask.sum().item()), 1) + total_tokens = int(input_ids.numel()) + + group_ids = torch.full((1, total_tokens), -1, device=device, dtype=torch.long) + parent_ids = torch.full((1, total_tokens), -1, device=device, dtype=torch.long) + group_ids[:, :actual_len] = 0 + parent_ids[:, :actual_len] = 0 + + assistant_mask = (labels != -100).unsqueeze(0).to(device=device, dtype=torch.bool) + return PackedTensors( + tokens=input_ids.unsqueeze(0).to(device=device, dtype=torch.long), + group_ids=group_ids, + parent_ids=parent_ids, + input_pos=torch.arange(total_tokens, device=device, dtype=torch.long).unsqueeze( + 0 + ), + assistant_mask=assistant_mask, + logprobs=torch.full( + (1, total_tokens), + float("nan"), + device=device, + dtype=torch.float32, + ), + advantages=torch.zeros((1, total_tokens), device=device, dtype=torch.float32), + weights=assistant_mask.to(dtype=torch.float32), + pixel_values=[None], + image_grid_thw=[None], + moe_routing_replay=None, + ) + + +def _prepare_sft_cp_micro_full( + micro: dict[str, torch.Tensor], + *, + device: torch.device, + topology: ParallelTopology, + model_support_handler: Any, + trace_token_uids: bool, +) -> PreparedMegatronBatch: + """Prepare SFT CP inputs through the same CPU-planning boundary as RL CP. + + The synthetic sparse-packed metadata is constructed on CPU and only the + rank-local dispatched tensors are moved to `device`. Constructing it on CUDA + would make shared-prefix planning read metadata back from the GPU. + """ + sparse_micro = _sft_inputs_to_sparse_packed_tensors( + micro, + device=torch.device("cpu"), + ) + return prepare_cp_micro( + micro=sparse_micro, + topology=topology, + config=ContextParallelConfig(), + cp_group=ps.get_context_parallel_group(check_initialized=False), + cp_rank=ps.get_context_parallel_rank(), + build_gdn_execution_spec=bool( + getattr(model_support_handler, "build_gdn_execution_spec", False) + ), + trace_token_uids=trace_token_uids, + target_device=device, + ) + + +def _prepared_sft_micro_from_cp_batch( + prepared: PreparedMegatronBatch, +) -> PreparedSFTMicroInputs: + loss_mask = prepared.tensors.assistant_mask + return PreparedSFTMicroInputs( + input_ids=prepared.tensors.tokens, + position_ids=prepared.tensors.input_pos, + labels=prepared.tensors.labels.masked_fill(~loss_mask, -100), + loss_mask=loss_mask, + attention_state=prepared.attention_state, + packed_seq_params=prepared.packed_seq_params, + local_token_uids=prepared.tensors.token_uids, + ) + + +def _prepare_current_sft_micro( + micro: dict[str, torch.Tensor], + *, + device: torch.device, + topology: ParallelTopology, + provider: Any, + model_support_handler: Any, + trace_token_uids: bool, + pending_prepared_micro: PreparedMegatronBatch | None, +) -> tuple[PreparedSFTMicroInputs, PreparedMegatronBatch | None]: + if int(topology.cp) <= 1: + return ( + _prepare_dense_sft_micro( + micro, + device=device, + provider=provider, + model_support_handler=model_support_handler, + ), + pending_prepared_micro, + ) + prepared = pending_prepared_micro + if prepared is None: + prepared = _prepare_sft_cp_micro_full( + micro, + device=device, + topology=topology, + model_support_handler=model_support_handler, + trace_token_uids=trace_token_uids, + ) + return _prepared_sft_micro_from_cp_batch(prepared), None + + +def _prepare_next_sft_cp_micro( + next_micro: dict[str, torch.Tensor] | None, + *, + device: torch.device, + topology: ParallelTopology, + model_support_handler: Any, + trace_token_uids: bool, +) -> PreparedMegatronBatch | None: + if next_micro is None or int(topology.cp) <= 1: + return None + return _prepare_sft_cp_micro_full( + next_micro, + device=device, + topology=topology, + model_support_handler=model_support_handler, + trace_token_uids=trace_token_uids, + ) diff --git a/src/art/megatron/training/offload.py b/src/art/megatron/training/offload.py index a25b9f120..4b6b6939b 100644 --- a/src/art/megatron/training/offload.py +++ b/src/art/megatron/training/offload.py @@ -1,6 +1,7 @@ from collections.abc import Iterator from dataclasses import dataclass, field import gc +import logging from typing import Any, Sequence, cast from megatron.core.distributed import DistributedDataParallel @@ -8,6 +9,15 @@ from .model_chunks import unwrap_megatron_chunk +logger = logging.getLogger(__name__) + +OFFLOADED_TRAINABLE_BUFFERS_MESSAGE = ( + "Offloaded Megatron trainable param buffers to CPU" +) +RELOADED_TRAINABLE_BUFFERS_MESSAGE = "Reloaded Megatron trainable param buffers to GPU" +OFFLOADED_FROZEN_PARAMS_MESSAGE = "Offloaded frozen model params to CPU" +RELOADED_FROZEN_PARAMS_MESSAGE = "Reloaded frozen model params to GPU" + @dataclass class OffloadState: @@ -36,6 +46,29 @@ def _iter_megatron_param_buffers(model: Sequence[torch.nn.Module]) -> Iterator[A yield from expert_buffers +def _rank0_info(rank: int, message: str) -> None: + if rank == 0: + logger.info(message) + + +def offload_trainable_buffers_to_cpu( + model: Sequence[torch.nn.Module], + rank: int, +) -> None: + for param_buffer in _iter_megatron_param_buffers(model): + param_buffer.offload_to_cpu(move_params=True, move_grads=True) + _rank0_info(rank, OFFLOADED_TRAINABLE_BUFFERS_MESSAGE) + + +def reload_trainable_buffers_to_gpu( + model: Sequence[torch.nn.Module], + rank: int, +) -> None: + for param_buffer in _iter_megatron_param_buffers(model): + param_buffer.reload_from_cpu(move_params=True, move_grads=True) + _rank0_info(rank, RELOADED_TRAINABLE_BUFFERS_MESSAGE) + + def offload_to_cpu( model: Sequence[torch.nn.Module], rank: int, @@ -46,8 +79,7 @@ def offload_to_cpu( return pinned_buffers = offload_state.pinned_buffers - for param_buffer in _iter_megatron_param_buffers(model): - param_buffer.offload_to_cpu(move_params=True, move_grads=True) + offload_trainable_buffers_to_cpu(model, rank) # Megatron remaps trainable params into contiguous DDP buffers. Offload those via the # native buffer APIs above, and only manually offload frozen params here. @@ -75,8 +107,7 @@ def offload_to_cpu( gc.collect() torch.cuda.empty_cache() offload_state.is_offloaded = True - if rank == 0: - print("Offloaded model params to CPU") + _rank0_info(rank, OFFLOADED_FROZEN_PARAMS_MESSAGE) def reload_to_gpu( @@ -94,8 +125,7 @@ def reload_to_gpu( else: device = torch.device(device) - for param_buffer in _iter_megatron_param_buffers(model): - param_buffer.reload_from_cpu(move_params=True, move_grads=True) + reload_trainable_buffers_to_gpu(model, rank) # Reload frozen params that were manually offloaded. for chunk in model: @@ -112,5 +142,4 @@ def reload_to_gpu( torch.cuda.synchronize() offload_state.is_offloaded = False - if rank == 0: - print("Reloaded LoRA params to GPU") + _rank0_info(rank, RELOADED_FROZEN_PARAMS_MESSAGE) diff --git a/src/art/megatron/training/streaming_weight_offload.py b/src/art/megatron/training/streaming_weight_offload.py new file mode 100644 index 000000000..41ffc219c --- /dev/null +++ b/src/art/megatron/training/streaming_weight_offload.py @@ -0,0 +1,541 @@ +from __future__ import annotations + +from collections import deque +from collections.abc import Sequence +from contextlib import suppress +import logging +import os +import threading +from typing import Any, Literal + +from megatron.core.models.gpt import GPTModel +from megatron.core.tensor_parallel.random import is_checkpointing +from pydantic import BaseModel, ConfigDict, Field +import torch + +from .model_chunks import ModelChunks + +logger = logging.getLogger(__name__) + +LayerOffloadStatus = Literal["cpu", "gpu", "loading"] +LAYER_STATUS_CPU: LayerOffloadStatus = "cpu" +LAYER_STATUS_GPU: LayerOffloadStatus = "gpu" +LAYER_STATUS_LOADING: LayerOffloadStatus = "loading" +STREAMING_INSTALLED_MESSAGE = ( + "Installed streaming frozen weight offload for %d layers (%d rank-local params)" +) +STREAMING_COMPILED_LAYERS_MESSAGE = ( + "Streaming weight offload managing compiled transformer layers" +) + + +def _rank0_info(rank: int, message: str, *args: object) -> None: + if rank == 0: + logger.info(message, *args) + + +class StreamingWeightOffloadConfig(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + enabled: bool = False + num_layers: int = Field(default=0, ge=0) + num_slots: int = Field(default=4, ge=2) + resident_layers: int = Field(default=2, ge=1) + + +class _ParamSpec: + def __init__( + self, + *, + name: str, + param: torch.nn.Parameter, + offset: int, + numel: int, + shape: torch.Size, + ) -> None: + self.name = name + self.param = param + self.offset = offset + self.numel = numel + self.shape = shape + + +class _TensorGroup: + def __init__( + self, *, dtype: torch.dtype, cpu_flat: torch.Tensor, specs: list[_ParamSpec] + ): + self.dtype = dtype + self.cpu_flat = cpu_flat + self.specs = specs + + +class _LoadSlot: + def __init__(self, index: int): + self.index = index + self.owner: _LayerState | None = None + self.release_stream: torch.cuda.Stream | None = None + self.pinned: dict[torch.dtype, torch.Tensor] = {} + self.gpu: dict[torch.dtype, torch.Tensor] = {} + + def ensure_capacity(self, dtype: torch.dtype, numel: int) -> None: + pinned = self.pinned.get(dtype) + if pinned is None or pinned.numel() < numel: + self.pinned[dtype] = torch.empty( + numel, dtype=dtype, device="cpu", pin_memory=True + ) + gpu = self.gpu.get(dtype) + if gpu is None or gpu.numel() < numel: + self.gpu[dtype] = torch.empty( + numel, dtype=dtype, device=torch.cuda.current_device() + ) + + +class _LayerState: + def __init__(self, index: int, layer: torch.nn.Module, groups: list[_TensorGroup]): + self.index = index + self.layer = layer + self.groups = groups + self.status: LayerOffloadStatus = LAYER_STATUS_GPU + self.slot: _LoadSlot | None = None + self.load_event: torch.cuda.Event | None = None + self.load_ready = False + self.load_error: BaseException | None = None + + +class StreamingWeightOffloader: + def __init__( + self, + *, + layers: list[torch.nn.Module], + rank: int, + config: StreamingWeightOffloadConfig, + ) -> None: + self.rank = rank + self.config = config + selected_layers = layers[: config.num_layers or len(layers)] + self.layers = [ + _LayerState(i, layer, _build_tensor_groups(_frozen_cuda_parameters(layer))) + for i, layer in enumerate(selected_layers) + ] + self.device = torch.cuda.current_device() + self.h2d_stream = torch.cuda.Stream() + self.slots = [_LoadSlot(i) for i in range(config.num_slots)] + self._condition = threading.Condition() + self._queue: deque[tuple[_LayerState, _LoadSlot]] = deque() + self._worker_error: BaseException | None = None + self._closed = False + self._worker = threading.Thread( + target=self._load_worker, + name=f"streaming_weight_offload_rank{rank}", + daemon=True, + ) + self._hooks: list[Any] = [] + + def install(self) -> None: + if not self.layers: + raise RuntimeError( + "Streaming weight offload found no transformer layers to manage" + ) + param_count = sum( + spec.numel + for layer in self.layers + for group in layer.groups + for spec in group.specs + ) + if param_count == 0: + raise RuntimeError( + "Streaming weight offload found no frozen CUDA parameters to manage" + ) + self._worker.start() + for layer_state in self.layers: + self._hooks.append( + layer_state.layer.register_forward_pre_hook( + lambda module, inputs, state=layer_state: self._pre_forward(state) + ) + ) + self._hooks.append( + layer_state.layer.register_forward_hook( + lambda module, inputs, output, state=layer_state: ( + self._post_forward(state) + ) + ) + ) + self.offload_all(wait=True) + _rank0_info( + self.rank, STREAMING_INSTALLED_MESSAGE, len(self.layers), param_count + ) + + def begin_job(self) -> None: + self._prefetch_window(0, 1, self.config.resident_layers) + + def finish_job(self) -> None: + self.offload_all(wait=True) + + def remove(self) -> None: + for handle in self._hooks: + handle.remove() + self._hooks.clear() + with self._condition: + self._closed = True + self._condition.notify_all() + self._worker.join(timeout=5.0) + + def offload_all(self, *, wait: bool) -> None: + for layer_state in self.layers: + self._ensure_offloaded(layer_state) + if wait: + torch.cuda.empty_cache() + + def _pre_forward(self, layer_state: _LayerState) -> None: + recompute_forward = _is_recompute_forward() + if recompute_forward: + self._offload_recomputed_successors(layer_state.index) + self._finish_load(layer_state) + if recompute_forward: + self._prefetch_window( + layer_state.index - 1, -1, self.config.resident_layers - 1 + ) + else: + self._prefetch_window( + layer_state.index + 1, 1, self.config.resident_layers - 1 + ) + + def _post_forward(self, layer_state: _LayerState) -> None: + if is_checkpointing() and not torch.is_grad_enabled(): + self._start_offload(layer_state) + self._prefetch_window(layer_state.index + self.config.resident_layers, 1, 1) + + def _offload_recomputed_successors(self, index: int) -> None: + for layer_state in self.layers[index + 1 :]: + if layer_state.status in {LAYER_STATUS_GPU, LAYER_STATUS_LOADING}: + self._ensure_offloaded(layer_state) + + def _start_load(self, index: int) -> None: + self._check_worker_error() + if index < 0 or index >= len(self.layers): + return + layer_state = self.layers[index] + if layer_state.status in {LAYER_STATUS_GPU, LAYER_STATUS_LOADING}: + return + if layer_state.status != LAYER_STATUS_CPU: + raise RuntimeError(f"Unexpected layer offload state {layer_state.status!r}") + slot = self._acquire_slot() + layer_state.slot = slot + layer_state.load_event = None + layer_state.load_ready = False + layer_state.load_error = None + layer_state.status = LAYER_STATUS_LOADING + slot.owner = layer_state + with self._condition: + self._queue.append((layer_state, slot)) + self._condition.notify() + + def _prefetch_window(self, start_index: int, step: int, count: int) -> None: + if step not in {-1, 1}: + raise RuntimeError(f"Unexpected streaming prefetch step {step}") + self._check_worker_error() + if count <= 0: + return + end_index = start_index + step * count + for index in range(start_index, end_index, step): + self._start_load(index) + + def _finish_load(self, layer_state: _LayerState) -> None: + self._check_worker_error() + if layer_state.status == LAYER_STATUS_GPU: + return + if layer_state.status == LAYER_STATUS_CPU: + self._start_load(layer_state.index) + if layer_state.status != LAYER_STATUS_LOADING: + raise RuntimeError(f"Unexpected layer load state {layer_state.status!r}") + self._wait_for_load_launch(layer_state) + if layer_state.load_error is not None: + raise RuntimeError( + f"Streaming weight offload failed while loading layer {layer_state.index}" + ) from layer_state.load_error + if layer_state.load_event is None or layer_state.slot is None: + raise RuntimeError(f"Unexpected layer load state {layer_state.status!r}") + # Transformer Engine can launch work on internal streams. Complete the H2D + # copy before installing the parameter pointer so every downstream stream + # observes initialized weights. + layer_state.load_event.synchronize() + self._install_gpu_views(layer_state) + layer_state.load_event = None + layer_state.status = LAYER_STATUS_GPU + + def _ensure_offloaded(self, layer_state: _LayerState) -> None: + if layer_state.status == LAYER_STATUS_CPU: + return + if layer_state.status == LAYER_STATUS_LOADING: + self._finish_load(layer_state) + if layer_state.status == LAYER_STATUS_GPU: + self._start_offload(layer_state) + + def _start_offload(self, layer_state: _LayerState) -> None: + if layer_state.status == LAYER_STATUS_CPU: + return + if layer_state.status == LAYER_STATUS_LOADING: + self._finish_load(layer_state) + if layer_state.status != LAYER_STATUS_GPU: + raise RuntimeError(f"Unexpected layer offload state {layer_state.status!r}") + current_stream = torch.cuda.current_stream() + slot = layer_state.slot + if slot is not None: + for tensor in slot.gpu.values(): + tensor.record_stream(current_stream) + slot.owner = None + slot.release_stream = current_stream + self._install_cpu_views(layer_state) + layer_state.slot = None + layer_state.status = LAYER_STATUS_CPU + + def _acquire_slot(self) -> _LoadSlot: + free_slots = [slot for slot in self.slots if slot.owner is None] + if not free_slots: + raise RuntimeError( + "Streaming weight offload has no free load slots; increase " + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_NUM_SLOTS" + ) + return next( + (slot for slot in free_slots if slot.release_stream is None), + free_slots[0], + ) + + def _load_worker(self) -> None: + torch.cuda.set_device(self.device) + while True: + with self._condition: + while not self._queue and not self._closed: + self._condition.wait() + if self._closed and not self._queue: + return + layer_state, slot = self._queue.popleft() + try: + self._run_load(layer_state, slot) + except BaseException as exc: # noqa: BLE001 - propagated to training thread. + with self._condition: + layer_state.load_error = exc + layer_state.load_ready = True + self._worker_error = exc + self._condition.notify_all() + + def _run_load(self, layer_state: _LayerState, slot: _LoadSlot) -> None: + for group in layer_state.groups: + slot.ensure_capacity(group.dtype, group.cpu_flat.numel()) + slot.pinned[group.dtype][: group.cpu_flat.numel()].copy_( + group.cpu_flat, + non_blocking=False, + ) + release_stream = slot.release_stream + if release_stream is not None: + self.h2d_stream.wait_stream(release_stream) + slot.release_stream = None + with torch.cuda.stream(self.h2d_stream): + for group in layer_state.groups: + n = group.cpu_flat.numel() + gpu_tensor = slot.gpu[group.dtype][:n] + gpu_tensor.copy_(slot.pinned[group.dtype][:n], non_blocking=True) + gpu_tensor.record_stream(self.h2d_stream) + event = torch.cuda.Event() + event.record(self.h2d_stream) + with self._condition: + layer_state.load_event = event + layer_state.load_ready = True + self._condition.notify_all() + + def _wait_for_load_launch(self, layer_state: _LayerState) -> None: + with self._condition: + while not layer_state.load_ready and self._worker_error is None: + self._condition.wait() + self._check_worker_error() + + def _check_worker_error(self) -> None: + if self._worker_error is not None: + raise RuntimeError( + "Streaming weight offload worker failed" + ) from self._worker_error + + def _install_cpu_views(self, layer_state: _LayerState) -> None: + for group in layer_state.groups: + for spec in group.specs: + _validate_streamed_param(spec) + spec.param.data = group.cpu_flat[ + spec.offset : spec.offset + spec.numel + ].view(spec.shape) + + def _install_gpu_views(self, layer_state: _LayerState) -> None: + if layer_state.slot is None: + raise RuntimeError( + "Cannot install GPU views before a layer has a load slot" + ) + for group in layer_state.groups: + gpu_flat = layer_state.slot.gpu[group.dtype] + for spec in group.specs: + _validate_streamed_param(spec) + spec.param.data = gpu_flat[spec.offset : spec.offset + spec.numel].view( + spec.shape + ) + + +def streaming_weight_offload_config_from_env() -> StreamingWeightOffloadConfig: + config = StreamingWeightOffloadConfig( + enabled=_env_flag("ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD"), + num_layers=_env_int("ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_NUM_LAYERS", 0), + num_slots=_env_int("ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_NUM_SLOTS", 4), + resident_layers=_env_int( + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_RESIDENT_LAYERS", 2 + ), + ) + if config.resident_layers > config.num_slots: + raise RuntimeError( + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_RESIDENT_LAYERS must be <= " + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_NUM_SLOTS" + ) + return config + + +def install_streaming_weight_offload( + *, + model: ModelChunks, + rank: int, + compile_enabled: bool, + config: StreamingWeightOffloadConfig, +) -> StreamingWeightOffloader | None: + if not config.enabled: + return None + layers = _transformer_layers(model) + if not layers: + raise RuntimeError("Streaming weight offload could not find transformer layers") + _validate_checkpoint_shape(layers[0]) + if compile_enabled: + _rank0_info(rank, STREAMING_COMPILED_LAYERS_MESSAGE) + offloader = StreamingWeightOffloader(layers=layers, rank=rank, config=config) + offloader.install() + return offloader + + +def maybe_install_streaming_weight_offload( + *, + model: ModelChunks, + rank: int, + compile_enabled: bool, +) -> StreamingWeightOffloader | None: + return install_streaming_weight_offload( + model=model, + rank=rank, + compile_enabled=compile_enabled, + config=streaming_weight_offload_config_from_env(), + ) + + +def _validate_checkpoint_shape(layer: torch.nn.Module) -> None: + config = getattr(layer, "config", None) + if ( + getattr(config, "recompute_granularity", None) != "full" + or getattr(config, "recompute_method", None) != "uniform" + or int(getattr(config, "recompute_num_layers", 0) or 0) != 1 + ): + raise RuntimeError( + "Streaming weight offload requires full uniform activation recompute with " + "recompute_num_layers=1" + ) + + +def _transformer_layers(model: Sequence[torch.nn.Module]) -> list[torch.nn.Module]: + layers: list[torch.nn.Module] = [] + for chunk in model: + module = _unwrap_module(chunk) + gpt_module = ( + module + if isinstance(module, GPTModel) + else getattr(module, "language_model", None) + ) + decoder = getattr(gpt_module, "decoder", None) + chunk_layers = getattr(decoder, "layers", None) + if chunk_layers is not None: + layers.extend(list(chunk_layers)) + return layers + + +def _unwrap_module(module: torch.nn.Module) -> torch.nn.Module: + current = module + seen: set[int] = set() + while id(current) not in seen: + seen.add(id(current)) + for attr_name in ("_orig_mod", "module"): + child = getattr(current, attr_name, None) + if isinstance(child, torch.nn.Module): + current = child + break + else: + return current + return current + + +def _frozen_cuda_parameters( + module: torch.nn.Module, +) -> list[tuple[str, torch.nn.Parameter]]: + return [ + (name, param) + for name, param in module.named_parameters() + if isinstance(param, torch.nn.Parameter) + and not param.requires_grad + and param.device.type == "cuda" + ] + + +def _build_tensor_groups( + params: list[tuple[str, torch.nn.Parameter]], +) -> list[_TensorGroup]: + grouped: dict[torch.dtype, list[tuple[str, torch.nn.Parameter]]] = {} + for name, param in params: + grouped.setdefault(param.dtype, []).append((name, param)) + groups: list[_TensorGroup] = [] + for dtype, dtype_params in grouped.items(): + total_numel = sum(param.numel() for _name, param in dtype_params) + cpu_flat = torch.empty(total_numel, dtype=dtype, device="cpu") + specs: list[_ParamSpec] = [] + offset = 0 + for name, param in dtype_params: + numel = param.numel() + cpu_flat[offset : offset + numel].copy_(param.detach().view(-1).cpu()) + specs.append( + _ParamSpec( + name=name, + param=param, + offset=offset, + numel=numel, + shape=param.shape, + ) + ) + offset += numel + groups.append(_TensorGroup(dtype=dtype, cpu_flat=cpu_flat, specs=specs)) + return groups + + +def _validate_streamed_param(spec: _ParamSpec) -> None: + if spec.param.requires_grad: + raise RuntimeError( + "Streaming weight offload cannot manage trainable parameter " + f"{spec.name}; trainable parameters must remain owned by Megatron buffers" + ) + + +def _is_recompute_forward() -> bool: + return is_checkpointing() and torch.is_grad_enabled() + + +def _env_flag(name: str, default: bool = False) -> bool: + raw = os.environ.get(name) + if raw is None or not raw.strip(): + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} + + +def _env_int(name: str, default: int) -> int: + raw = os.environ.get(name) + if raw is None or not raw.strip(): + return default + with suppress(ValueError): + return int(raw) + raise RuntimeError(f"{name} must be an integer, got {raw!r}") diff --git a/src/art/megatron/training/trace.py b/src/art/megatron/training/trace.py new file mode 100644 index 000000000..56435c3be --- /dev/null +++ b/src/art/megatron/training/trace.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from collections.abc import Iterator, Sequence +from contextlib import contextmanager +import os +from typing import Any + +import torch + +from art.megatron.context_parallel.types import ParallelTopology +from art.preprocessing.pack import PackedTensors + +ROOT_OUTPUT_TOKEN_UIDS_ATTR = "_art_root_output_token_uids" +TRACE_ROW_TOKEN_UIDS_ATTR = "_art_trace_row_token_uids" +TRACE_UID_SPAN_ATTR = "_art_trace_uid_span" + + +def trace_token_uids_enabled() -> bool: + raw = os.environ.get("ART_MEGATRON_ATTACH_TOKEN_UIDS", "") + return raw.strip().lower() in {"1", "true", "yes", "on"} + + +def context_parallel_trace_token_uids_enabled( + topology: ParallelTopology, + moe_routing_replay_controller: Any | None, +) -> bool: + return int(topology.cp) > 1 and ( + moe_routing_replay_controller is not None or trace_token_uids_enabled() + ) + + +def packed_sequence_token_uids( + micro: PackedTensors, + *, + device: torch.device, +) -> torch.Tensor: + del device + return torch.arange( + int(micro["tokens"].shape[1]), + dtype=torch.int64, + ).unsqueeze(0) + + +def sft_sequence_token_uids( + inputs: dict[str, torch.Tensor], + *, + device: torch.device, +) -> torch.Tensor: + del device + attention_mask = inputs["attention_mask"].reshape(-1) + actual_len = max(int(attention_mask.sum().item()), 1) + total_tokens = int(inputs["input_ids"].numel()) + token_uids = torch.full( + (1, total_tokens), + -1, + dtype=torch.int64, + ) + token_uids[:, :actual_len] = torch.arange( + actual_len, + dtype=torch.int64, + ).unsqueeze(0) + return token_uids + + +def flatten_local_token_uids( + token_uids: torch.Tensor | None, +) -> torch.Tensor | None: + if token_uids is None: + return None + return ( + token_uids.transpose(0, 1) + .contiguous() + .reshape(-1) + .to(dtype=torch.int64) + .contiguous() + ) + + +def prepare_replay_local_input_token_uids( + moe_routing_replay_controller: Any | None, + token_uids: torch.Tensor | None, + attention_state: Any | None = None, +) -> None: + if moe_routing_replay_controller is None or not hasattr( + moe_routing_replay_controller, + "prepare_micro_targets", + ): + return + token_uid_sets = _routing_replay_token_uid_sets( + token_uids, + attention_state=attention_state, + ) + moe_routing_replay_controller.prepare_micro_targets(token_uid_sets) + + +def _routing_replay_token_uid_sets( + token_uids: torch.Tensor | None, + *, + attention_state: Any | None, +) -> dict[str, torch.Tensor | None]: + plan = getattr(attention_state, "gdn_execution_plan", None) + if plan is not None: + return { + "attention": torch.tensor( + tuple(getattr(plan, "attention_token_indices")), + dtype=torch.int64, + ), + "gdn": torch.tensor( + tuple(getattr(plan, "gdn_token_indices")), + dtype=torch.int64, + ), + } + return {"attention": flatten_local_token_uids(token_uids)} + + +def _set_root_output_trace_token_uids( + root_module: torch.nn.Module, + token_uids: torch.Tensor | None, +) -> None: + if token_uids is None: + if hasattr(root_module, ROOT_OUTPUT_TOKEN_UIDS_ATTR): + delattr(root_module, ROOT_OUTPUT_TOKEN_UIDS_ATTR) + return + setattr( + root_module, + ROOT_OUTPUT_TOKEN_UIDS_ATTR, + token_uids.detach().to(device="cpu", dtype=torch.int64).contiguous(), + ) + + +def _set_module_trace_token_uids( + model_chunks: Sequence[torch.nn.Module], + token_uids: torch.Tensor | None, +) -> None: + row_token_uids = flatten_local_token_uids(token_uids) + for chunk in model_chunks: + for module in chunk.modules(): + if row_token_uids is None: + if hasattr(module, TRACE_ROW_TOKEN_UIDS_ATTR): + delattr(module, TRACE_ROW_TOKEN_UIDS_ATTR) + if hasattr(module, TRACE_UID_SPAN_ATTR): + delattr(module, TRACE_UID_SPAN_ATTR) + continue + setattr( + module, + TRACE_ROW_TOKEN_UIDS_ATTR, + row_token_uids.detach() + .to(device="cpu", dtype=torch.int64) + .contiguous(), + ) + if hasattr(module, TRACE_UID_SPAN_ATTR): + delattr(module, TRACE_UID_SPAN_ATTR) + + +@contextmanager +def attach_trace_token_uids( + model_chunks: Sequence[torch.nn.Module], + token_uids: torch.Tensor | None, +) -> Iterator[None]: + attach_module_token_uids = trace_token_uids_enabled() + _set_root_output_trace_token_uids(model_chunks[0], token_uids) + if attach_module_token_uids: + _set_module_trace_token_uids(model_chunks, token_uids) + try: + yield + finally: + _set_root_output_trace_token_uids(model_chunks[0], None) + if attach_module_token_uids: + _set_module_trace_token_uids(model_chunks, None) diff --git a/src/art/megatron/training/weight_offload.py b/src/art/megatron/training/weight_offload.py new file mode 100644 index 000000000..ab37efb6d --- /dev/null +++ b/src/art/megatron/training/weight_offload.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +import gc +import os + +import torch + +from .model_chunks import ModelChunks +from .offload import ( + OffloadState, + offload_to_cpu, + offload_trainable_buffers_to_cpu, + reload_to_gpu, + reload_trainable_buffers_to_gpu, +) +from .streaming_weight_offload import ( + StreamingWeightOffloadConfig, + StreamingWeightOffloader, + install_streaming_weight_offload, + streaming_weight_offload_config_from_env, +) + +OFFLOAD_BETWEEN_JOBS_ENV = "ART_MEGATRON_OFFLOAD_BETWEEN_JOBS" + + +class WeightOffloadManager: + def __init__( + self, + *, + model: ModelChunks, + rank: int, + compile_enabled: bool, + offload_between_jobs: bool, + streaming_config: StreamingWeightOffloadConfig, + ) -> None: + self.model = model + self.rank = rank + self.compile_enabled = compile_enabled + self.offload_between_jobs = offload_between_jobs + self.streaming_config = streaming_config + self.offload_state = OffloadState() + self.streaming: StreamingWeightOffloader | None = None + + @classmethod + def from_env( + cls, + *, + model: ModelChunks, + rank: int, + compile_enabled: bool, + ) -> WeightOffloadManager: + return cls( + model=model, + rank=rank, + compile_enabled=compile_enabled, + offload_between_jobs=_env_flag(OFFLOAD_BETWEEN_JOBS_ENV, default=True), + streaming_config=streaming_weight_offload_config_from_env(), + ) + + @classmethod + def from_config( + cls, + *, + model: ModelChunks, + rank: int, + compile_enabled: bool, + offload_between_jobs: bool = True, + streaming_config: StreamingWeightOffloadConfig | None = None, + ) -> WeightOffloadManager: + return cls( + model=model, + rank=rank, + compile_enabled=compile_enabled, + offload_between_jobs=offload_between_jobs, + streaming_config=streaming_config or StreamingWeightOffloadConfig(), + ) + + def install(self) -> None: + self.streaming = install_streaming_weight_offload( + model=self.model, + rank=self.rank, + compile_enabled=self.compile_enabled, + config=self.streaming_config, + ) + + def before_job(self) -> None: + if self.offload_between_jobs: + if self.streaming is None: + reload_to_gpu(self.model, self.rank, self.offload_state) + else: + reload_trainable_buffers_to_gpu(self.model, self.rank) + if self.streaming is not None: + self.streaming.begin_job() + + def after_job(self) -> None: + did_release_gpu_memory = False + if self.streaming is not None: + self.streaming.finish_job() + did_release_gpu_memory = True + if self.offload_between_jobs: + if self.streaming is None: + offload_to_cpu(self.model, self.rank, self.offload_state) + else: + offload_trainable_buffers_to_cpu(self.model, self.rank) + did_release_gpu_memory = True + if did_release_gpu_memory: + gc.collect() + torch.cuda.empty_cache() + + @contextmanager + def job(self) -> Iterator[None]: + self.before_job() + try: + yield + finally: + self.after_job() + + +def _env_flag(name: str, *, default: bool) -> bool: + raw = os.environ.get(name) + if raw is None: + return default + return raw.strip().lower() in {"1", "true", "yes", "on"} diff --git a/src/art/megatron/weights/adapter_export.py b/src/art/megatron/weights/adapter_export.py index 9f989f7de..cce081188 100644 --- a/src/art/megatron/weights/adapter_export.py +++ b/src/art/megatron/weights/adapter_export.py @@ -227,14 +227,14 @@ def add_gated_delta_net_adapter_weights( _zero_adapter_weight( base_prefix=base_prefix, adapter_key="adapter_b", - input_dim=int(in_proj.qkv_lora.A_T.shape[-1]), + input_dim=int(in_proj.qkv_lora.A_T.shape[-2]), output_dim=int(in_proj.num_value_heads_per_partition), like=in_proj.qkv_lora.B_T, ), _zero_adapter_weight( base_prefix=base_prefix, adapter_key="adapter_a", - input_dim=int(in_proj.qkv_lora.A_T.shape[-1]), + input_dim=int(in_proj.qkv_lora.A_T.shape[-2]), output_dim=int(in_proj.num_value_heads_per_partition), like=in_proj.qkv_lora.B_T, ), diff --git a/src/art/megatron/weights/lora_publish.py b/src/art/megatron/weights/lora_publish.py new file mode 100644 index 000000000..f4fd02a0a --- /dev/null +++ b/src/art/megatron/weights/lora_publish.py @@ -0,0 +1,774 @@ +from collections.abc import Iterable, Sequence +import re +from typing import Any, NamedTuple + +import torch + +from art.megatron.lora import LoRAPublishPlanner, LoraShardMeta +from art.megatron.model_support.lora_disk import save_vllm_lora_tensors +from art.megatron.model_support.spec import ExpertPackedLoraGroup, ExpertPackedLoraSlot +from art.megatron.training.model_chunks import ModelChunks + +_LAYER_BLOCK_RE = re.compile(r"^(?P.*\.layers\.\d+)\.") + + +class PackedExpertShardMeta(NamedTuple): + key: str + owner_rank: int + shape: tuple[int, ...] + dtype_name: str + manifest: dict[str, Any] + expert_start: int + expert_count: int + pack_layout: str + + @property + def numel(self) -> int: + total = 1 + for dim in self.shape: + total *= dim + return total + + +class _PinnedCpuStager: + def __init__(self) -> None: + self._events: list[torch.cuda.Event] = [] + self._stream = torch.cuda.Stream() if torch.cuda.is_available() else None + + def stage(self, tensor: torch.Tensor) -> torch.Tensor: + source = tensor.detach() + if self._stream is None or not source.is_cuda: + return source.cpu() + + source = source.contiguous() + target = torch.empty_like(source, device="cpu", pin_memory=True) + source_stream = torch.cuda.current_stream(source.device) + self._stream.wait_stream(source_stream) + with torch.cuda.stream(self._stream): + target.copy_(source, non_blocking=True) + source.record_stream(self._stream) + event = torch.cuda.Event() + event.record(self._stream) + self._events.append(event) + return target + + def finish(self) -> None: + for event in self._events: + event.synchronize() + self._events.clear() + + +def iter_lora_modules(model_chunks: ModelChunks) -> Iterable[Any]: + for chunk in model_chunks: + for module in chunk.modules(): + yield module + + +def _dtype_name(dtype: torch.dtype) -> str: + return str(dtype).removeprefix("torch.") + + +def _dtype_from_name(name: str) -> torch.dtype: + dtype = getattr(torch, name, None) + if not isinstance(dtype, torch.dtype): + raise RuntimeError(f"Unsupported LoRA tensor dtype={name!r}") + return dtype + + +def _block_for_key(key: str) -> str: + match = _LAYER_BLOCK_RE.match(key) + if match is not None: + return match.group("block") + return "__global__" + + +def _expert_prefix_projection(adapter_model_prefix: str) -> tuple[str, str] | None: + group_prefix, separator, projection = adapter_model_prefix.partition(".{expert}.") + if not separator: + return None + return group_prefix, projection + + +def _packed_expert_slot( + adapter_model_prefix: str, + suffix: str, + groups: Sequence[ExpertPackedLoraGroup], +) -> tuple[str, ExpertPackedLoraSlot] | None: + parts = _expert_prefix_projection(adapter_model_prefix) + if parts is None: + return None + group_prefix, projection = parts + lora_name = suffix.removesuffix(".weight") + for group in groups: + if not group_prefix.endswith(group.art_group_suffix): + continue + for slot in group.slots: + if slot.source_projection == projection and slot.source_lora == lora_name: + return group_prefix, slot + return None + + +def _uses_packed_expert_publish( + module: Any, + groups: Sequence[ExpertPackedLoraGroup], +) -> bool: + if int(getattr(module, "num_local_experts", 1)) <= 1: + return False + if not hasattr(module, "_lora_params"): + return False + adapter_model_prefix = getattr(module, "adapter_model_prefix", "") + if not isinstance(adapter_model_prefix, str): + return False + lora_suffixes = [ + suffix + for suffix, _param in module._lora_params() # type: ignore[attr-defined] + ] + return bool(lora_suffixes) and all( + _packed_expert_slot(adapter_model_prefix, suffix, groups) is not None + for suffix in lora_suffixes + ) + + +def collect_local_lora_entries( + model_chunks: ModelChunks, + adapter_model: dict[str, torch.Tensor], + *, + owner_rank: int, + packed_expert_groups: Sequence[ExpertPackedLoraGroup] = (), +) -> tuple[dict[str, torch.Tensor], list[LoraShardMeta]]: + local_tensors: dict[str, torch.Tensor] = {} + local_manifest: dict[str, dict[str, Any]] = {} + for module in iter_lora_modules(model_chunks): + if _uses_packed_expert_publish(module, packed_expert_groups): + continue + if hasattr(module, "sharded_lora_state_dict"): + module_state: dict[str, torch.Tensor] = module.sharded_lora_state_dict() # type: ignore[attr-defined] + for key, value in module_state.items(): + target_dtype = ( + adapter_model[key].dtype if key in adapter_model else value.dtype + ) + local_tensors[key] = value.to(target_dtype).contiguous() + if hasattr(module, "sharded_lora_manifest"): + local_manifest.update(module.sharded_lora_manifest()) # type: ignore[attr-defined] + + if set(local_tensors) != set(local_manifest): + raise RuntimeError( + "LoRA tensor/manifest mismatch: " + f"tensors={sorted(local_tensors)}, manifest={sorted(local_manifest)}" + ) + + metadata = [ + LoraShardMeta( + key=key, + owner_rank=owner_rank, + shape=tuple(int(dim) for dim in tensor.shape), + dtype_name=_dtype_name(tensor.dtype), + manifest=local_manifest[key], + block=_block_for_key(key), + ) + for key, tensor in local_tensors.items() + ] + return local_tensors, metadata + + +def _target_dtype_for_lora_param( + module: Any, + adapter_model: dict[str, torch.Tensor], + suffix: str, + fallback: torch.dtype, +) -> torch.dtype: + keys = module._expected_weight_keys(suffix.removesuffix(".weight")) # type: ignore[attr-defined] + return ( + adapter_model[keys[0]].dtype if keys and keys[0] in adapter_model else fallback + ) + + +def collect_local_packed_expert_entries( + model_chunks: ModelChunks, + adapter_model: dict[str, torch.Tensor], + *, + owner_rank: int, + packed_expert_groups: Sequence[ExpertPackedLoraGroup], +) -> tuple[dict[str, torch.Tensor], list[PackedExpertShardMeta]]: + local_tensors: dict[str, torch.Tensor] = {} + metadata: list[PackedExpertShardMeta] = [] + for module in iter_lora_modules(model_chunks): + if not _uses_packed_expert_publish(module, packed_expert_groups): + continue + adapter_model_prefix = module.adapter_model_prefix # type: ignore[attr-defined] + expert_start = int(module._expert_offset) # type: ignore[attr-defined] + expert_count = int(module.num_local_experts) # type: ignore[attr-defined] + for suffix, param in module._lora_params(): # type: ignore[attr-defined] + slot_match = _packed_expert_slot( + adapter_model_prefix, + suffix, + packed_expert_groups, + ) + if slot_match is None or not module._should_export_parameter(param): # type: ignore[attr-defined] + continue + group_prefix, slot = slot_match + key = f"{group_prefix}.{slot.output_suffix}" + tensor = param.data.transpose(1, 2).contiguous() + target_dtype = _target_dtype_for_lora_param( + module, + adapter_model, + suffix, + tensor.dtype, + ) + tensor = tensor.to(target_dtype).contiguous() + if key in local_tensors: + raise RuntimeError(f"Duplicate packed expert LoRA tensor: {key}") + local_tensors[key] = tensor + metadata.append( + PackedExpertShardMeta( + key=key, + owner_rank=owner_rank, + shape=tuple(int(dim) for dim in tensor.shape), + dtype_name=_dtype_name(tensor.dtype), + manifest=module._manifest_for_param(param), # type: ignore[attr-defined] + expert_start=expert_start, + expert_count=expert_count, + pack_layout=slot.pack_layout, + ) + ) + return local_tensors, metadata + + +def _global_packed_expert_metadata( + planner: LoRAPublishPlanner, + adapter_model: dict[str, torch.Tensor], + packed_expert_groups: Sequence[ExpertPackedLoraGroup], +) -> list[PackedExpertShardMeta]: + metadata: list[PackedExpertShardMeta] = [] + for template in planner.templates: + if int(template.num_local_experts) <= 1: + continue + slot_match = _packed_expert_slot( + template.adapter_model_prefix, + template.suffix, + packed_expert_groups, + ) + if slot_match is None: + continue + group_prefix, slot = slot_match + shard_ranks = range(template.shard_world_size) if template.sharded else (0,) + for ep_rank in range(planner._expert_model_world_size()): + expert_start = ep_rank * template.num_local_experts + expert_key = ( + f"{template.adapter_model_prefix.format(expert=expert_start)}." + f"{template.suffix}" + ) + for shard_rank in shard_ranks: + owner_rank = planner._expert_owner_rank(ep_rank, shard_rank) + per_expert_meta = planner._make_metadata( + template, + key=expert_key, + owner_rank=owner_rank, + shard_rank=shard_rank, + adapter_model=adapter_model, + ) + metadata.append( + PackedExpertShardMeta( + key=f"{group_prefix}.{slot.output_suffix}", + owner_rank=owner_rank, + shape=(template.num_local_experts, *per_expert_meta.shape), + dtype_name=per_expert_meta.dtype_name, + manifest=per_expert_meta.manifest, + expert_start=expert_start, + expert_count=template.num_local_experts, + pack_layout=slot.pack_layout, + ) + ) + return metadata + + +def _global_regular_metadata( + planner: LoRAPublishPlanner, + adapter_model: dict[str, torch.Tensor], + packed_expert_groups: Sequence[ExpertPackedLoraGroup], +) -> list[LoraShardMeta]: + if not packed_expert_groups: + return planner.global_metadata(adapter_model) + if _distributed_ready(): + from megatron.core import parallel_state as ps + + pp_world_size = ps.get_pipeline_model_parallel_world_size() + if pp_world_size != 1: + raise RuntimeError( + "LoRA publish planner requires pipeline_model_parallel_size=1; " + f"got {pp_world_size}. Rank-local modules cannot describe remote " + "pipeline stages without exchanging templates." + ) + metadata: list[LoraShardMeta] = [] + for template in planner.templates: + if ( + _packed_expert_slot( + template.adapter_model_prefix, + template.suffix, + packed_expert_groups, + ) + is not None + ): + continue + metadata.extend(planner._metadata_for_template(template, adapter_model)) + return metadata + + +def _merge_sharded_tensor( + key: str, + *, + ordered_shards: Sequence[torch.Tensor], + manifest: dict[str, Any], +) -> torch.Tensor: + strategy = manifest.get("export_shard_strategy") + assert strategy is not None + axis = int(manifest.get("export_shard_dim", 1 if "lora_A" in key else 0)) + if strategy == "componentwise": + component_sizes = [int(size) for size in manifest.get("component_sizes", [])] + world_size = int(manifest["shard_world_size"]) + if not component_sizes: + raise RuntimeError( + f"Missing component_sizes for key={key} shard strategy={strategy}" + ) + local_sizes = [] + for size in component_sizes: + if size % world_size != 0: + raise RuntimeError( + f"Component size {size} is not divisible by shard_world_size={world_size} for key={key}" + ) + local_sizes.append(size // world_size) + split_shards = [ + torch.split(shard, local_sizes, dim=axis) for shard in ordered_shards + ] + merged_components = [ + torch.cat([parts[index] for parts in split_shards], dim=axis) + for index in range(len(local_sizes)) + ] + return torch.cat(merged_components, dim=axis).contiguous() + if strategy != "uniform": + raise RuntimeError(f"Unsupported shard strategy={strategy} for key={key}") + return torch.cat(tuple(ordered_shards), dim=axis).contiguous() + + +def merge_sharded_adapter_entries( + entries_by_key: dict[str, list[tuple[dict[str, Any], torch.Tensor]]], +) -> dict[str, torch.Tensor]: + adapter_model: dict[str, torch.Tensor] = {} + for key, key_entries in entries_by_key.items(): + first_manifest = key_entries[0][0] + sharded = bool(first_manifest["sharded"]) + shard_world_size = int(first_manifest["shard_world_size"]) + for manifest_entry, _tensor in key_entries: + if bool(manifest_entry["sharded"]) != sharded: + raise RuntimeError(f"Inconsistent sharded flag for key={key}") + if int(manifest_entry["shard_world_size"]) != shard_world_size: + raise RuntimeError(f"Inconsistent shard world size for key={key}") + + if not sharded: + if len(key_entries) != 1: + raise RuntimeError( + f"Replicated key={key} expected 1 shard, got {len(key_entries)}" + ) + adapter_model[key] = key_entries[0][1] + continue + + shard_rank_to_tensor: dict[int, torch.Tensor] = {} + for manifest_entry, shard_tensor in key_entries: + shard_rank = int(manifest_entry["shard_rank"]) + if shard_rank in shard_rank_to_tensor: + raise RuntimeError(f"Duplicate shard_rank={shard_rank} for key={key}") + shard_rank_to_tensor[shard_rank] = shard_tensor + + expected_shard_ranks = set(range(shard_world_size)) + if set(shard_rank_to_tensor) != expected_shard_ranks: + raise RuntimeError( + f"Shard rank coverage mismatch for key={key}: " + f"expected {sorted(expected_shard_ranks)}, got {sorted(shard_rank_to_tensor)}" + ) + + ordered_shards = [ + shard_rank_to_tensor[shard_rank] for shard_rank in range(shard_world_size) + ] + adapter_model[key] = _merge_sharded_tensor( + key, + ordered_shards=ordered_shards, + manifest=first_manifest, + ) + return adapter_model + + +def _distributed_ready() -> bool: + is_initialized = getattr(torch.distributed, "is_initialized", None) + return ( + torch.distributed.is_available() + and callable(is_initialized) + and bool(is_initialized()) + ) + + +def _rank_and_device() -> tuple[int, torch.device]: + if _distributed_ready(): + rank = torch.distributed.get_rank() # type: ignore[possibly-missing-attribute] + else: + rank = 0 + if torch.cuda.is_available(): + return rank, torch.device("cuda", torch.cuda.current_device()) + return rank, torch.device("cpu") + + +def _metadata_by_owner_dtype( + metadata: Sequence[Any], +) -> dict[tuple[int, str], list[Any]]: + grouped: dict[tuple[int, str], list[Any]] = {} + for meta in metadata: + grouped.setdefault((meta.owner_rank, meta.dtype_name), []).append(meta) + return { + key: sorted(group, key=lambda meta: meta.key) + for key, group in sorted(grouped.items()) + } + + +def _pack_metadata_tensors( + metadata: Sequence[Any], + tensors: dict[str, torch.Tensor], +) -> torch.Tensor: + return torch.cat( + [tensors[meta.key].detach().contiguous().view(-1) for meta in metadata] + ) + + +def _views_from_flat( + *, + owner_rank: int, + metadata: Sequence[Any], + flat: torch.Tensor, +) -> dict[tuple[int, str], torch.Tensor]: + views: dict[tuple[int, str], torch.Tensor] = {} + offset = 0 + for meta in metadata: + views[(owner_rank, meta.key)] = flat.narrow(0, offset, meta.numel).view( + meta.shape + ) + offset += meta.numel + return views + + +def _exchange_batched_tensors( + metadata: Sequence[Any], + *, + local_tensors: dict[str, torch.Tensor], + rank: int, + device: torch.device, +) -> dict[tuple[int, str], torch.Tensor]: + if not _distributed_ready(): + return { + (rank, meta.key): local_tensors[meta.key].contiguous() for meta in metadata + } + + received: dict[tuple[int, str], torch.Tensor] = {} + for (owner_rank, dtype_name), group_metadata in _metadata_by_owner_dtype( + metadata + ).items(): + if rank == owner_rank: + flat = _pack_metadata_tensors(group_metadata, local_tensors) + if rank == 0: + received.update( + _views_from_flat( + owner_rank=owner_rank, + metadata=group_metadata, + flat=flat, + ) + ) + else: + torch.distributed.send(flat, dst=0) # type: ignore[possibly-missing-attribute] + elif rank == 0: + flat = torch.empty( + sum(meta.numel for meta in group_metadata), + dtype=_dtype_from_name(dtype_name), + device=device, + ) + torch.distributed.recv(flat, src=owner_rank) # type: ignore[possibly-missing-attribute] + received.update( + _views_from_flat( + owner_rank=owner_rank, + metadata=group_metadata, + flat=flat, + ) + ) + return received + + +def _entries_by_key( + metadata: list[LoraShardMeta], + tensors_by_owner_key: dict[tuple[int, str], torch.Tensor], +) -> dict[str, list[tuple[dict[str, Any], torch.Tensor]]]: + entries: dict[str, list[tuple[dict[str, Any], torch.Tensor]]] = {} + for meta in metadata: + entries.setdefault(meta.key, []).append( + (meta.manifest, tensors_by_owner_key[(meta.owner_rank, meta.key)]) + ) + return entries + + +def _merge_packed_expert_block( + key: str, + key_entries: list[tuple[dict[str, Any], torch.Tensor]], +) -> torch.Tensor: + first_manifest = key_entries[0][0] + sharded = bool(first_manifest["sharded"]) + shard_world_size = int(first_manifest["shard_world_size"]) + if not sharded: + if len(key_entries) != 1: + raise RuntimeError( + f"Replicated packed key={key} expected 1 shard, got {len(key_entries)}" + ) + return key_entries[0][1] + + shard_rank_to_tensor: dict[int, torch.Tensor] = {} + for manifest_entry, shard_tensor in key_entries: + if bool(manifest_entry["sharded"]) != sharded: + raise RuntimeError(f"Inconsistent sharded flag for packed key={key}") + if int(manifest_entry["shard_world_size"]) != shard_world_size: + raise RuntimeError(f"Inconsistent shard world size for packed key={key}") + shard_rank = int(manifest_entry["shard_rank"]) + if shard_rank in shard_rank_to_tensor: + raise RuntimeError( + f"Duplicate shard_rank={shard_rank} for packed key={key}" + ) + shard_rank_to_tensor[shard_rank] = shard_tensor + + expected_shard_ranks = set(range(shard_world_size)) + if set(shard_rank_to_tensor) != expected_shard_ranks: + raise RuntimeError( + f"Shard rank coverage mismatch for packed key={key}: " + f"expected {sorted(expected_shard_ranks)}, got {sorted(shard_rank_to_tensor)}" + ) + + manifest = dict(first_manifest) + manifest["export_shard_dim"] = int(manifest["export_shard_dim"]) + 1 + return _merge_sharded_tensor( + key, + ordered_shards=[ + shard_rank_to_tensor[shard_rank] for shard_rank in range(shard_world_size) + ], + manifest=manifest, + ) + + +def _pack_merged_expert_blocks( + key: str, + blocks: list[tuple[PackedExpertShardMeta, torch.Tensor]], +) -> torch.Tensor: + first_layout = blocks[0][0].pack_layout + next_expert = 0 + ordered_blocks: list[torch.Tensor] = [] + for meta, block in sorted(blocks, key=lambda item: item[0].expert_start): + if meta.pack_layout != first_layout: + raise RuntimeError(f"Inconsistent packed layout for key={key}") + if meta.expert_start != next_expert: + raise RuntimeError( + f"Packed expert coverage mismatch for key={key}: " + f"expected expert_start={next_expert}, got {meta.expert_start}" + ) + if int(block.shape[0]) != meta.expert_count: + raise RuntimeError( + f"Packed expert block shape mismatch for key={key}: " + f"shape={tuple(block.shape)} expert_count={meta.expert_count}" + ) + ordered_blocks.append(block) + next_expert += meta.expert_count + + joined = torch.cat(ordered_blocks, dim=0) + if first_layout == "expert_rows": + if joined.ndim != 3: + raise RuntimeError(f"{key}: expert_rows layout requires 3D blocks") + return joined.flatten(0, 1).contiguous() + if first_layout == "rank_major_expert_cols": + if joined.ndim != 3: + raise RuntimeError( + f"{key}: rank_major_expert_cols layout requires 3D blocks" + ) + return ( + joined.permute(1, 2, 0) + .reshape( + joined.shape[1], + joined.shape[2] * joined.shape[0], + ) + .contiguous() + ) + raise RuntimeError(f"Unsupported packed expert LoRA layout={first_layout!r}") + + +def merge_packed_expert_adapter_entries( + metadata: list[PackedExpertShardMeta], + tensors_by_owner_key: dict[tuple[int, str], torch.Tensor], +) -> dict[str, torch.Tensor]: + entries_by_key_start: dict[ + tuple[str, int], + list[tuple[PackedExpertShardMeta, dict[str, Any], torch.Tensor]], + ] = {} + for meta in metadata: + entries_by_key_start.setdefault((meta.key, meta.expert_start), []).append( + ( + meta, + meta.manifest, + tensors_by_owner_key[(meta.owner_rank, meta.key)], + ) + ) + + blocks_by_key: dict[str, list[tuple[PackedExpertShardMeta, torch.Tensor]]] = {} + for (key, _expert_start), entries in entries_by_key_start.items(): + representative = entries[0][0] + block = _merge_packed_expert_block( + key, + [(manifest, tensor) for _meta, manifest, tensor in entries], + ) + blocks_by_key.setdefault(key, []).append((representative, block)) + + return { + key: _pack_merged_expert_blocks(key, blocks) + for key, blocks in blocks_by_key.items() + } + + +def _stage_published_tensors( + tensors: dict[str, torch.Tensor], + stager: _PinnedCpuStager, +) -> dict[str, torch.Tensor]: + grouped: dict[tuple[str, int | None, str], list[tuple[str, torch.Tensor]]] = {} + for key, tensor in tensors.items(): + dtype_name = _dtype_name(tensor.dtype) + group_key = (tensor.device.type, tensor.device.index, dtype_name) + grouped.setdefault(group_key, []).append((key, tensor)) + + staged: dict[str, torch.Tensor] = {} + for _group_key, group in sorted(grouped.items()): + flat = torch.cat( + [tensor.detach().contiguous().view(-1) for _key, tensor in sorted(group)] + ) + staged_flat = stager.stage(flat) + offset = 0 + for key, tensor in sorted(group): + numel = tensor.numel() + if key in staged: + raise RuntimeError( + f"Duplicate vLLM LoRA tensor after conversion: {key}" + ) + staged[key] = staged_flat.narrow(0, offset, numel).view(tensor.shape) + offset += numel + return staged + + +def _save_rank0_vllm_lora( + *, + metadata: list[LoraShardMeta], + tensors_by_owner_key: dict[tuple[int, str], torch.Tensor], + packed_expert_metadata: list[PackedExpertShardMeta] | None = None, + packed_expert_tensors_by_owner_key: ( + dict[tuple[int, str], torch.Tensor] | None + ) = None, + handler: Any, + adapter_config: dict[str, Any], + output_dir: str, +) -> None: + merged_tensors = merge_sharded_adapter_entries( + _entries_by_key(metadata, tensors_by_owner_key) + ) + if packed_expert_metadata: + if packed_expert_tensors_by_owner_key is None: + raise RuntimeError("Missing packed expert tensors for LoRA publish") + packed_tensors = merge_packed_expert_adapter_entries( + packed_expert_metadata, + packed_expert_tensors_by_owner_key, + ) + for key, tensor in packed_tensors.items(): + if key in merged_tensors: + raise RuntimeError(f"Duplicate LoRA tensor after packed publish: {key}") + merged_tensors[key] = tensor + vllm_tensors, published_config = handler.to_vllm_lora_tensors( + merged_tensors, + adapter_config=dict(adapter_config), + ) + stager = _PinnedCpuStager() + published_tensors = _stage_published_tensors(vllm_tensors, stager) + stager.finish() + save_vllm_lora_tensors(output_dir, published_tensors, published_config) + + +def save_vllm_lora_from_model( + *, + model: ModelChunks, + adapter_model: dict[str, torch.Tensor], + handler: Any, + adapter_config: dict[str, Any], + output_dir: str, + rank: int, + world_size: int, +) -> None: + actual_rank, device = _rank_and_device() + if _distributed_ready(): + actual_world_size = torch.distributed.get_world_size() # type: ignore[possibly-missing-attribute] + if actual_rank != rank or actual_world_size != world_size: + raise RuntimeError( + "LoRA publisher rank/world-size mismatch: " + f"runtime=({rank}, {world_size}) distributed=({actual_rank}, {actual_world_size})" + ) + else: + if rank != 0 or world_size != 1: + raise RuntimeError( + "Non-distributed LoRA publish requires rank=0 and world_size=1, " + f"got rank={rank} world_size={world_size}" + ) + rank = 0 + packed_expert_groups = tuple(handler.expert_packed_lora_groups()) + planner = LoRAPublishPlanner(model) + local_tensors, local_metadata = collect_local_lora_entries( + model, + adapter_model, + owner_rank=rank, + packed_expert_groups=packed_expert_groups, + ) + local_packed_tensors, local_packed_metadata = collect_local_packed_expert_entries( + model, + adapter_model, + owner_rank=rank, + packed_expert_groups=packed_expert_groups, + ) + all_packed_metadata = ( + _global_packed_expert_metadata(planner, adapter_model, packed_expert_groups) + if rank == 0 + else local_packed_metadata + ) + if rank == 0: + all_metadata = _global_regular_metadata( + planner, + adapter_model, + packed_expert_groups if all_packed_metadata else (), + ) + else: + all_metadata = local_metadata + exchanged_tensors = _exchange_batched_tensors( + all_metadata, + local_tensors=local_tensors, + rank=rank, + device=device, + ) + exchanged_packed_tensors = _exchange_batched_tensors( + all_packed_metadata, + local_tensors=local_packed_tensors, + rank=rank, + device=device, + ) + + if rank != 0: + return + + _save_rank0_vllm_lora( + metadata=all_metadata, + tensors_by_owner_key=exchanged_tensors, + packed_expert_metadata=all_packed_metadata, + packed_expert_tensors_by_owner_key=exchanged_packed_tensors, + handler=handler, + adapter_config=adapter_config, + output_dir=output_dir, + ) diff --git a/src/art/megatron/weights/merge.py b/src/art/megatron/weights/merge.py deleted file mode 100644 index a4c359d2f..000000000 --- a/src/art/megatron/weights/merge.py +++ /dev/null @@ -1,208 +0,0 @@ -import importlib -import json -from pathlib import Path -from typing import Any - -import torch - -from art.megatron.model_support.lora_disk import ( - load_adapter_config, - load_lora_tensors_for_megatron, - normalize_lora_checkpoint_to_vllm, - resolve_lora_handler, - save_vllm_lora_tensors, -) -from art.megatron.model_support.spec import ModelSupportHandler - -safetensors = importlib.import_module("safetensors") -safetensors_torch = importlib.import_module("safetensors.torch") -safe_open = safetensors.safe_open -save_file = safetensors_torch.save_file - - -def _merge_sharded_tensor( - key: str, - *, - ordered_shards: list[torch.Tensor], - manifest: dict[str, Any], -) -> torch.Tensor: - strategy = manifest.get("export_shard_strategy") - assert strategy is not None - axis = int(manifest.get("export_shard_dim", 1 if "lora_A" in key else 0)) - if strategy == "componentwise": - component_sizes = [int(size) for size in manifest.get("component_sizes", [])] - world_size = int(manifest["shard_world_size"]) - if not component_sizes: - raise RuntimeError( - f"Missing component_sizes for key={key} shard strategy={strategy}" - ) - local_sizes = [] - for size in component_sizes: - if size % world_size != 0: - raise RuntimeError( - f"Component size {size} is not divisible by shard_world_size={world_size} for key={key}" - ) - local_sizes.append(size // world_size) - split_shards = [ - torch.split(shard, local_sizes, dim=axis) for shard in ordered_shards - ] - merged_components = [ - torch.cat([parts[index] for parts in split_shards], dim=axis) - for index in range(len(local_sizes)) - ] - return torch.cat(merged_components, dim=axis).contiguous() - if strategy != "uniform": - raise RuntimeError(f"Unsupported shard strategy={strategy} for key={key}") - return torch.cat(ordered_shards, dim=axis).contiguous() - - -def merge_sharded_adapter_entries( - entries_by_key: dict[str, list[tuple[dict[str, Any], torch.Tensor]]], -) -> dict[str, torch.Tensor]: - adapter_model: dict[str, torch.Tensor] = {} - for key, key_entries in entries_by_key.items(): - first_manifest = key_entries[0][0] - sharded = bool(first_manifest["sharded"]) - shard_world_size = int(first_manifest["shard_world_size"]) - for manifest_entry, _tensor in key_entries: - if bool(manifest_entry["sharded"]) != sharded: - raise RuntimeError(f"Inconsistent sharded flag for key={key}") - if int(manifest_entry["shard_world_size"]) != shard_world_size: - raise RuntimeError(f"Inconsistent shard world size for key={key}") - - if not sharded: - if len(key_entries) != 1: - raise RuntimeError( - f"Replicated key={key} expected 1 shard, got {len(key_entries)}" - ) - adapter_model[key] = key_entries[0][1] - continue - - shard_rank_to_tensor: dict[int, torch.Tensor] = {} - for manifest_entry, shard_tensor in key_entries: - shard_rank = int(manifest_entry["shard_rank"]) - if shard_rank in shard_rank_to_tensor: - raise RuntimeError(f"Duplicate shard_rank={shard_rank} for key={key}") - shard_rank_to_tensor[shard_rank] = shard_tensor - - expected_shard_ranks = set(range(shard_world_size)) - if set(shard_rank_to_tensor) != expected_shard_ranks: - raise RuntimeError( - f"Shard rank coverage mismatch for key={key}: " - f"expected {sorted(expected_shard_ranks)}, got {sorted(shard_rank_to_tensor)}" - ) - - ordered_shards = [ - shard_rank_to_tensor[shard_rank] for shard_rank in range(shard_world_size) - ] - adapter_model[key] = _merge_sharded_tensor( - key, - ordered_shards=ordered_shards, - manifest=first_manifest, - ) - return adapter_model - - -def _load_adapter_shards( - base_dir: Path, -) -> tuple[ - dict[str, torch.Tensor], - list[Path], - list[Path], -]: - shard_filenames = sorted(base_dir.glob("adapter_model-*-of-*.safetensors")) - if not shard_filenames: - raise FileNotFoundError(f"No adapter shards found in {base_dir}") - - shard_files_by_suffix = { - path.name.removeprefix("adapter_model-").removesuffix(".safetensors"): path - for path in shard_filenames - } - manifest_filenames = sorted(base_dir.glob("adapter_manifest-*-of-*.json")) - manifest_files_by_suffix = { - path.name.removeprefix("adapter_manifest-").removesuffix(".json"): path - for path in manifest_filenames - } - - if set(shard_files_by_suffix) != set(manifest_files_by_suffix): - raise RuntimeError( - "Shard/manifest coverage mismatch: " - f"shards={sorted(shard_files_by_suffix)}, " - f"manifests={sorted(manifest_files_by_suffix)}" - ) - - entries_by_key: dict[str, list[tuple[dict[str, Any], torch.Tensor]]] = {} - for suffix in sorted(shard_files_by_suffix): - shard_path = shard_files_by_suffix[suffix] - manifest_path = manifest_files_by_suffix[suffix] - with open(manifest_path, "r", encoding="utf-8") as manifest_file: - shard_manifest: dict[str, dict[str, Any]] = json.load(manifest_file) - with safe_open(shard_path, framework="pt") as file: - shard_tensors = {key: file.get_tensor(key) for key in file.keys()} - - if set(shard_tensors) != set(shard_manifest): - raise RuntimeError( - f"Tensor/manifest key mismatch for shard suffix={suffix}: " - f"tensor_keys={sorted(shard_tensors)}, " - f"manifest_keys={sorted(shard_manifest)}" - ) - for key, tensor in shard_tensors.items(): - entries_by_key.setdefault(key, []).append((shard_manifest[key], tensor)) - - adapter_model = merge_sharded_adapter_entries(entries_by_key) - return adapter_model, shard_filenames, manifest_filenames - - -def load_lora_adapter_state_dict( - lora_path: str, - *, - handler: ModelSupportHandler | None = None, - allow_unvalidated_arch: bool = False, -) -> dict[str, torch.Tensor]: - base_dir = Path(lora_path) - adapter_model_path = base_dir / "adapter_model.safetensors" - if adapter_model_path.exists(): - return load_lora_tensors_for_megatron( - lora_path, - handler=handler, - allow_unvalidated_arch=allow_unvalidated_arch, - ) - - adapter_model, _shard_filenames, _manifest_filenames = _load_adapter_shards( - base_dir - ) - return adapter_model - - -def merge_lora_adapter( - lora_path: str, - *, - output_dir: str | Path | None = None, - allow_unvalidated_arch: bool = False, -) -> None: - base_dir = Path(lora_path) - adapter_model, shard_filenames, manifest_filenames = _load_adapter_shards(base_dir) - target_dir = Path(output_dir) if output_dir is not None else base_dir - target_dir.mkdir(parents=True, exist_ok=True) - - if target_dir == base_dir: - save_file(adapter_model, base_dir / "adapter_model.safetensors") - normalize_lora_checkpoint_to_vllm( - base_dir, - allow_unvalidated_arch=allow_unvalidated_arch, - ) - else: - handler = resolve_lora_handler( - base_dir, - allow_unvalidated_arch=allow_unvalidated_arch, - ) - adapter_config = load_adapter_config(base_dir) - tensors, adapter_config = handler.to_vllm_lora_tensors( - adapter_model, - adapter_config=adapter_config, - ) - save_vllm_lora_tensors(target_dir, tensors, adapter_config) - for filename in shard_filenames: - filename.unlink() - for filename in manifest_filenames: - filename.unlink() diff --git a/src/art/pipeline_trainer/trainer.py b/src/art/pipeline_trainer/trainer.py index 721ddd791..d056ecb11 100644 --- a/src/art/pipeline_trainer/trainer.py +++ b/src/art/pipeline_trainer/trainer.py @@ -2,7 +2,7 @@ import asyncio from collections import Counter -from contextlib import asynccontextmanager +from contextlib import AsyncExitStack, asynccontextmanager from datetime import datetime, timezone import json import os @@ -92,6 +92,7 @@ def __init__( normalize_advantages: bool = True, adam_params: object | None = None, packed_sequence_length: int | None = None, + megatron_topology: art.MegatronTopologyConfig | None = None, max_steps: int | None = None, # Discard handling discard_queue_multiplier: int = 100, @@ -148,6 +149,7 @@ def __init__( self.normalize_advantages = normalize_advantages self.adam_params = adam_params self.packed_sequence_length = packed_sequence_length + self.megatron_topology = megatron_topology self.max_steps = max_steps self._status_log_interval_seconds = log_interval_seconds self.eval_every_n_steps = eval_every_n_steps @@ -162,6 +164,7 @@ def __init__( self._collapse_triggered = False self._checkpoint_lease_counts: Counter[int] = Counter() self._scheduled_eval_steps: set[int] = set() + self._scheduled_eval_leases: dict[int, AsyncExitStack] = {} self.state = PipelineState() self._scenario_lock = asyncio.Lock() @@ -218,9 +221,7 @@ async def train(self, *, handle_signals: bool = True) -> None: self._eval_queue = asyncio.Queue() if self.eval_fn is not None and self.eval_at_start: - self._scheduled_eval_steps.add(start_step) - await self._eval_queue.put(start_step) - self.state.last_eval_step = start_step + await self._schedule_eval_step(start_step) self._persist_state(start_step) self._status.start(initial_step=start_step) @@ -279,6 +280,7 @@ def _sync_signal_handler(signum: int, _frame: object | None) -> None: pass self._status.flush() self._status.close() + await self._release_all_scheduled_eval_leases() def request_stop(self) -> None: """Request a clean shutdown of the pipeline stages.""" @@ -375,29 +377,73 @@ async def _wait_for_policy(self) -> None: await self.state.policy_updated.wait() @asynccontextmanager - async def _adapter_lease(self, step: int) -> AsyncIterator[None]: + async def _checkpoint_lease(self, step: int) -> AsyncIterator[None]: self._checkpoint_lease_counts[step] += 1 + try: + yield + finally: + self._release_checkpoint_lease(step) + + @asynccontextmanager + async def _adapter_retention_lease(self, step: int) -> AsyncIterator[None]: + async with self._checkpoint_lease(step): + if not hasattr(type(self.backend), "adapter_retention_lease"): + yield + return + lease = getattr(self.backend, "adapter_retention_lease", None) + if lease is None: + yield + return + async with lease(self.model, step): + yield + + @asynccontextmanager + async def _adapter_lease(self, step: int) -> AsyncIterator[None]: if not hasattr(type(self.backend), "adapter_lease"): - try: + async with self._checkpoint_lease(step): yield - finally: - self._release_checkpoint_lease(step) return - try: + async with self._checkpoint_lease(step): lease = getattr(self.backend, "adapter_lease", None) if lease is None: yield return async with lease(self.model, step): yield - finally: - self._release_checkpoint_lease(step) def _release_checkpoint_lease(self, step: int) -> None: self._checkpoint_lease_counts[step] -= 1 if self._checkpoint_lease_counts[step] <= 0: del self._checkpoint_lease_counts[step] + async def _schedule_eval_step(self, step: int) -> None: + if self._eval_queue is None: + raise RuntimeError("eval queue is not initialized") + if step in self._scheduled_eval_steps: + return + stack = AsyncExitStack() + await stack.enter_async_context(self._adapter_retention_lease(step)) + try: + self._scheduled_eval_leases[step] = stack + self._scheduled_eval_steps.add(step) + await self._eval_queue.put(step) + self.state.last_eval_step = step + except Exception: + self._scheduled_eval_steps.discard(step) + self._scheduled_eval_leases.pop(step, None) + await stack.aclose() + raise + + async def _release_scheduled_eval_lease(self, step: int) -> None: + self._scheduled_eval_steps.discard(step) + stack = self._scheduled_eval_leases.pop(step, None) + if stack is not None: + await stack.aclose() + + async def _release_all_scheduled_eval_leases(self) -> None: + for step in tuple(self._scheduled_eval_leases): + await self._release_scheduled_eval_lease(step) + def _retained_adapter_steps(self, current_step: int) -> set[int]: min_step = max(0, current_step - self.max_steps_off_policy) return set(range(min_step, current_step + 1)) @@ -528,6 +574,8 @@ async def _training_stage(self) -> None: } if self.packed_sequence_length is not None: train_kwargs["packed_sequence_length"] = self.packed_sequence_length + if self.megatron_topology is not None: + train_kwargs["megatron_topology"] = self.megatron_topology result = await self.backend.train( self.model, batch, @@ -580,10 +628,7 @@ async def _training_stage(self) -> None: await self._log_zero_variance_groups(current_step) if self.eval_fn is not None and should_eval_step: - self._scheduled_eval_steps.add(current_step) - if self._eval_queue is not None: - await self._eval_queue.put(current_step) - self.state.last_eval_step = current_step + await self._schedule_eval_step(current_step) self._persist_state(current_step) finally: @@ -744,7 +789,7 @@ async def _run_eval(self, step: int) -> None: except Exception as exc: print(f"Eval failed at step {step}: {exc}") finally: - self._scheduled_eval_steps.discard(step) + await self._release_scheduled_eval_lease(step) if eval_completed: self.state.completed_eval_steps.add(step) self._persist_state(self.state.next_training_step) @@ -1021,11 +1066,7 @@ def _checkpoint_infos(self) -> list[CheckpointInfo]: return sorted(checkpoints, key=lambda checkpoint: checkpoint.step) def _protected_checkpoint_steps(self, current_step: int) -> set[int]: - return ( - {current_step} - | set(self._checkpoint_lease_counts) - | set(self._scheduled_eval_steps) - ) + return {current_step} | set(self._checkpoint_lease_counts) async def _run_checkpoint_retention(self, current_step: int) -> None: strategy = self.checkpoint_retention_strategy diff --git a/src/art/preprocessing/tokenize.py b/src/art/preprocessing/tokenize.py index 20783dc2b..051863bb0 100644 --- a/src/art/preprocessing/tokenize.py +++ b/src/art/preprocessing/tokenize.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Callable from dataclasses import dataclass, field from functools import cached_property @@ -5,14 +7,16 @@ import json import math import random -from typing import Any, Generator, Literal, cast +from typing import TYPE_CHECKING, Any, Generator, Literal, cast from openai.types.chat.chat_completion import Choice from PIL import Image import torch -from transformers.image_processing_utils import BaseImageProcessor from transformers.tokenization_utils_base import BatchEncoding, PreTrainedTokenizerBase +if TYPE_CHECKING: + from transformers.image_processing_utils import BaseImageProcessor + from ..trajectories import History, Trajectory, TrajectoryGroup, get_messages from ..types import MessagesAndChoices from ..utils.chat_template import ( diff --git a/src/art/test/test_kl_advantage.py b/src/art/test/test_kl_advantage.py index 21f575d3a..796ae69be 100644 --- a/src/art/test/test_kl_advantage.py +++ b/src/art/test/test_kl_advantage.py @@ -2,7 +2,7 @@ import torch -from art.loss import Loss, loss_fn +from art.loss import LossInputs, loss_fn def _make_inputs( @@ -40,9 +40,13 @@ def test_kl_advantage_no_effect_when_disabled(): ref_logprobs = torch.full((1, 8), -1.0) # different from new_logprobs loss_no_kl = loss_fn( - inputs, new_logprobs, ref_logprobs, None, {"kl_penalty_coef": 0.0} + LossInputs(inputs=inputs), + new_logprobs, + ref_logprobs, + None, + {"kl_penalty_coef": 0.0}, ) - loss_without_ref = loss_fn(inputs, new_logprobs, None, None, {}) + loss_without_ref = loss_fn(LossInputs(inputs=inputs), new_logprobs, None, None, {}) assert loss_no_kl.kl_policy_ref is None assert loss_without_ref.kl_policy_ref is None @@ -56,7 +60,13 @@ def test_kl_advantage_enabled(): new_logprobs = torch.zeros(1, 8) ref_logprobs = torch.full((1, 8), -0.5) - loss = loss_fn(inputs, new_logprobs, ref_logprobs, None, {"kl_penalty_coef": 0.1}) + loss = loss_fn( + LossInputs(inputs=inputs), + new_logprobs, + ref_logprobs, + None, + {"kl_penalty_coef": 0.1}, + ) assert loss.kl_policy_ref is not None assert loss.kl_policy_ref.item() > 0 # KL should be positive when logprobs differ @@ -103,7 +113,13 @@ def test_kl_advantage_direction(): new_logprobs[0, 5] = -0.1 ref_logprobs[0, 5] = -0.1 # no gap = low KL - loss = loss_fn(inputs, new_logprobs, ref_logprobs, None, {"kl_penalty_coef": 1.0}) + loss = loss_fn( + LossInputs(inputs=inputs), + new_logprobs, + ref_logprobs, + None, + {"kl_penalty_coef": 1.0}, + ) # The metric should exist assert loss.kl_policy_ref is not None @@ -114,5 +130,11 @@ def test_kl_advantage_does_not_affect_when_no_ref(): inputs = _make_inputs() new_logprobs = torch.zeros(1, 8) - loss = loss_fn(inputs, new_logprobs, None, None, {"kl_penalty_coef": 0.5}) + loss = loss_fn( + LossInputs(inputs=inputs), + new_logprobs, + None, + None, + {"kl_penalty_coef": 0.5}, + ) assert loss.kl_policy_ref is None diff --git a/src/art/tinker/service.py b/src/art/tinker/service.py index eff922d6b..30206890a 100644 --- a/src/art/tinker/service.py +++ b/src/art/tinker/service.py @@ -14,7 +14,7 @@ import yaml from .. import dev, types -from ..loss import loss_fn, shift_tensor +from ..loss import LossInputs, loss_fn, shift_tensor from ..preprocessing.inputs import TrainInputs, create_train_inputs from ..preprocessing.pack import ( DiskPackedTensors, @@ -100,7 +100,13 @@ def custom_loss_fn( ) for mask, lp in zip(masks, logprobs_list): logprobs[mask] = lp - loss = loss_fn(inputs, logprobs.unsqueeze(0), None, None, _config) + loss = loss_fn( + LossInputs(inputs=inputs), + logprobs.unsqueeze(0), + None, + None, + _config, + ) return loss.policy_loss, {"loss/train": loss.policy_loss.item()} shifted_tokens = shift_tensor(packed_tensors["tokens"], 0) diff --git a/src/art/transformers/patches.py b/src/art/transformers/patches.py index 97e09f6c8..8d0bb9ec7 100644 --- a/src/art/transformers/patches.py +++ b/src/art/transformers/patches.py @@ -35,3 +35,30 @@ def _patched_preprocess_mask_arguments( def patch_preprocess_mask_arguments() -> None: masking_utils._preprocess_mask_arguments = _patched_preprocess_mask_arguments # ty:ignore[invalid-assignment] + + +def disable_broken_torchvision_for_transformers() -> None: + try: + import torchvision # noqa: F401 + + return + except Exception: + import sys + + for module_name in list(sys.modules): + if module_name == "torchvision" or module_name.startswith("torchvision."): + sys.modules.pop(module_name, None) + + from transformers import utils as transformers_utils + from transformers.utils import import_utils + + def _torchvision_unavailable() -> bool: + return False + + for module in (import_utils, transformers_utils): + for name in ("is_torchvision_available", "is_torchvision_v2_available"): + original = getattr(module, name, None) + cache_clear = getattr(original, "cache_clear", None) + if callable(cache_clear): + cache_clear() + setattr(module, name, _torchvision_unavailable) diff --git a/src/art/types.py b/src/art/types.py index 389d513ff..db04390ad 100644 --- a/src/art/types.py +++ b/src/art/types.py @@ -14,15 +14,33 @@ Tools = list[ChatCompletionToolParam] +def _visible_device_count() -> int: + try: + import torch + except Exception: + return 1 + return max(int(torch.cuda.device_count()), 1) + + class TrainConfig(pydantic.BaseModel): learning_rate: float = 5e-6 kl_penalty_coef: float = 0.0 grad_accumulation_sequences: int | None = pydantic.Field(default=None, ge=1) +class MegatronTopologyConfig(pydantic.BaseModel): + tp: int = pydantic.Field(default=1, ge=1) + cp: int = pydantic.Field(default_factory=_visible_device_count, ge=1) + ep: int = pydantic.Field(default_factory=_visible_device_count, ge=1) + pp: int = pydantic.Field(default=1, ge=1) + vpp: int | None = pydantic.Field(default=None, ge=1) + etp: int = pydantic.Field(default=1, ge=1) + + class TrainSFTConfig(pydantic.BaseModel): learning_rate: float | list[float] = 5e-5 # Single value or per-batch list batch_size: int | Literal["auto"] = "auto" + megatron_topology: MegatronTopologyConfig | None = None Verbosity = Literal[0, 1, 2] diff --git a/src/art/unsloth/service.py b/src/art/unsloth/service.py index d719685c1..017316c80 100644 --- a/src/art/unsloth/service.py +++ b/src/art/unsloth/service.py @@ -6,7 +6,6 @@ import logging import os import socket -import subprocess from typing import Any, AsyncIterator, Literal, TypedDict, cast import torch @@ -23,16 +22,11 @@ from ..utils.lifecycle import ( ChildProcessSupervisor, ServiceLifecycle, - managed_process_cmd, - terminate_popen_process_group, ) from ..utils.output_dirs import get_step_checkpoint_dir from ..vllm_runtime import ( + ManagedVllmRuntime, VllmRuntimeLaunchConfig, - build_vllm_runtime_server_cmd, - get_vllm_runtime_nccl_so_path, - get_vllm_runtime_working_dir, - wait_for_vllm_runtime, ) from ..weight_transfer import ( DEFAULT_PACKED_BUFFER_SIZE_BYTES, @@ -142,14 +136,11 @@ class UnslothService: output_dir: str _is_sleeping: bool = False _latest_step: int = 0 - # Dedicated mode subprocess state - _vllm_process: subprocess.Popen | None = field(default=None, repr=False) # type: ignore[type-arg] - _vllm_log_file: Any = field(default=None, repr=False) - _vllm_log_path: str | None = None - _vllm_host: str = "127.0.0.1" - _vllm_port: int = 0 - _vllm_api_key: str | None = None - _vllm_nccl_so_path: str | None = None + _vllm_runtime: ManagedVllmRuntime = field( + default_factory=ManagedVllmRuntime, + init=False, + repr=False, + ) _weight_transfer_group: Any = field(default=None, init=False, repr=False) _lifecycle: ServiceLifecycle = field( default_factory=ServiceLifecycle, @@ -185,7 +176,27 @@ def rollout_weights_mode(self) -> Literal["lora", "merged"]: @property def _vllm_base_url(self) -> str: - return f"http://{self._vllm_host}:{self._vllm_port}" + return self._vllm_runtime.base_url + + @property + def _vllm_host(self) -> str: + return self._vllm_runtime.host + + @property + def _vllm_port(self) -> int: + return self._vllm_runtime.port + + @_vllm_port.setter + def _vllm_port(self, port: int) -> None: + self._vllm_runtime.port = port + + @property + def _vllm_api_key(self) -> str | None: + return self._vllm_runtime.api_key + + @property + def _vllm_nccl_so_path(self) -> str | None: + return self._vllm_runtime.nccl_so_path def _runtime_cuda_visible_devices(self) -> str: if self.is_dedicated: @@ -256,96 +267,31 @@ async def _start_vllm_subprocess( ) -> tuple[str, int]: self._raise_if_child_failed() server_args = self._runtime_server_args(config) - api_key = server_args.get("api_key") - self._vllm_api_key = api_key if isinstance(api_key, str) else None - self._vllm_nccl_so_path = ( - str(get_vllm_runtime_nccl_so_path()) - if self.rollout_weights_mode == "merged" - else None - ) - cmd = build_vllm_runtime_server_cmd( - VllmRuntimeLaunchConfig( + location = await self._vllm_runtime.start( + launch_config=VllmRuntimeLaunchConfig( base_model=self.base_model, port=port, - host=self._vllm_host, + host=self._vllm_runtime.host, cuda_visible_devices=self._runtime_cuda_visible_devices(), lora_path=lora_path, served_model_name=f"{self.model_name}@{self._latest_step}", rollout_weights_mode=self.rollout_weights_mode, engine_args=self._runtime_engine_args(config), server_args=server_args, - ) - ) - self._lifecycle.install_parent_cleanup(self.close) - - log_dir = os.path.join(self.output_dir, "logs") - os.makedirs(log_dir, exist_ok=True) - self._vllm_log_path = os.path.join(log_dir, "vllm-runtime.log") - self._vllm_log_file = open(self._vllm_log_path, "w", buffering=1) - - self._vllm_process = subprocess.Popen( - managed_process_cmd(cmd), - cwd=str(get_vllm_runtime_working_dir()), - env=os.environ.copy(), - stdout=self._vllm_log_file, - stderr=subprocess.STDOUT, - bufsize=1, - start_new_session=True, - ) - self._vllm_port = port - - import httpx - - timeout = float(os.environ.get("ART_DEDICATED_VLLM_TIMEOUT", 1200)) - async with httpx.AsyncClient() as client: - try: - await wait_for_vllm_runtime( - process=self._vllm_process, - host=self._vllm_host, - port=self._vllm_port, - timeout=timeout, - ) - except TimeoutError as exc: - self.close() - raise TimeoutError( - f"vLLM subprocess did not become ready within {timeout}s. " - f"Check logs at {log_dir}/vllm-runtime.log" - ) from exc - except RuntimeError as exc: - returncode = self._vllm_process.returncode - self.close() - raise RuntimeError( - f"vLLM subprocess exited with code {returncode}. " - f"Check logs at {log_dir}/vllm-runtime.log" - ) from exc - - try: - resp = await client.get( - f"http://{self._vllm_host}:{self._vllm_port}/v1/models", - **self._runtime_request_kwargs(), - timeout=5.0, - ) - resp.raise_for_status() - except httpx.HTTPError as exc: - self.close() - raise RuntimeError( - "vLLM passed /health but /v1/models was not reachable. " - f"Check logs at {log_dir}/vllm-runtime.log" - ) from exc - - assert self._vllm_process is not None - assert self._vllm_log_path is not None - self._child_processes.watch_popen( - "vLLM runtime", - self._vllm_process, - log_path=self._vllm_log_path, + ), + output_dir=self.output_dir, + child_processes=self._child_processes, + install_parent_cleanup=lambda: self._lifecycle.install_parent_cleanup( + self.close + ), + cleanup_on_error=self.close, ) logger.info( "vLLM runtime ready on port %d (GPUs: %s)", port, self._runtime_cuda_visible_devices(), ) - return self._vllm_host, self._vllm_port + return location async def _set_served_model_name(self, step: int) -> None: import httpx @@ -617,18 +563,18 @@ def close(self) -> None: """Terminate vLLM subprocess if running.""" if not self._lifecycle.begin_close(): return + weight_transfer_group = self._weight_transfer_group self._weight_transfer_group = None try: - self._child_processes.close() - if self._vllm_process is not None: - terminate_popen_process_group(self._vllm_process) - self._vllm_process = None - if self._vllm_log_file is not None: - self._vllm_log_file.close() - self._vllm_log_file = None - self._vllm_log_path = None - self._vllm_nccl_so_path = None - self._loaded_adapter_steps.clear() + try: + if weight_transfer_group is not None: + close = getattr(weight_transfer_group, "close", None) + if close is not None: + close() + finally: + self._child_processes.close() + self._vllm_runtime.close() + self._loaded_adapter_steps.clear() finally: self._lifecycle.restore_parent_cleanup() diff --git a/src/art/unsloth/train.py b/src/art/unsloth/train.py index 46d4e410f..3fe51823b 100644 --- a/src/art/unsloth/train.py +++ b/src/art/unsloth/train.py @@ -26,7 +26,7 @@ from trl import GRPOConfig, GRPOTrainer from .. import dev, types -from ..loss import loss_fn, shift_tensor +from ..loss import LossInputs, loss_fn, shift_tensor from ..preprocessing.inputs import TrainInputs, create_train_inputs from ..preprocessing.pack import ( DiskPackedTensors, @@ -479,7 +479,7 @@ def compute_loss( del attn_bias loss = loss_fn( - inputs, + LossInputs(inputs=inputs), new_logprobs, ref_logprobs, entropies, diff --git a/src/art/utils/sft.py b/src/art/utils/sft.py index 73db8cd28..6a7c6497b 100644 --- a/src/art/utils/sft.py +++ b/src/art/utils/sft.py @@ -10,7 +10,7 @@ from art.dev import TrainSFTConfig as DevTrainSFTConfig from art.model import TrainableModel from art.trajectories import Trajectory - from art.types import TrainSFTConfig + from art.types import MegatronTopologyConfig, TrainSFTConfig class SFTChunk(NamedTuple): @@ -349,6 +349,7 @@ async def train_sft_from_file( warmup_ratio: float = 0.1, initial_step: int = 0, final_step: int | None = None, + megatron_topology: "MegatronTopologyConfig | None" = None, _config: "DevTrainSFTConfig | None" = None, verbose: bool = False, shuffle_buffer_size: int = 10000, @@ -371,6 +372,7 @@ async def train_sft_from_file( initial_step: Starting step for resuming training. Default: 0 final_step: Ending step (exclusive). If None, trains to end of dataset. Useful for breaking training into segments with benchmarks in between. + megatron_topology: Parallel topology for Megatron SFT training. _config: Experimental configuration. Use at your own risk. verbose: Whether to print verbose output. Default: False shuffle_buffer_size: Size of shuffle buffer. Default: 10000. @@ -442,6 +444,7 @@ async def train_sft_from_file( config = TrainSFTConfig( learning_rate=learning_rates, batch_size=batch_size, + megatron_topology=megatron_topology, ) await model.train_sft( diff --git a/src/art/vllm_runtime.py b/src/art/vllm_runtime.py index 93a12a0a7..f6238135c 100644 --- a/src/art/vllm_runtime.py +++ b/src/art/vllm_runtime.py @@ -10,11 +10,17 @@ import shutil import subprocess import tempfile -from typing import Any, Literal +from typing import Any, Callable, Literal, TypedDict import httpx from pydantic import BaseModel, ConfigDict, Field +from .utils.lifecycle import ( + ChildProcessSupervisor, + managed_process_cmd, + terminate_popen_process_group, +) + RUNTIME_SERVER = "art-vllm-runtime-server" RUNTIME_PACKAGE = "art-vllm-runtime" RUNTIME_PROTOCOL_VERSION = 1 @@ -64,6 +70,139 @@ class VllmRuntimeInstallMarker(BaseModel): cache_root: str +class VllmRuntimeRequestKwargs(TypedDict, total=False): + headers: dict[str, str] + + +class ManagedVllmRuntime: + def __init__(self, *, host: str = "127.0.0.1") -> None: + self.host = host + self.port = 0 + self.api_key: str | None = None + self.nccl_so_path: str | None = None + self.process: subprocess.Popen[Any] | None = None + self.log_file: Any = None + self.log_path: str | None = None + + @property + def base_url(self) -> str: + return f"http://{self.host}:{self.port}" + + def request_kwargs(self) -> VllmRuntimeRequestKwargs: + if self.api_key is None: + return {} + return {"headers": {"Authorization": f"Bearer {self.api_key}"}} + + async def start( + self, + *, + launch_config: VllmRuntimeLaunchConfig, + output_dir: str, + child_processes: ChildProcessSupervisor, + install_parent_cleanup: Callable[[], None], + cleanup_on_error: Callable[[], None] | None = None, + timeout: float | None = None, + ) -> tuple[str, int]: + self.host = launch_config.host + self.port = launch_config.port + api_key = launch_config.server_args.get("api_key") + self.api_key = api_key if isinstance(api_key, str) else None + self.nccl_so_path = ( + str(get_vllm_runtime_nccl_so_path()) + if launch_config.rollout_weights_mode == "merged" + else None + ) + + cmd = build_vllm_runtime_server_cmd(launch_config) + install_parent_cleanup() + log_dir = os.path.join(output_dir, "logs") + os.makedirs(log_dir, exist_ok=True) + self.log_path = os.path.join(log_dir, "vllm-runtime.log") + self.log_file = open(self.log_path, "w", buffering=1) + self.process = subprocess.Popen( + managed_process_cmd(cmd), + cwd=str(get_vllm_runtime_working_dir()), + env=os.environ.copy(), + stdout=self.log_file, + stderr=subprocess.STDOUT, + bufsize=1, + start_new_session=True, + ) + + runtime_timeout = ( + timeout + if timeout is not None + else float(os.environ.get("ART_DEDICATED_VLLM_TIMEOUT", 1200)) + ) + async with httpx.AsyncClient() as client: + try: + await wait_for_vllm_runtime( + process=self.process, + host=self.host, + port=self.port, + timeout=runtime_timeout, + ) + except TimeoutError as exc: + log_path = self.log_path + self._cleanup_after_start_error(cleanup_on_error) + raise TimeoutError( + "vLLM subprocess did not become ready within " + f"{runtime_timeout}s. Check logs at {log_path}" + ) from exc + except RuntimeError as exc: + returncode = self.process.returncode + log_path = self.log_path + self._cleanup_after_start_error(cleanup_on_error) + raise RuntimeError( + f"vLLM subprocess exited with code {returncode}. " + f"Check logs at {log_path}" + ) from exc + + try: + response = await client.get( + f"{self.base_url}/v1/models", + **self.request_kwargs(), + timeout=5.0, + ) + response.raise_for_status() + except httpx.HTTPError as exc: + log_path = self.log_path + self._cleanup_after_start_error(cleanup_on_error) + raise RuntimeError( + "vLLM passed /health but /v1/models was not reachable. " + f"Check logs at {log_path}" + ) from exc + + assert self.process is not None + assert self.log_path is not None + child_processes.watch_popen( + "vLLM runtime", + self.process, + log_path=self.log_path, + ) + return self.host, self.port + + def close(self) -> None: + if self.process is not None: + terminate_popen_process_group(self.process) + self.process = None + if self.log_file is not None: + self.log_file.close() + self.log_file = None + self.log_path = None + self.api_key = None + self.nccl_so_path = None + self.port = 0 + + def _cleanup_after_start_error( + self, cleanup_on_error: Callable[[], None] | None + ) -> None: + if cleanup_on_error is None: + self.close() + else: + cleanup_on_error() + + def get_vllm_runtime_project_root() -> Path: override = os.environ.get("ART_VLLM_RUNTIME_PROJECT_ROOT") if override: diff --git a/src/art/weight_transfer/nccl.py b/src/art/weight_transfer/nccl.py index 64e37ce23..cecd56731 100644 --- a/src/art/weight_transfer/nccl.py +++ b/src/art/weight_transfer/nccl.py @@ -5,6 +5,7 @@ import ctypes from datetime import timedelta import importlib.util +import os from pathlib import Path import pickle import socket @@ -90,6 +91,8 @@ def __init__(self, so_file: str | None = None): _nccl_result_t, [ctypes.POINTER(_nccl_comm_t), ctypes.c_int, _NcclUniqueId, ctypes.c_int], ) + self._configure("ncclCommDestroy", _nccl_result_t, [_nccl_comm_t]) + self._configure("ncclCommAbort", _nccl_result_t, [_nccl_comm_t]) self._configure( "ncclAllReduce", _nccl_result_t, @@ -139,6 +142,12 @@ def init_rank(self, world_size: int, unique_id: _NcclUniqueId, rank: int) -> Any ) return comm + def destroy_comm(self, comm: Any) -> None: + self._check(self._lib.ncclCommDestroy(comm)) + + def abort_comm(self, comm: Any) -> None: + self._check(self._lib.ncclCommAbort(comm)) + def all_reduce( self, tensor: torch.Tensor, @@ -236,6 +245,20 @@ def broadcast_obj(self, obj: Any | None, *, src: int) -> Any: self._broadcast_recv_counter[src] += 1 return received + def close(self) -> None: + if self.socket is not None: + self.socket.close() + self.socket = None + + +def _canonical_cuda_device(device: int | torch.device) -> torch.device: + cuda_device = torch.device(f"cuda:{device}") if isinstance(device, int) else device + if cuda_device.type != "cuda": + raise RuntimeError(f"NCCL weight transfer requires a CUDA device, got {device}") + if cuda_device.index is None: + return torch.device("cuda", torch.cuda.current_device()) + return cuda_device + class TrainerNcclCommunicator: def __init__( @@ -248,6 +271,7 @@ def __init__( device: int | torch.device, nccl_so_path: str | None = None, ) -> None: + self.device = _canonical_cuda_device(device) bootstrap_group = _BootstrapGroup( host=host, port=port, @@ -257,9 +281,6 @@ def __init__( self._bootstrap_group = bootstrap_group self.rank = rank self.world_size = world_size - self.device = ( - torch.device(f"cuda:{device}") if isinstance(device, int) else device - ) self._nccl = _NcclLibrary(nccl_so_path) unique_id_bytes = ( _nccl_unique_id_to_bytes(self._nccl.get_unique_id()) if rank == 0 else None @@ -274,16 +295,54 @@ def __init__( self.all_reduce(warmup, stream=stream) stream.synchronize() + def _require_comm(self) -> Any: + if self._comm is None: + raise RuntimeError("NCCL weight transfer communicator is closed") + return self._comm + + def _validate_collective_tensor(self, tensor: torch.Tensor) -> None: + if not tensor.is_cuda: + raise RuntimeError( + f"NCCL weight transfer requires a CUDA tensor, got {tensor.device}" + ) + if tensor.device != self.device: + raise RuntimeError( + "NCCL weight transfer tensor device mismatch: " + f"expected {self.device}, got {tensor.device}" + ) + if not tensor.is_contiguous(): + raise RuntimeError("NCCL weight transfer requires contiguous tensors") + + def close(self) -> None: + comm = self._comm + if comm is None: + return + self._comm = None + try: + self._nccl.destroy_comm(comm) + finally: + self._bootstrap_group.close() + + def abort(self) -> None: + comm = self._comm + if comm is None: + return + self._comm = None + try: + self._nccl.abort_comm(comm) + finally: + self._bootstrap_group.close() + def all_reduce( self, tensor: torch.Tensor, *, stream: torch.cuda.Stream | None = None, ) -> None: - assert tensor.device == self.device + self._validate_collective_tensor(tensor) self._nccl.all_reduce( tensor, - self._comm, + self._require_comm(), stream=stream or torch.cuda.current_stream(self.device), ) @@ -294,10 +353,10 @@ def broadcast( src: int, stream: torch.cuda.Stream | None = None, ) -> None: - assert tensor.device == self.device + self._validate_collective_tensor(tensor) self._nccl.broadcast( tensor, - self._comm, + self._require_comm(), rank=self.rank, src=src, stream=stream or torch.cuda.current_stream(self.device), @@ -305,6 +364,8 @@ def broadcast( def _find_nccl_library() -> str: + if override := os.environ.get("VLLM_NCCL_SO_PATH"): + return override if torch.version.cuda is not None: spec = importlib.util.find_spec("nvidia.nccl") if spec is None or spec.submodule_search_locations is None: diff --git a/src/art/weight_transfer/packed_tensor.py b/src/art/weight_transfer/packed_tensor.py index 100bb5008..c8bc41f8f 100644 --- a/src/art/weight_transfer/packed_tensor.py +++ b/src/art/weight_transfer/packed_tensor.py @@ -20,6 +20,14 @@ def packed_broadcast_producer( buffer_size_bytes: int = DEFAULT_PACKED_BUFFER_SIZE_BYTES, num_buffers: int = DEFAULT_PACKED_NUM_BUFFERS, ) -> None: + """Pack and broadcast tensors on side streams with stable ring buffers. + + The caller owns producer-side ordering: source tensors must already be on the + active CUDA device, must not be mutated while this function may read them, + and any prior writer streams must be ordered before entry. Each ring-buffer + slot is synchronized before reuse, and the function returns only after every + side-stream broadcast has completed. + """ target_packed_tensor_size = buffer_size_bytes streams = [torch.cuda.Stream() for _ in range(num_buffers)] buffer_idx = 0 @@ -70,6 +78,14 @@ def packed_broadcast_consumer( buffer_size_bytes: int = DEFAULT_PACKED_BUFFER_SIZE_BYTES, num_buffers: int = DEFAULT_PACKED_NUM_BUFFERS, ) -> None: + """Receive packed tensors on side streams and unpack views for a callback. + + The tensors passed to ``post_unpack_func`` are backed by the current packed + receive buffer. The callback must copy into durable storage before returning + if it needs to keep them, and it must add its own stream waits or lifetime + recording if it launches consumers outside the active side stream. + """ + def unpack_tensor( packed_tensor: torch.Tensor, names: list[str], diff --git a/tests/integration/megatron/artifacts.py b/tests/integration/megatron/artifacts.py new file mode 100644 index 000000000..86b2557ab --- /dev/null +++ b/tests/integration/megatron/artifacts.py @@ -0,0 +1,183 @@ +"""Shared helpers for integration tests that need durable per-run artifacts. + +These helpers create a suite-owned artifacts/ directory keyed by test node id, +git commit, and run id, then write metadata that ties logs and JSON outputs back +to the exact committed code. They do not replace repo .local logs used by oracle +workflows that intentionally keep mutable local development output. +""" + +from __future__ import annotations + +from datetime import datetime, timezone +import os +from pathlib import Path +import re +import subprocess +import sys +from typing import Any +import uuid + +from pydantic import BaseModel + +REPO_ROOT = Path(__file__).resolve().parents[3] +ARTIFACTS_ROOT = Path(__file__).resolve().parent / "artifacts" +SUITE_NAME = "Megatron integration tests" +LONGREP_MAX_CHARS = 12000 + + +class GitRepoState(BaseModel): + path: str + commit: str + dirty: bool + status: tuple[str, ...] = () + + +class ArtifactMetadata(BaseModel): + commit: str + branch: str + test_nodeid: str + created_at_utc: str + python_executable: str + artifact_dir: str + + +class PytestPhaseResult(BaseModel): + when: str + outcome: str + duration: float + location: tuple[str, int | None, str] | None = None + longrepr: str | None = None + + +class PytestResult(BaseModel): + commit: str + branch: str + test_nodeid: str + created_at_utc: str + phases: list[PytestPhaseResult] + + +def _git(*args: str) -> str: + return subprocess.run( + ["git", *args], + cwd=REPO_ROOT, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def _sanitize_nodeid(nodeid: str) -> str: + collapsed = re.sub(r"[^A-Za-z0-9_.-]+", "_", nodeid.strip()) + return collapsed.strip("._") or "unnamed_test" + + +def _short_text(value: object | None) -> str | None: + if value is None: + return None + text = str(value) + if len(text) <= LONGREP_MAX_CHARS: + return text + return text[-LONGREP_MAX_CHARS:] + + +def require_clean_git_state(suite_name: str) -> str: + """Return the current commit after checking artifacts can be tied to clean code.""" + dirty = _git("status", "--porcelain=v1", "--untracked-files=all").splitlines() + if dirty: + rendered = "\n".join(dirty) + raise RuntimeError( + f"{suite_name} require a fully committed worktree.\n" + "Commit or remove these changes before running tests:\n" + f"{rendered}" + ) + return _git("rev-parse", "HEAD") + + +def git_state(path: Path) -> GitRepoState: + status = tuple( + line + for line in _git("-C", str(path), "status", "--porcelain=v1").splitlines() + if line + ) + return GitRepoState( + path=str(path), + commit=_git("-C", str(path), "rev-parse", "HEAD"), + dirty=bool(status), + status=status, + ) + + +def pinned_git_state(suite_name: str) -> GitRepoState: + require_clean_git_state(suite_name) + return git_state(REPO_ROOT) + + +def create_artifact_dir( + test_nodeid: str, + *, + artifacts_root: Path, + suite_name: str, +) -> Path: + """Create a durable, git-addressed artifact directory for one test invocation.""" + commit = require_clean_git_state(suite_name) + branch = _git("branch", "--show-current") + timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + run_id = f"{timestamp}_{os.getpid()}_{uuid.uuid4().hex[:8]}" + artifact_dir = artifacts_root / _sanitize_nodeid(test_nodeid) / commit[:12] / run_id + artifact_dir.mkdir(parents=True, exist_ok=False) + + metadata = ArtifactMetadata( + commit=commit, + branch=branch, + test_nodeid=test_nodeid, + created_at_utc=datetime.now(timezone.utc).isoformat(), + python_executable=sys.executable, + artifact_dir=str(artifact_dir), + ) + (artifact_dir / "run_metadata.json").write_text( + metadata.model_dump_json(indent=2) + "\n", + encoding="utf-8", + ) + return artifact_dir + + +def create_megatron_artifact_dir(test_nodeid: str) -> Path: + return create_artifact_dir( + test_nodeid, + artifacts_root=ARTIFACTS_ROOT, + suite_name=SUITE_NAME, + ) + + +def write_pytest_result( + artifact_dir: Path, + *, + test_nodeid: str, + reports: list[Any], +) -> Path: + result = PytestResult( + commit=_git("rev-parse", "HEAD"), + branch=_git("branch", "--show-current"), + test_nodeid=test_nodeid, + created_at_utc=datetime.now(timezone.utc).isoformat(), + phases=[ + PytestPhaseResult( + when=str(report.when), + outcome=str(report.outcome), + duration=float(report.duration), + location=( + str(report.location[0]), + int(report.location[1]) if report.location[1] is not None else None, + str(report.location[2]), + ) + if getattr(report, "location", None) is not None + else None, + longrepr=_short_text(getattr(report, "longrepr", None)), + ) + for report in reports + ], + ) + path = artifact_dir / "pytest_result.json" + path.write_text(result.model_dump_json(indent=2) + "\n", encoding="utf-8") + return path diff --git a/tests/integration/megatron/artifacts/.gitignore b/tests/integration/megatron/artifacts/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/tests/integration/megatron/artifacts/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/integration/megatron/conftest.py b/tests/integration/megatron/conftest.py new file mode 100644 index 000000000..2ba2def03 --- /dev/null +++ b/tests/integration/megatron/conftest.py @@ -0,0 +1,43 @@ +from pathlib import Path +from typing import Any + +import pytest + +import art # noqa: F401 + +from .artifacts import create_megatron_artifact_dir, write_pytest_result + +_ARTIFACT_DIR_ATTR = "_megatron_integration_artifact_dir" +_REPORTS_ATTR = "_megatron_integration_reports" + + +def _artifact_dir_for_item(item: pytest.Item) -> Path: + artifact_dir = getattr(item, _ARTIFACT_DIR_ATTR, None) + if artifact_dir is None: + artifact_dir = create_megatron_artifact_dir(item.nodeid) + setattr(item, _ARTIFACT_DIR_ATTR, artifact_dir) + return artifact_dir + + +def pytest_runtest_setup(item: pytest.Item) -> None: + _artifact_dir_for_item(item) + + +@pytest.fixture +def artifact_dir(request: pytest.FixtureRequest) -> Path: + return _artifact_dir_for_item(request.node) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[Any]): + del call + outcome = yield + report = outcome.get_result() + reports = list(getattr(item, _REPORTS_ATTR, [])) + reports.append(report) + setattr(item, _REPORTS_ATTR, reports) + write_pytest_result( + _artifact_dir_for_item(item), + test_nodeid=item.nodeid, + reports=reports, + ) diff --git a/tests/integration/megatron/cp_attn/__init__.py b/tests/integration/megatron/cp_attn/__init__.py new file mode 100644 index 000000000..ce1370e99 --- /dev/null +++ b/tests/integration/megatron/cp_attn/__init__.py @@ -0,0 +1 @@ +"""Context-parallel attention integration tests.""" diff --git a/tests/integration/megatron/cp_attn/megatron_attention_oracle_harness.py b/tests/integration/megatron/cp_attn/megatron_attention_oracle_harness.py new file mode 100644 index 000000000..05cbfd9ec --- /dev/null +++ b/tests/integration/megatron/cp_attn/megatron_attention_oracle_harness.py @@ -0,0 +1,259 @@ +from __future__ import annotations + +import os +from pathlib import Path + +from ..model_support.oracle_harness import ( + FlexBackend, + LoraConfig, + MetricThresholdRule, + OracleCaseConfig, + PackedTensorConfig, + PhasePassFn, + SensitivityMutation, + StepTrace, + StreamingWeightOffloadConfig, + Topology, + VariantReport, + VariantRunner, + VariantSpec, + WorkerRunRequest, +) +from .megatron_attention_oracle_worker import ( + run_worker_subprocess as run_attention_worker_subprocess, +) + +ATTN_SENSITIVITY_MUTATION_ENV = "ART_ATTN_SENSITIVITY_MUTATIONS" +ATTN_TOPOLOGY_INDICES_ENV = "ART_ATTN_TOPOLOGY_INDICES" +# Testing design: the full model oracle remains fp32, while this suite +# intentionally validates the production bf16 FLASH CP-attention path against a +# Triton reference backend. Do not change this backend/dtype split or loosen the +# gate without discussing the oracle coverage tradeoff. +ATTN_BF16_MEAN_ABS_PCT_THRESHOLD = 2.0 + +ATTN_SENSITIVITY_MUTATIONS = ( + "attn_kv_fetch_pack_on_comm_stream", + "attn_skip_nested_grad_sanitize", + "attn_skip_flash_lse_normalize", +) + +ATTN_TOPOLOGIES = [ + Topology(tp=1, ep=1, etp=1, dp=1, cp=2, sp=False), + Topology(tp=2, ep=1, etp=1, dp=1, cp=2, sp=True), + Topology(tp=1, ep=1, etp=1, dp=1, cp=4, sp=False), + Topology(tp=2, ep=1, etp=1, dp=1, cp=4, sp=True), + Topology(tp=1, ep=1, etp=1, dp=1, cp=8, sp=False), +] + +ATTN_SENSITIVITY_TOPOLOGY_BY_MUTATION = { + "attn_kv_fetch_pack_on_comm_stream": Topology( + tp=2, ep=1, etp=1, dp=1, cp=2, sp=True + ), + "attn_skip_nested_grad_sanitize": Topology(tp=1, ep=1, etp=1, dp=1, cp=2, sp=False), + "attn_skip_flash_lse_normalize": Topology(tp=1, ep=1, etp=1, dp=1, cp=4, sp=False), +} + + +def attention_case_config( + base_model: str = "Qwen/Qwen3.5-35B-A3B", +) -> OracleCaseConfig: + return OracleCaseConfig( + base_model=base_model, + precision="bf16", + num_layers=1, + packed_tensors=PackedTensorConfig( + num_sequences=4, + sequence_length=1024, + prefill_tokens=256, + completion_branches_per_prefix=2, + decode_tokens=128, + decode_tokens_jitter=32, + packing_mode="stop_early", + vocab_high=8192, + ), + lora=LoraConfig( + rank=1, + alpha=32, + target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], + ), + ) + + +def attention_sensitivity_mutations() -> list[str]: + raw = os.environ.get(ATTN_SENSITIVITY_MUTATION_ENV) + if raw is None or raw.strip() == "": + return [] + normalized = raw.strip().lower() + if normalized == "all": + return list(ATTN_SENSITIVITY_MUTATIONS) + mutations = [item.strip().lower() for item in raw.split(",") if item.strip()] + unsupported = [ + mutation for mutation in mutations if mutation not in ATTN_SENSITIVITY_MUTATIONS + ] + if unsupported: + supported = ", ".join(ATTN_SENSITIVITY_MUTATIONS) + raise ValueError( + f"Unsupported {ATTN_SENSITIVITY_MUTATION_ENV} value '{raw}'. " + f"Supported values: {supported}, CSV of supported values, or all." + ) + return mutations + + +def attention_sensitivity_enabled() -> bool: + return bool(attention_sensitivity_mutations()) + + +def attention_required_world_size(mutations: list[str]) -> int: + return max( + ATTN_SENSITIVITY_TOPOLOGY_BY_MUTATION[mutation].world_size() + for mutation in mutations + ) + + +def _selected_attention_topologies() -> list[tuple[int, Topology]]: + raw = os.environ.get(ATTN_TOPOLOGY_INDICES_ENV, "all") + normalized = raw.strip().lower() + if normalized in {"", "all"}: + return list(enumerate(ATTN_TOPOLOGIES)) + selected: list[int] = [] + seen: set[int] = set() + for item in raw.split(","): + stripped = item.strip() + if not stripped: + continue + index = int(stripped) + if index in seen: + continue + selected.append(index) + seen.add(index) + invalid = [index for index in selected if index not in range(len(ATTN_TOPOLOGIES))] + if invalid: + available = ", ".join( + f"{index}:{topology.slug()}" + for index, topology in enumerate(ATTN_TOPOLOGIES) + ) + raise ValueError( + f"Unsupported {ATTN_TOPOLOGY_INDICES_ENV} indices {invalid}. " + f"Available topology candidates: {available}" + ) + return [(index, ATTN_TOPOLOGIES[index]) for index in selected] + + +def _attention_phase_pass_fns() -> dict[str, PhasePassFn]: + metric_rule = MetricThresholdRule( + limits={"mean_abs_pct": ATTN_BF16_MEAN_ABS_PCT_THRESHOLD} + ) + return { + "forward": metric_rule, + "outputs": metric_rule, + "losses": metric_rule, + "grads": metric_rule, + "deltas": metric_rule, + } + + +class AttentionVariantRunner(VariantRunner): + """Runs the attention-only oracle with its dedicated worker and no routing replay.""" + + def _run_topology( + self, + *, + topology: Topology, + output_slug: str, + mutation: SensitivityMutation | None, + replay_bundle_dir: Path | None, + capture_bundle_dir: Path | None, + regenerate: bool, + flex_backend: FlexBackend | None = None, + offload_between_jobs: bool = True, + streaming_weight_offload: StreamingWeightOffloadConfig | None = None, + ) -> Path: + del replay_bundle_dir, capture_bundle_dir + topology_dir = self.case_dir / output_slug + manifest_path = topology_dir / "manifest.json" + from ..model_support.oracle_harness import ( + REPO_ROOT, + _manifest_matches_current_commit, + _replace_topology_dir, + ) + + if ( + manifest_path.exists() + and not regenerate + and _manifest_matches_current_commit(manifest_path) + ): + return topology_dir + + _replace_topology_dir(topology_dir) + request = WorkerRunRequest( + git=self.git, + case_id=self.case_id, + objective=self.objective, + case_config=self.case_config, + topology=topology, + topology_dir=str(topology_dir), + packed_tensors=self.case_artifacts.packed_tensors, + shared_init_adapter_path=str(self.shared_init_path), + mutation=mutation, + moe_routing_replay_path=None, + moe_routing_replay_strict=True, + capture_moe_routing_bundle_path=None, + flex_backend=flex_backend, + offload_between_jobs=offload_between_jobs, + streaming_weight_offload=( + streaming_weight_offload or StreamingWeightOffloadConfig() + ), + ) + run_attention_worker_subprocess(request, topology_dir, repo_root=REPO_ROOT) + return topology_dir + + +def run_attention_suite( + *, + case_config: OracleCaseConfig, + max_world_size: int | None = None, +) -> list[VariantReport]: + phase_pass = _attention_phase_pass_fns() + variants: list[VariantSpec] = [] + for _, topology in _selected_attention_topologies(): + if max_world_size is not None and topology.world_size() > max_world_size: + continue + variants.append( + VariantSpec( + name=f"attention_{topology.slug()}", + topology=topology, + output_slug=f"{topology.slug()}__flash_attention", + pass_fn_by_phase=phase_pass, + flex_backend="FLASH", + ) + ) + runner = AttentionVariantRunner( + case_config=case_config, + oracle_flex_backend="TRITON_LEGACY", + ) + return runner.run_suite(variants) + + +def run_attention_sensitivity_suite( + *, + case_config: OracleCaseConfig, + mutations: list[str], +) -> list[VariantReport]: + phase_pass = _attention_phase_pass_fns() + variants = [ + VariantSpec( + name=f"attention_sensitivity_{mutation}", + topology=ATTN_SENSITIVITY_TOPOLOGY_BY_MUTATION[mutation], + mutation=mutation, + output_slug=f"{ATTN_SENSITIVITY_TOPOLOGY_BY_MUTATION[mutation].slug()}__{mutation}", + expected_signal="fail", + pass_fn_by_phase=phase_pass, + flex_backend="FLASH", + ) + for mutation in mutations + ] + runner = AttentionVariantRunner( + case_config=case_config, + oracle_flex_backend="TRITON_LEGACY", + ) + return runner.run_suite(variants) diff --git a/tests/integration/megatron/cp_attn/megatron_attention_oracle_worker.py b/tests/integration/megatron/cp_attn/megatron_attention_oracle_worker.py new file mode 100644 index 000000000..697dfce64 --- /dev/null +++ b/tests/integration/megatron/cp_attn/megatron_attention_oracle_worker.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import argparse +from contextlib import contextmanager +import os +from pathlib import Path +import selectors +import shlex +import subprocess +import sys +import time +from typing import Any, cast + +from ..model_support import oracle_worker +from ..model_support.oracle_harness import ( + LIVE_TRAINING_LOG_PATH, + WorkerRunRequest, + _format_elapsed, + _read_json, + _write_json, +) + + +@contextmanager +def _apply_attention_only_mlp_noop(): + """Disables decoder-layer MLP for the attention-only oracle worker.""" + from megatron.core.transformer.transformer_layer import TransformerLayer + + transformer_layer = cast(Any, TransformerLayer) + original_forward_mlp = transformer_layer._forward_mlp + + def _noop_forward_mlp(self, hidden_states, *args, **kwargs): + del args, kwargs + return hidden_states + + transformer_layer._forward_mlp = _noop_forward_mlp + try: + yield + finally: + transformer_layer._forward_mlp = original_forward_mlp + + +def run_worker_subprocess( + request: WorkerRunRequest, + topology_dir: Path, + *, + repo_root: Path, +) -> None: + """Runs the attention-only distributed worker subprocess and stores combined logs.""" + request_path = topology_dir / "run_request.json" + _write_json(request_path, request.model_dump(mode="json")) + worker_module = "integration.megatron.cp_attn.megatron_attention_oracle_worker" + worker_cwd = repo_root / "tests" + pythonpath_entries = [str(repo_root / "src"), str(repo_root / "tests")] + existing_pythonpath = os.environ.get("PYTHONPATH") + if existing_pythonpath: + pythonpath_entries.append(existing_pythonpath) + + command = [ + sys.executable, + "-m", + "torch.distributed.run", + "--standalone", + "--nproc_per_node", + str(request.topology.world_size()), + "-m", + worker_module, + "--worker-run", + "--run-request", + str(request_path), + ] + env = { + **os.environ, + "ART_MEGATRON_ATTACH_TOKEN_UIDS": "1", + "PYTHONUNBUFFERED": "1", + "PYTHONPATH": os.pathsep.join(pythonpath_entries), + } + env.pop("ART_FLEX_BACKEND", None) + for cache_env in ("TORCHINDUCTOR_CACHE_DIR", "TRITON_CACHE_DIR"): + cache_root = env.get(cache_env) + if not cache_root: + continue + env[cache_env] = str(Path(cache_root) / topology_dir.name) + worker_log_path = topology_dir / "worker.log" + launch_start = time.perf_counter() + LIVE_TRAINING_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + with ( + worker_log_path.open("w", encoding="utf-8") as log_file, + LIVE_TRAINING_LOG_PATH.open("a", encoding="utf-8") as live_log_file, + ): + header = ( + "[attention-oracle-harness] launching_worker_subprocess " + f"topology={request.topology.slug()} world_size={request.topology.world_size()} " + f"cwd={worker_cwd} command={shlex.join(command)}\n" + ) + log_file.write(header) + log_file.flush() + live_log_file.write(header) + live_log_file.flush() + run = subprocess.Popen( + command, + cwd=str(worker_cwd), + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=False, + bufsize=0, + ) + assert run.stdout is not None + selector = selectors.DefaultSelector() + selector.register(run.stdout, selectors.EVENT_READ) + while True: + events = selector.select(timeout=0.1) + if not events and run.poll() is not None: + break + for key, _ in events: + fileobj = cast(Any, key.fileobj) + chunk = os.read(fileobj.fileno(), 8192) + if not chunk: + selector.unregister(key.fileobj) + continue + text = chunk.decode("utf-8", errors="replace") + log_file.write(text) + log_file.flush() + live_log_file.write(text) + live_log_file.flush() + run.wait() + footer = ( + "\n[attention-oracle-harness] worker_subprocess_exit " + f"topology={request.topology.slug()} returncode={run.returncode} " + f"elapsed={_format_elapsed(time.perf_counter() - launch_start)}\n" + ) + log_file.write(footer) + log_file.flush() + live_log_file.write(footer) + live_log_file.flush() + if run.returncode != 0: + tail = "\n".join(worker_log_path.read_text(encoding="utf-8").splitlines()[-80:]) + raise RuntimeError( + f"Topology run failed for {request.topology.slug()} with exit code " + f"{run.returncode}.\n{tail}" + ) + + +def run_worker_cli(run_request_path: Path) -> None: + request = WorkerRunRequest.model_validate(_read_json(run_request_path)) + with _apply_attention_only_mlp_noop(): + oracle_worker._worker_run(request) + + +def _main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--worker-run", + action="store_true", + help="Run one distributed attention-only worker invocation from a JSON request.", + ) + parser.add_argument( + "--run-request", + type=Path, + help="Path to the worker run request JSON file.", + ) + args = parser.parse_args(argv) + if args.worker_run: + if args.run_request is None: + parser.error("--run-request is required with --worker-run") + run_worker_cli(args.run_request) + return 0 + parser.error("No action specified") + return 2 + + +if __name__ == "__main__": + raise SystemExit(_main(sys.argv[1:])) diff --git a/tests/integration/megatron/cp_attn/test_attention_packed_vs_flattened.py b/tests/integration/megatron/cp_attn/test_attention_packed_vs_flattened.py new file mode 100644 index 000000000..3d3d51d4c --- /dev/null +++ b/tests/integration/megatron/cp_attn/test_attention_packed_vs_flattened.py @@ -0,0 +1,235 @@ +from __future__ import annotations + +from contextlib import ExitStack +import math +from typing import Any + +import pytest + +torch = pytest.importorskip("torch") + +from art.megatron.flex_attn.attention import FlexAttentionWrapper +from art.megatron.shared_prefix_state import create_shared_prefix_state +from tests.integration.megatron.gdn_shared_prefix.cases import default_phase0_cases +from tests.integration.megatron.gdn_shared_prefix.metrics import ( + GDN_CORRECTNESS_DTYPE, + MEAN_ABS_PCT_MISMATCH_THRESHOLD, + MEAN_ABS_PCT_THRESHOLD, + assert_mean_abs_pct, + mean_abs_pct, +) +from tests.integration.megatron.gdn_shared_prefix.packed_layout import ( + build_phase0_packed_tensors, +) +from tests.integration.megatron.gdn_shared_prefix.parser_import import ( + parse_gdn_shared_prefix_segments, +) +from tests.integration.megatron.model_support.oracle_harness import ( + TEST_DEFAULT_FLEX_BACKEND, +) +from tests.integration.megatron.model_support.oracle_worker import ( + _apply_requested_flex_backend_patch, + _apply_test_attention_full_fp32_patch, + _apply_test_flex_inner_fp32_patch, +) + + +@pytest.fixture(autouse=True) +def _fp32_test_flex_backend(): + with ExitStack() as stack: + stack.enter_context( + _apply_requested_flex_backend_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + stack.enter_context( + _apply_test_flex_inner_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + stack.enter_context( + _apply_test_attention_full_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + yield + + +@pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA is required for compiled flex-attention shared-prefix coverage.", +) +def test_shared_prefix_attention_matches_flattened_grad_accumulation() -> None: + case = next( + item for item in default_phase0_cases() if item.name == "multi_family_repeated" + ) + tensors = build_phase0_packed_tensors(case) + group_ids = tensors["group_ids"].cuda() + parent_ids = tensors["parent_ids"].cuda() + spec = parse_gdn_shared_prefix_segments( + group_ids.cpu(), parent_ids.cpu(), min_completions_per_family=1 + ) + q, k, v = _attention_inputs(group_ids.shape, seed=20260425) + q_ref = q.detach().clone().requires_grad_(True) + k_ref = k.detach().clone().requires_grad_(True) + v_ref = v.detach().clone().requires_grad_(True) + output_grad = _packed_output_grad(spec, q.shape, seed=20260426) + + attention_state = create_shared_prefix_state(group_ids, parent_ids) + packed_out = FlexAttentionWrapper()( + q, + k, + v, + block_mask=attention_state.block_mask, + scale=1.0 / math.sqrt(q.shape[-1]), + enable_gqa=False, + ) + (packed_out * output_grad).sum().backward() + + ref_out = torch.zeros_like(packed_out) + ref_loss = q_ref.new_zeros(()) + for family in spec.families: + prefix = family.prefix + prefix_grad_used = False + for completion in family.completions: + indices = torch.tensor( + [ + *range(prefix.start, prefix.end), + *range(completion.start, completion.end), + ], + device=q.device, + dtype=torch.long, + ) + row = family.row_index + q_slice = q_ref[row : row + 1].index_select(2, indices) + k_slice = k_ref[row : row + 1].index_select(2, indices) + v_slice = v_ref[row : row + 1].index_select(2, indices) + flat_out = _dense_causal_attention(q_slice, k_slice, v_slice) + + ref_out[row, :, completion.start : completion.end] = flat_out[ + 0, :, prefix.length : + ] + flat_grad = torch.zeros_like(flat_out) + flat_grad[0, :, prefix.length :] = output_grad[ + row, :, completion.start : completion.end + ] + if not prefix_grad_used: + ref_out[row, :, prefix.start : prefix.end] = flat_out[ + 0, :, : prefix.length + ] + flat_grad[0, :, : prefix.length] = output_grad[ + row, :, prefix.start : prefix.end + ] + prefix_grad_used = True + ref_loss = ref_loss + (flat_out * flat_grad).sum() + ref_loss.backward() + + real_mask = _real_token_mask(spec, q.shape, device=q.device) + assert_mean_abs_pct(ref_out[real_mask], packed_out[real_mask], "attention_output") + assert q.grad is not None + assert k.grad is not None + assert v.grad is not None + assert q_ref.grad is not None + assert k_ref.grad is not None + assert v_ref.grad is not None + assert_mean_abs_pct(q_ref.grad, q.grad, "q_grad") + assert_mean_abs_pct(k_ref.grad, k.grad, "k_grad") + assert_mean_abs_pct(v_ref.grad, v.grad, "v_grad") + + +@pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA is required for compiled flex-attention shared-prefix coverage.", +) +def test_physical_causal_attention_leaks_across_siblings() -> None: + case = next( + item for item in default_phase0_cases() if item.name == "multi_family_repeated" + ) + tensors = build_phase0_packed_tensors(case) + group_ids = tensors["group_ids"].cuda() + parent_ids = tensors["parent_ids"].cuda() + spec = parse_gdn_shared_prefix_segments( + group_ids.cpu(), parent_ids.cpu(), min_completions_per_family=1 + ) + q, k, v = _attention_inputs(group_ids.shape, seed=20260427) + attention_state = create_shared_prefix_state(group_ids, parent_ids) + packed_out = FlexAttentionWrapper()( + q, + k, + v, + block_mask=attention_state.block_mask, + scale=1.0 / math.sqrt(q.shape[-1]), + enable_gqa=False, + ) + physical_out = _dense_causal_attention(q, k, v) + completion_mask = _completion_token_mask(spec, q.shape, device=q.device) + assert ( + mean_abs_pct( + packed_out[completion_mask], + physical_out[completion_mask], + ) + > MEAN_ABS_PCT_MISMATCH_THRESHOLD + ) + + +def _attention_inputs( + shape: torch.Size, *, seed: int +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + batch_size, sequence_length = shape + generator = torch.Generator(device="cuda").manual_seed(seed) + q = torch.randn( + batch_size, + 2, + sequence_length, + 16, + device="cuda", + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + requires_grad=True, + ) + k = torch.randn( + q.shape, device="cuda", dtype=GDN_CORRECTNESS_DTYPE, generator=generator + ) + v = torch.randn( + q.shape, device="cuda", dtype=GDN_CORRECTNESS_DTYPE, generator=generator + ) + return q, k.requires_grad_(True), v.requires_grad_(True) + + +def _dense_causal_attention( + q: torch.Tensor, k: torch.Tensor, v: torch.Tensor +) -> torch.Tensor: + scores = torch.matmul(q, k.transpose(-1, -2)) * (1.0 / math.sqrt(q.shape[-1])) + length = int(q.shape[-2]) + causal_mask = torch.ones(length, length, device=q.device, dtype=torch.bool).tril() + scores = scores.masked_fill(~causal_mask, float("-inf")) + probs = torch.softmax(scores, dim=-1) + return torch.matmul(probs, v) + + +def _packed_output_grad(spec: Any, shape: torch.Size, *, seed: int) -> torch.Tensor: + generator = torch.Generator(device="cuda").manual_seed(seed) + grad = torch.randn( + shape, + device="cuda", + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + return grad * _real_token_mask(spec, shape, device=grad.device) * 0.1 + + +def _real_token_mask( + spec: Any, shape: torch.Size, *, device: torch.device +) -> torch.Tensor: + mask = torch.zeros(shape, device=device, dtype=torch.bool) + for row_index, valid_length in enumerate(spec.valid_lengths): + mask[row_index, :, :valid_length] = True + return mask + + +def _completion_token_mask( + spec: Any, shape: torch.Size, *, device: torch.device +) -> torch.Tensor: + mask = torch.zeros(shape, device=device, dtype=torch.bool) + for family in spec.families: + for completion in family.completions: + mask[ + family.row_index, + :, + completion.start : completion.end, + ] = True + return mask diff --git a/tests/integration/megatron/cp_attn/test_megatron_attention_oracle_correctness.py b/tests/integration/megatron/cp_attn/test_megatron_attention_oracle_correctness.py new file mode 100644 index 000000000..d307f2838 --- /dev/null +++ b/tests/integration/megatron/cp_attn/test_megatron_attention_oracle_correctness.py @@ -0,0 +1,106 @@ +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path +from typing import Callable + +import pytest + +from ..model_support.oracle_harness import LIVE_TRAINING_LOG_PATH, available_gpu_count +from .megatron_attention_oracle_harness import ( + ATTN_SENSITIVITY_MUTATION_ENV, + attention_case_config, + attention_required_world_size, + attention_sensitivity_enabled, + attention_sensitivity_mutations, + run_attention_sensitivity_suite, + run_attention_suite, +) + +REPO_ROOT = Path(__file__).resolve().parents[4] +ATTN_CORRECTNESS_LOG_PATH = REPO_ROOT / ".local" / "attention_correctness.log" +ATTN_SENSITIVITY_LOG_PATH = REPO_ROOT / ".local" / "attention_sensitivity.log" + + +def _run_suite_with_log( + *, + log_path: Path, + run: Callable[[], object], +) -> None: + log_path.parent.mkdir(parents=True, exist_ok=True) + LIVE_TRAINING_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + LIVE_TRAINING_LOG_PATH.write_text("", encoding="utf-8") + with log_path.open("w", encoding="utf-8") as log_file: + with redirect_stdout(log_file), redirect_stderr(log_file): + run() + + +def _announce_report_log( + *, + log_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + with capsys.disabled(): + print(f"\nMegatron attention oracle report log: {log_path}", flush=True) + print( + f"Megatron attention live training log: {LIVE_TRAINING_LOG_PATH}", + flush=True, + ) + + +def _require_gpus_for(topology_world_size: int) -> None: + gpu_count = available_gpu_count() + if gpu_count < topology_world_size: + pytest.skip( + f"Need {topology_world_size} GPUs for attention topology run, only found {gpu_count}" + ) + + +def test_megatron_attention_diff_sensitivity( + capsys: pytest.CaptureFixture[str], +) -> None: + _announce_report_log(log_path=ATTN_SENSITIVITY_LOG_PATH, capsys=capsys) + if not attention_sensitivity_enabled(): + ATTN_SENSITIVITY_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + ATTN_SENSITIVITY_LOG_PATH.write_text( + ( + "Attention sensitivity suite skipped. " + f"Set {ATTN_SENSITIVITY_MUTATION_ENV}=all (or one mutation / CSV).\n" + ), + encoding="utf-8", + ) + pytest.skip( + f"Set {ATTN_SENSITIVITY_MUTATION_ENV}=all (or one mutation / CSV) to enable attention sensitivity check." + ) + mutations = attention_sensitivity_mutations() + sensitivity_world_size = attention_required_world_size(mutations) + _require_gpus_for(sensitivity_world_size) + _run_suite_with_log( + log_path=ATTN_SENSITIVITY_LOG_PATH, + run=lambda: run_attention_sensitivity_suite( + case_config=attention_case_config(), + mutations=mutations, + ), + ) + + +def test_megatron_attention_topology_suite( + capsys: pytest.CaptureFixture[str], +) -> None: + _announce_report_log(log_path=ATTN_CORRECTNESS_LOG_PATH, capsys=capsys) + gpu_count = available_gpu_count() + if gpu_count < 2: + ATTN_CORRECTNESS_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + ATTN_CORRECTNESS_LOG_PATH.write_text( + ( + "Attention topology suite skipped. " + f"Need at least 2 GPUs, found {gpu_count}.\n" + ), + encoding="utf-8", + ) + _require_gpus_for(2) + _run_suite_with_log( + log_path=ATTN_CORRECTNESS_LOG_PATH, + run=lambda: run_attention_suite( + case_config=attention_case_config(), + max_world_size=gpu_count, + ), + ) diff --git a/tests/integration/megatron/gdn_shared_prefix/.gitignore b/tests/integration/megatron/gdn_shared_prefix/.gitignore new file mode 100644 index 000000000..8038fa9a7 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/.gitignore @@ -0,0 +1,12 @@ +/README.md +/bench_gdn_conv_gelu.py +/bench_gdn_cp_layout_exchange.py +/bench_gdn_cp_packed_layer.py +/bench_single_gdn_operation.py +/bench_stacked_gdn_proxy.py +/benchmark_gdn.py +/configs/ +/nsys_profile_tables.py +/test_gdn_cp_packed_vs_flattened.py +/test_real_gdn_cp_chain.py +/test_real_gdn_cp_local_fork.py diff --git a/tests/integration/megatron/gdn_shared_prefix/__init__.py b/tests/integration/megatron/gdn_shared_prefix/__init__.py new file mode 100644 index 000000000..d1e3e6581 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/__init__.py @@ -0,0 +1,3 @@ +"""Shared-prefix GDN integration validation package.""" + +import art # noqa: F401 diff --git a/tests/integration/megatron/gdn_shared_prefix/artifacts.py b/tests/integration/megatron/gdn_shared_prefix/artifacts.py new file mode 100644 index 000000000..0de831b89 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/artifacts.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from datetime import UTC, datetime +import importlib.metadata +import json +from pathlib import Path +import platform +import subprocess +import sys + +from pydantic import BaseModel, ConfigDict, Field + + +class GitRepoState(BaseModel): + model_config = ConfigDict(frozen=True) + + path: str + commit: str + dirty: bool + status: tuple[str, ...] = Field(default_factory=tuple) + + +class RuntimeInfo(BaseModel): + model_config = ConfigDict(frozen=True) + + python: str + platform: str + torch: str | None = None + cuda_available: bool | None = None + cuda: str | None = None + cudnn: int | None = None + gpu_names: tuple[str, ...] = Field(default_factory=tuple) + package_versions: dict[str, str] = Field(default_factory=dict) + + +class GdnArtifactManifest(BaseModel): + model_config = ConfigDict(frozen=True) + + created_at: str + kind: str + command: tuple[str, ...] + art: GitRepoState + project_tracking: GitRepoState | None = None + runtime: RuntimeInfo + configs: dict[str, object] = Field(default_factory=dict) + cases: tuple[dict[str, object], ...] = Field(default_factory=tuple) + caveats: tuple[str, ...] = Field(default_factory=tuple) + + +def write_manifest( + output_dir: Path, + *, + kind: str, + command: list[str], + configs: dict[str, object] | None = None, + cases: tuple[dict[str, object], ...] = (), + caveats: tuple[str, ...] = (), +) -> Path: + output_dir.mkdir(parents=True, exist_ok=True) + manifest = GdnArtifactManifest( + created_at=datetime.now(UTC).isoformat(), + kind=kind, + command=tuple(command), + art=git_state(Path(__file__).resolve().parents[4]), + project_tracking=_optional_git_state( + Path("/root/ws/project_tracking/art/megatron_bridge_model_support_skill") + ), + runtime=runtime_info(), + configs={} if configs is None else configs, + cases=cases, + caveats=caveats, + ) + path = output_dir / "manifest.json" + path.write_text(json.dumps(manifest.model_dump(), indent=2, sort_keys=True) + "\n") + return path + + +def git_state(path: Path) -> GitRepoState: + commit = _git(path, "rev-parse", "HEAD") + status = tuple( + line for line in _git(path, "status", "--short").splitlines() if line + ) + return GitRepoState( + path=str(path), + commit=commit, + dirty=bool(status), + status=status, + ) + + +def runtime_info() -> RuntimeInfo: + torch_version: str | None = None + cuda_available: bool | None = None + cuda_version: str | None = None + cudnn_version: int | None = None + gpu_names: tuple[str, ...] = () + try: + import torch + + torch_version = torch.__version__ + cuda_available = torch.cuda.is_available() + cuda_version = torch.version.cuda + cudnn_version = torch.backends.cudnn.version() + if cuda_available: + gpu_names = tuple( + torch.cuda.get_device_name(index) + for index in range(torch.cuda.device_count()) + ) + except Exception: + pass + packages = { + name: version + for name in ( + "triton", + "flash-linear-attention", + "fla", + "megatron-core", + "transformer-engine", + "causal-conv1d", + ) + if (version := _dist_version(name)) is not None + } + return RuntimeInfo( + python=sys.version.split()[0], + platform=platform.platform(), + torch=torch_version, + cuda_available=cuda_available, + cuda=cuda_version, + cudnn=cudnn_version, + gpu_names=gpu_names, + package_versions=packages, + ) + + +def _optional_git_state(path: Path) -> GitRepoState | None: + if not path.exists(): + return None + try: + return git_state(path) + except subprocess.CalledProcessError: + return None + + +def _dist_version(name: str) -> str | None: + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return None + + +def _git(path: Path, *args: str) -> str: + result = subprocess.run( + ("git", "-C", str(path), *args), + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + return result.stdout.strip() diff --git a/tests/integration/megatron/gdn_shared_prefix/cases.py b/tests/integration/megatron/gdn_shared_prefix/cases.py new file mode 100644 index 000000000..e573c86c8 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/cases.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field + + +class GdnFamilyShape(BaseModel): + model_config = ConfigDict(frozen=True) + + prefix_length: int = Field(ge=1) + suffix_lengths: tuple[int, ...] = Field(min_length=1) + + +class GdnPackedRowShape(BaseModel): + model_config = ConfigDict(frozen=True) + + families: tuple[GdnFamilyShape, ...] = Field(min_length=1) + + +class GdnPhase0Case(BaseModel): + model_config = ConfigDict(frozen=True) + + name: str + sequence_length: int = Field(ge=1) + rows: tuple[GdnPackedRowShape, ...] = Field(min_length=1) + seed: int = 0 + description: str = "" + + +def gdn_family_token_count(family: GdnFamilyShape) -> int: + return int(family.prefix_length) + sum( + int(length) for length in family.suffix_lengths + ) + + +def fit_gdn_family_to_remaining( + family: GdnFamilyShape, remaining_tokens: int +) -> GdnFamilyShape | None: + if int(remaining_tokens) < int(family.prefix_length): + return None + used = int(family.prefix_length) + suffixes: list[int] = [] + for suffix_length in family.suffix_lengths: + length = int(suffix_length) + if used + length > int(remaining_tokens): + break + suffixes.append(length) + used += length + if not suffixes: + return None + if len(suffixes) == len(family.suffix_lengths): + return family + return GdnFamilyShape( + prefix_length=family.prefix_length, + suffix_lengths=tuple(suffixes), + ) + + +def default_phase0_cases(conv_width: int = 4) -> tuple[GdnPhase0Case, ...]: + return ( + GdnPhase0Case( + name="single_family_two_branches", + sequence_length=24, + rows=( + GdnPackedRowShape( + families=(GdnFamilyShape(prefix_length=5, suffix_lengths=(3, 4)),) + ), + ), + seed=11, + description="One prompt family with two child completions.", + ), + GdnPhase0Case( + name="multi_family_repeated", + sequence_length=64, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=5, suffix_lengths=(3, 3)), + GdnFamilyShape(prefix_length=6, suffix_lengths=(2, 4)), + GdnFamilyShape(prefix_length=4, suffix_lengths=(5, 3)), + ) + ), + ), + seed=13, + description="Several independent prompt families in one packed row.", + ), + GdnPhase0Case( + name="ragged_family_mix", + sequence_length=96, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=7, suffix_lengths=(2, 6, 3)), + GdnFamilyShape(prefix_length=3, suffix_lengths=(8, 1)), + ) + ), + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=9, suffix_lengths=(4, 5)), + GdnFamilyShape(prefix_length=2, suffix_lengths=(2, 2, 7)), + ) + ), + ), + seed=17, + description="Ragged prefix lengths, branch counts, and suffix lengths.", + ), + GdnPhase0Case( + name="dominant_family", + sequence_length=128, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=32, suffix_lengths=(20, 7, 5)), + GdnFamilyShape(prefix_length=4, suffix_lengths=(3, 3)), + ) + ), + ), + seed=19, + description="One long family plus a small background family.", + ), + GdnPhase0Case( + name="conv_tail_boundary", + sequence_length=64, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape( + prefix_length=conv_width + 2, + suffix_lengths=(conv_width - 1, conv_width, conv_width + 1), + ), + ) + ), + ), + seed=23, + description="Suffixes shorter than, equal to, and longer than conv width.", + ), + GdnPhase0Case( + name="padding_tail", + sequence_length=80, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=6, suffix_lengths=(4, 4)), + GdnFamilyShape(prefix_length=5, suffix_lengths=(3, 3)), + ) + ), + ), + seed=29, + description="Real tokens followed by padding.", + ), + GdnPhase0Case( + name="cp_boundary_prefix", + sequence_length=96, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=30, suffix_lengths=(4, 4)), + GdnFamilyShape(prefix_length=8, suffix_lengths=(5, 5)), + ) + ), + ), + seed=31, + description="A prefix crosses a proportional CP partition boundary.", + ), + GdnPhase0Case( + name="cp_boundary_suffix", + sequence_length=112, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=8, suffix_lengths=(35, 4)), + GdnFamilyShape(prefix_length=6, suffix_lengths=(5, 5)), + ) + ), + ), + seed=37, + description="A suffix crosses a proportional CP partition boundary.", + ), + GdnPhase0Case( + name="long_sibling", + sequence_length=192, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=8, suffix_lengths=(96, 7, 5)), + GdnFamilyShape(prefix_length=6, suffix_lengths=(4, 4)), + ) + ), + ), + seed=41, + description="One sibling completion dominates the row and crosses CP waves.", + ), + GdnPhase0Case( + name="many_branches_wave", + sequence_length=96, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape( + prefix_length=4, + suffix_lengths=(2, 3, 2, 4, 2, 3, 2, 4, 2, 3, 2, 4), + ), + ) + ), + ), + seed=43, + description="Many short siblings force multi-wave completion scheduling.", + ), + GdnPhase0Case( + name="family_boundary_at_partition", + sequence_length=80, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=12, suffix_lengths=(20,)), + GdnFamilyShape(prefix_length=8, suffix_lengths=(24,)), + ) + ), + ), + seed=47, + description="A whole family boundary lands exactly on the CP2 partition.", + ), + GdnPhase0Case( + name="empty_trailing_rank", + sequence_length=8, + rows=( + GdnPackedRowShape( + families=(GdnFamilyShape(prefix_length=2, suffix_lengths=(2,)),) + ), + ), + seed=53, + description="Tiny row leaves trailing CP ranks empty.", + ), + ) diff --git a/tests/integration/megatron/gdn_shared_prefix/distributed_grad.py b/tests/integration/megatron/gdn_shared_prefix/distributed_grad.py new file mode 100644 index 000000000..3159c13ac --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/distributed_grad.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from collections import defaultdict + +import torch + + +def all_reduce_parameter_grads_coalesced( + module: torch.nn.Module, *, group: object | None = None +) -> None: + grad_entries: dict[ + tuple[torch.device, torch.dtype], + list[tuple[torch.nn.Parameter, torch.Tensor | None, int]], + ] = defaultdict(list) + main_grad_entries: dict[tuple[torch.device, torch.dtype], list[torch.Tensor]] = ( + defaultdict(list) + ) + for parameter in module.parameters(): + grad_entries[(parameter.device, parameter.dtype)].append( + (parameter, parameter.grad, 1 if parameter.grad is not None else 0) + ) + main_grad = getattr(parameter, "main_grad", None) + if main_grad is not None: + main_grad_entries[(main_grad.device, main_grad.dtype)].append(main_grad) + for entries in grad_entries.values(): + _all_reduce_parameter_grad_group(entries, group=group) + for entries in main_grad_entries.values(): + _all_reduce_tensor_group(entries, group=group) + + +def _all_reduce_parameter_grad_group( + entries: list[tuple[torch.nn.Parameter, torch.Tensor | None, int]], + *, + group: object | None, +) -> None: + if not entries: + return + has_grad = torch.tensor( + [entry_has_grad for _, _, entry_has_grad in entries], + device=entries[0][0].device, + dtype=torch.int32, + ) + torch.distributed.all_reduce(has_grad, group=group) # ty: ignore[possibly-missing-attribute] + flat = torch.cat( + [ + torch.zeros( + parameter.numel(), device=parameter.device, dtype=parameter.dtype + ) + if grad is None + else grad.reshape(-1) + for parameter, grad, _ in entries + ] + ) + torch.distributed.all_reduce(flat, group=group) # ty: ignore[possibly-missing-attribute] + offset = 0 + for index, (parameter, grad, _) in enumerate(entries): + size = parameter.numel() + reduced = flat.narrow(0, offset, size).view_as(parameter) + if int(has_grad[index].item()) > 0: + if grad is None: + parameter.grad = torch.empty_like(parameter) + grad = parameter.grad + grad.copy_(reduced) + offset += size + + +def _all_reduce_tensor_group( + entries: list[torch.Tensor], *, group: object | None +) -> None: + if not entries: + return + flat = torch.cat([tensor.reshape(-1) for tensor in entries]) + torch.distributed.all_reduce(flat, group=group) # ty: ignore[possibly-missing-attribute] + offset = 0 + for tensor in entries: + size = tensor.numel() + tensor.copy_(flat.narrow(0, offset, size).view_as(tensor)) + offset += size diff --git a/tests/integration/megatron/gdn_shared_prefix/layout_reference.py b/tests/integration/megatron/gdn_shared_prefix/layout_reference.py new file mode 100644 index 000000000..7369eaef7 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/layout_reference.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from collections.abc import Sequence + +from pydantic import BaseModel, ConfigDict, Field +import torch +from torch import Tensor + +from art.megatron.context_parallel.layout_index import TokenLayoutIndex +from art.megatron.gdn.gdn_shared_prefix import ( + GdnPackedExecutionSpec, + parse_gdn_shared_prefix_segments, +) +from art.megatron.gdn.layout import ( + GdnCpExchangePlan, + GdnCpPeerTransfer, + build_local_rank_cp_exchange_plan_from_dest_ranges, +) + + +class TestGdnCpLayoutPlan(BaseModel): + model_config = ConfigDict(frozen=True) + + batch_size: int = Field(ge=1) + sequence_length: int = Field(ge=1) + cp_size: int = Field(ge=1) + attention_token_ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...] + gdn_token_ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...] + attention_to_gdn: GdnCpExchangePlan + gdn_to_attention: GdnCpExchangePlan + + +def build_test_gdn_cp_layout_plan( + *, + group_ids: Tensor, + parent_ids: Tensor, + cp_size: int, + attention_token_layout_index: TokenLayoutIndex | None = None, + gdn_token_ranges_by_rank: Sequence[Sequence[tuple[int, int, int]]] | None = None, + device: torch.device | str | None = None, +) -> TestGdnCpLayoutPlan: + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + gdn_ranges = ( + _normalize_rank_ranges(gdn_token_ranges_by_rank, cp_size=cp_size) + if gdn_token_ranges_by_rank is not None + else _split_gdn_token_ranges_by_rank(spec, cp_size=cp_size) + ) + source_layout = attention_token_layout_index or _token_layout_from_rank_ranges( + _split_attention_token_ranges_by_rank(spec, cp_size=cp_size) + ) + attention_to_gdn = _build_full_exchange_plan( + source_layout=source_layout, + dest_ranges_by_rank=gdn_ranges, + device=device, + ) + gdn_layout = _token_layout_from_rank_ranges(gdn_ranges) + gdn_to_attention = _build_full_exchange_plan( + source_layout=gdn_layout, + dest_ranges_by_rank=source_layout.ownership_ranges_by_rank, + device=device, + ) + return TestGdnCpLayoutPlan( + batch_size=spec.batch_size, + sequence_length=spec.sequence_length, + cp_size=cp_size, + attention_token_ranges_by_rank=source_layout.ownership_ranges_by_rank, + gdn_token_ranges_by_rank=gdn_ranges, + attention_to_gdn=attention_to_gdn, + gdn_to_attention=gdn_to_attention, + ) + + +def _build_full_exchange_plan( + *, + source_layout: TokenLayoutIndex, + dest_ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...], + device: torch.device | str | None, +) -> GdnCpExchangePlan: + transfers: dict[tuple[int, int], GdnCpPeerTransfer] = {} + for local_rank in range(len(source_layout.token_counts_by_rank)): + local_plan = build_local_rank_cp_exchange_plan_from_dest_ranges( + source_layout=source_layout, + dest_ranges_by_rank=dest_ranges_by_rank, + device=device, + local_rank=local_rank, + cross_rank_token_count=0, + ) + for transfer in local_plan.transfers: + transfers.setdefault((transfer.source_rank, transfer.dest_rank), transfer) + return GdnCpExchangePlan.model_construct( + cp_size=len(source_layout.token_counts_by_rank), + source_token_counts_by_rank=source_layout.token_counts_by_rank, + dest_token_counts_by_rank=tuple( + sum(end - start for start, end, _ in ranges) + for ranges in dest_ranges_by_rank + ), + transfers=tuple( + sorted( + transfers.values(), key=lambda item: (item.source_rank, item.dest_rank) + ) + ), + ) + + +def _split_attention_token_ranges_by_rank( + spec: GdnPackedExecutionSpec, + *, + cp_size: int, +) -> tuple[tuple[tuple[int, int, int], ...], ...]: + return _split_ordered_ranges_by_rank( + tuple( + ( + row_index * spec.sequence_length, + row_index * spec.sequence_length + valid_length, + ) + for row_index, valid_length in enumerate(spec.valid_lengths) + if valid_length + ), + cp_size=cp_size, + ) + + +def _split_gdn_token_ranges_by_rank( + spec: GdnPackedExecutionSpec, + *, + cp_size: int, +) -> tuple[tuple[tuple[int, int, int], ...], ...]: + return _split_ordered_ranges_by_rank( + tuple( + ( + _segment_token_start(segment, spec.sequence_length), + _segment_token_start(segment, spec.sequence_length) + segment.length, + ) + for segment in spec.segments() + ), + cp_size=cp_size, + ) + + +def _split_ordered_ranges_by_rank( + ordered_ranges: Sequence[tuple[int, int]], + *, + cp_size: int, +) -> tuple[tuple[tuple[int, int, int], ...], ...]: + total_tokens = sum(end - start for start, end in ordered_ranges) + ranks: list[list[tuple[int, int, int]]] = [[] for _ in range(cp_size)] + rank_positions = [0] * cp_size + rank = 0 + rank_end = (total_tokens * (rank + 1)) // cp_size + consumed = 0 + for start, end in ordered_ranges: + cursor = start + while cursor < end: + while rank + 1 < cp_size and consumed >= rank_end: + rank += 1 + rank_end = (total_tokens * (rank + 1)) // cp_size + piece_end = end + if rank + 1 < cp_size: + piece_end = min(piece_end, cursor + rank_end - consumed) + ranks[rank].append((cursor, piece_end, rank_positions[rank])) + piece_length = piece_end - cursor + rank_positions[rank] += piece_length + consumed += piece_length + cursor = piece_end + return tuple(tuple(ranges) for ranges in ranks) + + +def _token_layout_from_rank_ranges( + ranges_by_rank: Sequence[Sequence[tuple[int, int, int]]], +) -> TokenLayoutIndex: + ranges = _normalize_rank_ranges(ranges_by_rank, cp_size=len(ranges_by_rank)) + return TokenLayoutIndex( + ownership_ranges_by_rank=ranges, + token_counts_by_rank=tuple( + sum(end - start for start, end, _ in rank_ranges) for rank_ranges in ranges + ), + ) + + +def _normalize_rank_ranges( + ranges_by_rank: Sequence[Sequence[tuple[int, int, int]]], + *, + cp_size: int, +) -> tuple[tuple[tuple[int, int, int], ...], ...]: + if len(ranges_by_rank) != cp_size: + raise ValueError("rank range count must equal cp_size") + return tuple( + tuple((int(start), int(end), int(position)) for start, end, position in ranges) + for ranges in ranges_by_rank + ) + + +def _segment_token_start(segment: object, sequence_length: int) -> int: + return int(getattr(segment, "row_index")) * int(sequence_length) + int( + getattr(segment, "start") + ) diff --git a/tests/integration/megatron/gdn_shared_prefix/metrics.py b/tests/integration/megatron/gdn_shared_prefix/metrics.py new file mode 100644 index 000000000..b2f0a0ca2 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/metrics.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +from typing import Any + +import torch +from torch import Tensor + +from ..metrics import ( + DEFAULT_MEAN_ABS_PCT_THRESHOLD, + mean_abs_pct, + mean_abs_pct_from_sums, +) + +# Testing design: the full model oracle remains fp32 and uses a narrow torch +# reference for the Qwen3.5 GDN recurrent math because the current FLA/TileLang +# stack has no valid fp32 GDN backward path. These real-GDN tests intentionally +# exercise the production bf16 kernels and CP machinery instead. Do not change +# this dtype/threshold split without discussing the oracle coverage tradeoff. +GDN_CORRECTNESS_DTYPE = torch.bfloat16 +MEAN_ABS_PCT_THRESHOLD = DEFAULT_MEAN_ABS_PCT_THRESHOLD +MEAN_ABS_PCT_MISMATCH_THRESHOLD = 0.1 +REAL_GDN_LOSS_MEAN_ABS_PCT_THRESHOLD = ( + 3.0 if GDN_CORRECTNESS_DTYPE == torch.bfloat16 else MEAN_ABS_PCT_THRESHOLD +) +REAL_GDN_OUTPUT_MEAN_ABS_PCT_THRESHOLD = ( + 3.0 if GDN_CORRECTNESS_DTYPE == torch.bfloat16 else MEAN_ABS_PCT_THRESHOLD +) +REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD = ( + 5.0 if GDN_CORRECTNESS_DTYPE == torch.bfloat16 else MEAN_ABS_PCT_THRESHOLD +) + + +def assert_mean_abs_pct( + reference: Tensor, + candidate: Tensor, + name: str, + *, + threshold: float = MEAN_ABS_PCT_THRESHOLD, +) -> None: + pct = mean_abs_pct(reference, candidate) + assert pct <= threshold, f"{name}: mean_abs_pct={pct:.6g}% > {threshold}%" + + +def assert_scalar_loss_close( + reference: Tensor, + candidate: Tensor, + name: str, + *, + threshold: float = REAL_GDN_LOSS_MEAN_ABS_PCT_THRESHOLD, +) -> None: + pct = mean_abs_pct(reference, candidate) + assert pct <= threshold, f"{name}: mean_abs_pct={pct:.6g}% > {threshold}%" + + +def stable_output_mse_loss( + output: Tensor, + target: Tensor, + *, + mask: Tensor | None = None, + denominator: Tensor | None = None, +) -> Tensor: + diff = output.float() - target.float() + if mask is not None: + diff = diff * mask.to(device=diff.device, dtype=diff.dtype) + if denominator is None: + denominator = ( + mask.to(device=diff.device, dtype=diff.dtype).expand_as(diff).sum() + ) + if denominator is None: + denominator = diff.new_tensor(float(diff.numel())) + denominator = denominator.to(device=diff.device, dtype=diff.dtype) + return diff.square().sum() / (denominator + 1e-18) + + +def assert_real_gdn_metrics(metrics: Any, name: str) -> None: + assert metrics.loss_mean_abs_pct <= REAL_GDN_LOSS_MEAN_ABS_PCT_THRESHOLD, ( + f"{name}: loss_mean_abs_pct={metrics.loss_mean_abs_pct:.6g}% > " + f"{REAL_GDN_LOSS_MEAN_ABS_PCT_THRESHOLD}%" + ) + assert metrics.output_mean_abs_pct <= REAL_GDN_OUTPUT_MEAN_ABS_PCT_THRESHOLD, ( + f"{name}: output_mean_abs_pct={metrics.output_mean_abs_pct:.6g}% > " + f"{REAL_GDN_OUTPUT_MEAN_ABS_PCT_THRESHOLD}%" + ) + assert metrics.hidden_grad_mean_abs_pct <= REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD, ( + f"{name}: hidden_grad_mean_abs_pct={metrics.hidden_grad_mean_abs_pct:.6g}% > " + f"{REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD}%" + ) + assert metrics.param_grad_mean_abs_pct <= REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD, ( + f"{name}: param_grad_mean_abs_pct={metrics.param_grad_mean_abs_pct:.6g}% > " + f"{REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD}%" + ) + + +def parameter_grad_mean_abs_pct_with_name( + reference: torch.nn.Module, + candidate: torch.nn.Module, +) -> tuple[str, float]: + worst_name = "" + worst_pct = 0.0 + abs_diff_sum = 0.0 + reference_abs_sum = 0.0 + numel = 0 + candidate_params = dict(candidate.named_parameters()) + for name, reference_param in reference.named_parameters(): + candidate_param = candidate_params[name] + reference_grad = parameter_grad(reference_param) + candidate_grad = parameter_grad(candidate_param) + if reference_grad is None and candidate_grad is None: + continue + if reference_grad is None or candidate_grad is None: + raise AssertionError(f"mismatched parameter grad presence for {name}") + pct = mean_abs_pct(reference_grad, candidate_grad) + if pct > worst_pct: + worst_name = name + worst_pct = pct + reference_grad_fp32 = reference_grad.detach().float() + candidate_grad_fp32 = candidate_grad.detach().float() + abs_diff_sum += float( + (candidate_grad_fp32 - reference_grad_fp32).abs().sum().item() + ) + reference_abs_sum += float(reference_grad_fp32.abs().sum().item()) + numel += int(reference_grad_fp32.numel()) + if numel == 0: + return worst_name, 0.0 + return worst_name, mean_abs_pct_from_sums(abs_diff_sum, reference_abs_sum, numel) + + +def assert_parameter_grad_mean_abs_pct( + reference: torch.nn.Module, + candidate: torch.nn.Module, + name: str, + *, + threshold: float = MEAN_ABS_PCT_THRESHOLD, +) -> None: + param_name, pct = parameter_grad_mean_abs_pct_with_name(reference, candidate) + assert pct <= threshold, ( + f"{name}:{param_name}: mean_abs_pct={pct:.6g}% > {threshold}%" + ) + + +def parameter_grad(parameter: torch.nn.Parameter) -> Tensor | None: + main_grad = getattr(parameter, "main_grad", None) + if parameter.grad is not None and main_grad is not None: + if not getattr(parameter, "grad_added_to_main_grad", False) or getattr( + parameter, "zero_out_wgrad", False + ): + return main_grad + parameter.grad.to(dtype=main_grad.dtype) + return main_grad + if parameter.grad is not None: + return parameter.grad + if main_grad is not None: + return main_grad + return None diff --git a/tests/integration/megatron/gdn_shared_prefix/oracles.py b/tests/integration/megatron/gdn_shared_prefix/oracles.py new file mode 100644 index 000000000..3d3f9ae12 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/oracles.py @@ -0,0 +1,296 @@ +from __future__ import annotations + +from copy import deepcopy + +from pydantic import BaseModel, ConfigDict, Field +import torch +from torch import Tensor +import torch.nn.functional as F + +from .metrics import ( + mean_abs_pct, + parameter_grad_mean_abs_pct_with_name, + stable_output_mse_loss, +) +from .parser_import import parse_gdn_shared_prefix_segments + + +class ToyGdnConfig(BaseModel): + model_config = ConfigDict(frozen=True) + + hidden_size: int = Field(default=8, ge=1) + conv_width: int = Field(default=4, ge=2) + + +class ToyOracleMetrics(BaseModel): + model_config = ConfigDict(frozen=True) + + loss_mean_abs_pct: float + output_mean_abs_pct: float + hidden_grad_mean_abs_pct: float + param_grad_mean_abs_pct: float + + +class ToyStatefulGdn(torch.nn.Module): + """Small stateful block used to validate oracle mechanics on CPU. + + This is not a GDN approximation. It deliberately has the two state classes + that make GDN shared-prefix execution non-trivial: a finite conv tail and a + recurrent state. That is enough to prove parser routing, flattened + accumulation, and known-bad physical-stream sensitivity before the real FLA + kernels are invoked. + """ + + def __init__(self, config: ToyGdnConfig) -> None: + super().__init__() + self.config = config + self.in_proj = torch.nn.Linear(config.hidden_size, config.hidden_size) + self.gate_proj = torch.nn.Linear(config.hidden_size, config.hidden_size) + self.rec_proj = torch.nn.Linear( + config.hidden_size, config.hidden_size, bias=False + ) + self.out_proj = torch.nn.Linear(config.hidden_size, config.hidden_size) + self.conv_weight = torch.nn.Parameter( + torch.empty(config.hidden_size, config.conv_width) + ) + self.conv_bias = torch.nn.Parameter(torch.empty(config.hidden_size)) + self.reset_parameters() + + def reset_parameters(self) -> None: + torch.nn.init.normal_(self.conv_weight, mean=0.0, std=0.15) + torch.nn.init.normal_(self.conv_bias, mean=0.0, std=0.05) + for module in (self.in_proj, self.gate_proj, self.rec_proj, self.out_proj): + if hasattr(module, "reset_parameters"): + module.reset_parameters() + + def zero_conv_state(self, reference: Tensor) -> Tensor: + return reference.new_zeros( + self.config.hidden_size, + self.config.conv_width - 1, + ) + + def zero_recurrent_state(self, reference: Tensor) -> Tensor: + return reference.new_zeros(self.config.hidden_size) + + def forward_segment( + self, + hidden: Tensor, + *, + conv_initial: Tensor, + recurrent_initial: Tensor, + ) -> tuple[Tensor, Tensor, Tensor]: + projected = self.in_proj(hidden) + conv_input = torch.cat([conv_initial, projected.T], dim=1) + conv_out = F.conv1d( + conv_input.unsqueeze(0), + self.conv_weight.unsqueeze(1), + self.conv_bias, + padding=0, + groups=self.config.hidden_size, + ).squeeze(0) + conv_out = F.silu(conv_out.T) + conv_final = conv_input[:, -(self.config.conv_width - 1) :] + + recurrent = recurrent_initial + outputs = [] + gates = torch.sigmoid(self.gate_proj(hidden)) + for token_index in range(hidden.shape[0]): + recurrent = torch.tanh(recurrent + self.rec_proj(conv_out[token_index])) + outputs.append(self.out_proj(recurrent * gates[token_index])) + return torch.stack(outputs), conv_final, recurrent + + +def run_toy_packed( + module: ToyStatefulGdn, + hidden: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, +) -> Tensor: + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=1 + ) + output = torch.zeros_like(hidden) + for family in spec.families: + row = family.row_index + prefix_hidden = hidden[row, family.prefix.start : family.prefix.end] + prefix_out, prefix_conv, prefix_rec = module.forward_segment( + prefix_hidden, + conv_initial=module.zero_conv_state(hidden), + recurrent_initial=module.zero_recurrent_state(hidden), + ) + output[row, family.prefix.start : family.prefix.end] = prefix_out + for completion in family.completions: + suffix_hidden = hidden[row, completion.start : completion.end] + suffix_out, _, _ = module.forward_segment( + suffix_hidden, + conv_initial=prefix_conv, + recurrent_initial=prefix_rec, + ) + output[row, completion.start : completion.end] = suffix_out + return output + + +def run_toy_flattened_reference( + module: ToyStatefulGdn, + hidden: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, +) -> Tensor: + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=1 + ) + output = torch.zeros_like(hidden) + for family in spec.families: + row = family.row_index + prefix_hidden = hidden[row, family.prefix.start : family.prefix.end] + prefix_len = family.prefix.length + for child_index, completion in enumerate(family.completions): + suffix_hidden = hidden[row, completion.start : completion.end] + flattened = torch.cat([prefix_hidden, suffix_hidden], dim=0) + flat_out, _, _ = module.forward_segment( + flattened, + conv_initial=module.zero_conv_state(hidden), + recurrent_initial=module.zero_recurrent_state(hidden), + ) + if child_index == 0: + output[row, family.prefix.start : family.prefix.end] = flat_out[ + :prefix_len + ] + output[row, completion.start : completion.end] = flat_out[prefix_len:] + return output + + +def run_toy_physical_stream( + module: ToyStatefulGdn, + hidden: Tensor, + *, + group_ids: Tensor, +) -> Tensor: + output = torch.zeros_like(hidden) + for row in range(hidden.shape[0]): + valid_length = int((group_ids[row] != -1).sum().item()) + if valid_length == 0: + continue + row_out, _, _ = module.forward_segment( + hidden[row, :valid_length], + conv_initial=module.zero_conv_state(hidden), + recurrent_initial=module.zero_recurrent_state(hidden), + ) + output[row, :valid_length] = row_out + return output + + +def compare_toy_packed_to_flattened( + module: ToyStatefulGdn, + hidden: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, + assistant_mask: Tensor, +) -> ToyOracleMetrics: + packed_module = deepcopy(module) + flat_module = deepcopy(module) + packed_hidden = hidden.clone().detach().requires_grad_(True) + flat_hidden = hidden.clone().detach().requires_grad_(True) + + packed_out = run_toy_packed( + packed_module, + packed_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + flat_out = run_toy_flattened_reference( + flat_module, + flat_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + packed_loss = _masked_quadratic_loss(packed_out, assistant_mask) + flat_loss = _masked_quadratic_loss(flat_out, assistant_mask) + packed_loss.backward() + flat_loss.backward() + + return ToyOracleMetrics( + loss_mean_abs_pct=mean_abs_pct(flat_loss.detach(), packed_loss.detach()), + output_mean_abs_pct=mean_abs_pct(flat_out.detach(), packed_out.detach()), + hidden_grad_mean_abs_pct=mean_abs_pct( + _require_grad(flat_hidden), _require_grad(packed_hidden) + ), + param_grad_mean_abs_pct=parameter_grad_mean_abs_pct_with_name( + flat_module, packed_module + )[1], + ) + + +def compare_toy_packed_to_flattened_with_output_grad( + module: ToyStatefulGdn, + hidden: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, + output_grad: Tensor, +) -> ToyOracleMetrics: + packed_module = deepcopy(module) + flat_module = deepcopy(module) + packed_hidden = hidden.clone().detach().requires_grad_(True) + flat_hidden = hidden.clone().detach().requires_grad_(True) + + packed_out = run_toy_packed( + packed_module, + packed_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + flat_out = run_toy_flattened_reference( + flat_module, + flat_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + real_mask = group_ids != -1 + real_mask = ( + real_mask.unsqueeze(-1) + if output_grad.shape[:2] == real_mask.shape + else real_mask.transpose(0, 1).unsqueeze(-1) + ) + loss_denominator = real_mask.expand_as(output_grad).sum() + packed_loss = stable_output_mse_loss( + packed_out, + output_grad, + mask=real_mask, + denominator=loss_denominator, + ) + flat_loss = stable_output_mse_loss( + flat_out, + output_grad, + mask=real_mask, + denominator=loss_denominator, + ) + packed_loss.backward() + flat_loss.backward() + + return ToyOracleMetrics( + loss_mean_abs_pct=mean_abs_pct(flat_loss.detach(), packed_loss.detach()), + output_mean_abs_pct=mean_abs_pct(flat_out.detach(), packed_out.detach()), + hidden_grad_mean_abs_pct=mean_abs_pct( + _require_grad(flat_hidden), _require_grad(packed_hidden) + ), + param_grad_mean_abs_pct=parameter_grad_mean_abs_pct_with_name( + flat_module, packed_module + )[1], + ) + + +def _masked_quadratic_loss(output: Tensor, assistant_mask: Tensor) -> Tensor: + selected = output[assistant_mask] + if selected.numel() == 0: + raise ValueError("assistant_mask selects no tokens") + return selected.square().sum() + + +def _require_grad(tensor: Tensor) -> Tensor: + if tensor.grad is None: + raise AssertionError("expected tensor.grad to be populated") + return tensor.grad diff --git a/tests/integration/megatron/gdn_shared_prefix/packed_layout.py b/tests/integration/megatron/gdn_shared_prefix/packed_layout.py new file mode 100644 index 000000000..45a41ff58 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/packed_layout.py @@ -0,0 +1,263 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field +import torch + +from .cases import GdnPhase0Case +from .parser_import import GdnPackedExecutionSpec, parse_gdn_shared_prefix_segments + + +class GdnCaseSummary(BaseModel): + model_config = ConfigDict(frozen=True) + + name: str + total_tokens: int + family_count: int + completion_count: int + max_segment_length: int + suffix_shorter_than_conv: bool + suffix_equal_to_conv: bool + suffix_longer_than_conv: bool + cp_boundary_prefix: bool + cp_boundary_suffix: bool + family_boundary_at_partition: bool + empty_trailing_rank: bool + valid_lengths: tuple[int, ...] + + +def build_phase0_packed_tensors(case: GdnPhase0Case) -> dict[str, Any]: + shape = (len(case.rows), case.sequence_length) + generator = torch.Generator().manual_seed(case.seed) + tokens = torch.zeros(shape, dtype=torch.long) + group_ids = torch.full(shape, -1, dtype=torch.long) + parent_ids = torch.full(shape, -1, dtype=torch.long) + input_pos = torch.zeros(shape, dtype=torch.long) + assistant_mask = torch.zeros(shape, dtype=torch.bool) + logprobs = torch.full(shape, float("nan"), dtype=torch.float32) + advantages = torch.zeros(shape, dtype=torch.float32) + weights = torch.zeros(shape, dtype=torch.float32) + + for row_index, row in enumerate(case.rows): + cursor = 0 + next_group_id = row_index * 100_000 + for family in row.families: + required = family.prefix_length + sum(family.suffix_lengths) + if cursor + required > case.sequence_length: + raise ValueError( + f"case {case.name} row {row_index}: family requires {required} " + f"tokens with only {case.sequence_length - cursor} remaining" + ) + prefix_group_id = next_group_id + next_group_id += 1 + prefix_end = cursor + family.prefix_length + _write_tokens(tokens, row_index, cursor, prefix_end, generator) + group_ids[row_index, cursor:prefix_end] = prefix_group_id + parent_ids[row_index, cursor:prefix_end] = prefix_group_id + input_pos[row_index, cursor:prefix_end] = torch.arange( + family.prefix_length, dtype=torch.long + ) + cursor = prefix_end + + for suffix_length in family.suffix_lengths: + completion_group_id = next_group_id + next_group_id += 1 + suffix_end = cursor + suffix_length + _write_tokens(tokens, row_index, cursor, suffix_end, generator) + group_ids[row_index, cursor:suffix_end] = completion_group_id + parent_ids[row_index, cursor:suffix_end] = prefix_group_id + input_pos[row_index, cursor:suffix_end] = torch.arange( + family.prefix_length, + family.prefix_length + suffix_length, + dtype=torch.long, + ) + if suffix_length > 1: + trainable_start = cursor + 1 + assistant_mask[row_index, trainable_start:suffix_end] = True + logprobs[row_index, trainable_start:suffix_end] = _sample_logprobs( + suffix_length - 1, generator + ) + advantages[row_index, trainable_start:suffix_end] = ( + _sample_advantage(generator) + ) + weights[row_index, trainable_start:suffix_end] = 1.0 / ( + suffix_length - 1 + ) + cursor = suffix_end + + return { + "tokens": tokens, + "group_ids": group_ids, + "parent_ids": parent_ids, + "input_pos": input_pos, + "assistant_mask": assistant_mask, + "logprobs": logprobs, + "advantages": advantages, + "weights": weights, + "pixel_values": [None] * len(case.rows), + "image_grid_thw": [None] * len(case.rows), + } + + +def build_gdn_group_parent_tensors(case: GdnPhase0Case) -> dict[str, torch.Tensor]: + shape = (len(case.rows), case.sequence_length) + group_ids = torch.full(shape, -1, dtype=torch.long) + parent_ids = torch.full(shape, -1, dtype=torch.long) + for row_index, row in enumerate(case.rows): + cursor = 0 + next_group_id = row_index * 100_000 + for family in row.families: + required = family.prefix_length + sum(family.suffix_lengths) + if cursor + required > case.sequence_length: + raise ValueError( + f"case {case.name} row {row_index}: family requires {required} " + f"tokens with only {case.sequence_length - cursor} remaining" + ) + prefix_group_id = next_group_id + next_group_id += 1 + prefix_end = cursor + family.prefix_length + group_ids[row_index, cursor:prefix_end] = prefix_group_id + parent_ids[row_index, cursor:prefix_end] = prefix_group_id + cursor = prefix_end + for suffix_length in family.suffix_lengths: + completion_group_id = next_group_id + next_group_id += 1 + suffix_end = cursor + suffix_length + group_ids[row_index, cursor:suffix_end] = completion_group_id + parent_ids[row_index, cursor:suffix_end] = prefix_group_id + cursor = suffix_end + return {"group_ids": group_ids, "parent_ids": parent_ids} + + +def summarize_case( + case: GdnPhase0Case, + tensors: dict[str, Any], + *, + conv_width: int, + cp_sizes: tuple[int, ...] = (2, 4, 8), +) -> GdnCaseSummary: + spec = parse_gdn_shared_prefix_segments( + tensors["group_ids"], tensors["parent_ids"], min_completions_per_family=1 + ) + suffix_lengths = [ + segment.length for family in spec.families for segment in family.completions + ] + boundary = _boundary_flags(spec, cp_sizes) + return GdnCaseSummary( + name=case.name, + total_tokens=spec.real_token_count, + family_count=spec.family_count, + completion_count=spec.completion_count, + max_segment_length=spec.max_segment_length, + suffix_shorter_than_conv=any(length < conv_width for length in suffix_lengths), + suffix_equal_to_conv=any(length == conv_width for length in suffix_lengths), + suffix_longer_than_conv=any(length > conv_width for length in suffix_lengths), + cp_boundary_prefix=boundary["cp_boundary_prefix"], + cp_boundary_suffix=boundary["cp_boundary_suffix"], + family_boundary_at_partition=boundary["family_boundary_at_partition"], + empty_trailing_rank=boundary["empty_trailing_rank"], + valid_lengths=spec.valid_lengths, + ) + + +def format_case_summary(summary: GdnCaseSummary) -> str: + flags = [] + for name in ( + "suffix_shorter_than_conv", + "suffix_equal_to_conv", + "suffix_longer_than_conv", + "cp_boundary_prefix", + "cp_boundary_suffix", + "family_boundary_at_partition", + "empty_trailing_rank", + ): + if getattr(summary, name): + flags.append(name) + return ( + f"{summary.name}: tokens={summary.total_tokens} " + f"families={summary.family_count} completions={summary.completion_count} " + f"max_segment={summary.max_segment_length} flags={','.join(flags) or 'none'}" + ) + + +def _write_tokens( + tokens: torch.Tensor, + row_index: int, + start: int, + end: int, + generator: torch.Generator, +) -> None: + tokens[row_index, start:end] = torch.randint( + low=10, high=8192, size=(end - start,), dtype=torch.long, generator=generator + ) + + +def _sample_logprobs(length: int, generator: torch.Generator) -> torch.Tensor: + return ( + torch.randn((length,), generator=generator, dtype=torch.float32) * 0.25 - 1.75 + ) + + +def _sample_advantage(generator: torch.Generator) -> float: + return float( + (torch.randn((1,), generator=generator, dtype=torch.float32) * 0.5).item() + ) + + +def _boundary_flags( + spec: GdnPackedExecutionSpec, cp_sizes: tuple[int, ...] +) -> dict[str, bool]: + real_index: dict[int, int] = {} + cursor = 0 + for row_index, valid_length in enumerate(spec.valid_lengths): + for position in range(valid_length): + real_index[row_index * spec.sequence_length + position] = cursor + cursor += 1 + flags = { + "cp_boundary_prefix": False, + "cp_boundary_suffix": False, + "family_boundary_at_partition": False, + "empty_trailing_rank": False, + } + if spec.real_token_count == 0: + return flags + for cp_size in cp_sizes: + shard = (spec.real_token_count + cp_size - 1) // cp_size + boundaries = {shard * rank for rank in range(1, cp_size)} + if shard * (cp_size - 1) >= spec.real_token_count: + flags["empty_trailing_rank"] = True + for family in spec.families: + family_start = _segment_real_start(family.prefix, spec, real_index) + family_end = _segment_real_end(family.completions[-1], spec, real_index) + if family_start in boundaries or family_end in boundaries: + flags["family_boundary_at_partition"] = True + if _crosses_boundary(family.prefix, spec, real_index, boundaries): + flags["cp_boundary_prefix"] = True + for completion in family.completions: + if _crosses_boundary(completion, spec, real_index, boundaries): + flags["cp_boundary_suffix"] = True + return flags + + +def _segment_real_start( + segment: Any, spec: GdnPackedExecutionSpec, real_index: dict[int, int] +) -> int: + return real_index[segment.row_index * spec.sequence_length + segment.start] + + +def _segment_real_end( + segment: Any, spec: GdnPackedExecutionSpec, real_index: dict[int, int] +) -> int: + return real_index[segment.row_index * spec.sequence_length + segment.end - 1] + 1 + + +def _crosses_boundary( + segment: Any, + spec: GdnPackedExecutionSpec, + real_index: dict[int, int], + boundaries: set[int], +) -> bool: + start = _segment_real_start(segment, spec, real_index) + end = _segment_real_end(segment, spec, real_index) + return any(start < boundary < end for boundary in boundaries) diff --git a/tests/integration/megatron/gdn_shared_prefix/parser_import.py b/tests/integration/megatron/gdn_shared_prefix/parser_import.py new file mode 100644 index 000000000..ce184d96e --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/parser_import.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import sys +from types import ModuleType +from typing import Any + + +def _load_parser_module() -> ModuleType: + repo_root = Path(__file__).resolve().parents[4] + module_path = repo_root / "src/art/megatron/gdn/gdn_shared_prefix.py" + spec = importlib.util.spec_from_file_location( + "_art_gdn_shared_prefix_for_tests", module_path + ) + if spec is None or spec.loader is None: + raise RuntimeError(f"Failed to load parser module from {module_path}") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +_MODULE = _load_parser_module() + +GdnPackedExecutionSpec: Any = _MODULE.GdnPackedExecutionSpec +build_gdn_cp_segment_schedule: Any = _MODULE.build_gdn_cp_segment_schedule +build_gdn_rank_execution_plan: Any = _MODULE.build_gdn_rank_execution_plan +parse_gdn_shared_prefix_segments: Any = _MODULE.parse_gdn_shared_prefix_segments diff --git a/tests/integration/megatron/gdn_shared_prefix/real_gdn_oracle.py b/tests/integration/megatron/gdn_shared_prefix/real_gdn_oracle.py new file mode 100644 index 000000000..e69fef22b --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/real_gdn_oracle.py @@ -0,0 +1,939 @@ +from __future__ import annotations + +from typing import Any, Literal + +from pydantic import BaseModel, ConfigDict +import torch +from torch import Tensor +import torch.nn.functional as F + +from art.megatron.context_parallel.layout_index import TokenLayoutIndex +from art.megatron.gdn.gdn_shared_prefix import FLA_CHUNK_SIZE +from art.megatron.gdn.operator import ( + _apply_gated_rms_norm, + _chunk_gated_delta_rule, + _disable_reentrant_te_linear_transpose_cache, + _in_proj, + _l2norm, + _local_key_heads, + _local_value_dim, + _local_value_heads, + _out_proj, + _zero_conv_state, + _zero_recurrent_state, + gdn_shared_prefix_forward, +) + +from .layout_reference import build_test_gdn_cp_layout_plan +from .metrics import ( + mean_abs_pct, + parameter_grad_mean_abs_pct_with_name, + stable_output_mse_loss, +) +from .parser_import import parse_gdn_shared_prefix_segments + + +class RealGdnOracleMetrics(BaseModel): + model_config = ConfigDict(frozen=True) + + loss_mean_abs_pct: float + loss_abs_diff: float + output_mean_abs_pct: float + hidden_grad_mean_abs_pct: float + param_grad_mean_abs_pct: float + + +class GdnChainBoundaryDebug(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) + + family_index: int + segment_kind: str + child_index: int | None + boundary_kind: str + shard_index: int + token_offset: int + conv_initial: Tensor + recurrent_initial: Tensor + + +GdnChainMutation = Literal[ + "detach_prefix_state", "zero_conv_tail", "zero_recurrent_parent" +] + + +def compare_real_gdn_cp1_to_flattened( + *, + packed_gdn: Any, + flat_gdn: Any, + hidden_states: Tensor, + group_ids: Tensor, + parent_ids: Tensor, + assistant_mask: Tensor, +) -> RealGdnOracleMetrics: + packed_hidden = hidden_states.clone().detach().requires_grad_(True) + flat_hidden = hidden_states.clone().detach().requires_grad_(True) + + packed_out, _ = gdn_shared_prefix_forward( + packed_gdn, + packed_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + flat_out = run_real_gdn_flattened_reference( + flat_gdn, + flat_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + + packed_loss = _masked_quadratic_loss(packed_out, assistant_mask) + flat_loss = _masked_quadratic_loss(flat_out, assistant_mask) + packed_loss.backward() + flat_loss.backward() + + return RealGdnOracleMetrics( + loss_mean_abs_pct=mean_abs_pct(flat_loss.detach(), packed_loss.detach()), + loss_abs_diff=float( + (flat_loss.detach().float() - packed_loss.detach().float()).abs() + ), + output_mean_abs_pct=mean_abs_pct(flat_out.detach(), packed_out.detach()), + hidden_grad_mean_abs_pct=mean_abs_pct( + _require_grad(flat_hidden), _require_grad(packed_hidden) + ), + param_grad_mean_abs_pct=parameter_grad_mean_abs_pct_with_name( + flat_gdn, packed_gdn + )[1], + ) + + +def compare_real_gdn_cp1_to_flattened_with_output_grad( + *, + packed_gdn: Any, + flat_gdn: Any, + hidden_states: Tensor, + group_ids: Tensor, + parent_ids: Tensor, + output_grad: Tensor, +) -> RealGdnOracleMetrics: + packed_hidden = hidden_states.clone().detach().requires_grad_(True) + flat_hidden = hidden_states.clone().detach().requires_grad_(True) + + packed_out, _ = gdn_shared_prefix_forward( + packed_gdn, + packed_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + flat_out = run_real_gdn_flattened_reference( + flat_gdn, + flat_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + + real_mask = (group_ids != -1).transpose(0, 1).unsqueeze(-1) + loss_denominator = real_mask.expand_as(output_grad).sum() + packed_loss = stable_output_mse_loss( + packed_out, + output_grad, + mask=real_mask, + denominator=loss_denominator, + ) + flat_loss = stable_output_mse_loss( + flat_out, + output_grad, + mask=real_mask, + denominator=loss_denominator, + ) + packed_loss.backward() + flat_loss.backward() + + return RealGdnOracleMetrics( + loss_mean_abs_pct=mean_abs_pct(flat_loss.detach(), packed_loss.detach()), + loss_abs_diff=float( + (flat_loss.detach().float() - packed_loss.detach().float()).abs() + ), + output_mean_abs_pct=mean_abs_pct(flat_out.detach(), packed_out.detach()), + hidden_grad_mean_abs_pct=mean_abs_pct( + _require_grad(flat_hidden), _require_grad(packed_hidden) + ), + param_grad_mean_abs_pct=parameter_grad_mean_abs_pct_with_name( + flat_gdn, packed_gdn + )[1], + ) + + +def attach_main_grads(module: torch.nn.Module) -> None: + for parameter in module.parameters(): + if not hasattr(parameter, "main_grad"): + setattr(parameter, "main_grad", torch.zeros_like(parameter)) + + +def zero_parameter_grads(module: torch.nn.Module) -> None: + for parameter in module.parameters(): + parameter.grad = None + main_grad = getattr(parameter, "main_grad", None) + if main_grad is not None: + main_grad.zero_() + + +def _run_gdn_segment( + gdn: Any, + hidden_states: Tensor, + *, + conv_initial: Tensor, + recurrent_initial: Tensor, + output_final_state: bool = True, +) -> tuple[Tensor, Tensor | None, Tensor | None, Tensor | None]: + _disable_reentrant_te_linear_transpose_cache(gdn) + seq_len, batch_size, _ = hidden_states.shape + if int(conv_initial.shape[0]) != batch_size: + raise ValueError( + "conv_initial batch must match hidden_states batch, got " + f"{tuple(conv_initial.shape)} for hidden {tuple(hidden_states.shape)}" + ) + if int(recurrent_initial.shape[0]) != batch_size: + raise ValueError( + "recurrent_initial batch must match hidden_states batch, got " + f"{tuple(recurrent_initial.shape)} for hidden {tuple(hidden_states.shape)}" + ) + + qkvzba, _ = _in_proj(gdn, hidden_states) + qkvzba = qkvzba.transpose(0, 1) + qkv, gate, beta, alpha = torch.split( + qkvzba, + [ + (gdn.qk_dim * 2 + gdn.v_dim) // gdn.tp_size, + gdn.v_dim // gdn.tp_size, + gdn.num_value_heads // gdn.tp_size, + gdn.num_value_heads // gdn.tp_size, + ], + dim=-1, + ) + key_heads = _local_key_heads(gdn) + value_heads = _local_value_heads(gdn) + gate = gate.reshape(batch_size, seq_len, value_heads, gdn.value_head_dim) + beta = beta.reshape(batch_size, seq_len, value_heads) + alpha = alpha.reshape(batch_size, seq_len, value_heads) + + qkv = qkv.transpose(1, 2) + qkv, conv_final = _dense_causal_conv1d_with_state( + gdn, + qkv, + conv_initial, + output_final_state=output_final_state, + ) + qkv = qkv.transpose(1, 2) + + query, key, value = torch.split( + qkv, + [ + gdn.qk_dim // gdn.tp_size, + gdn.qk_dim // gdn.tp_size, + gdn.v_dim // gdn.tp_size, + ], + dim=-1, + ) + query = query.reshape(batch_size, seq_len, key_heads, gdn.key_head_dim) + key = key.reshape(batch_size, seq_len, key_heads, gdn.key_head_dim) + value = value.reshape(batch_size, seq_len, value_heads, gdn.value_head_dim) + if gdn.use_qk_l2norm: + query = _l2norm(query.contiguous()) + key = _l2norm(key.contiguous()) + if gdn.num_value_heads // gdn.num_key_heads > 1: + repeat = gdn.num_value_heads // gdn.num_key_heads + query = query.repeat_interleave(repeat, dim=2) + key = key.repeat_interleave(repeat, dim=2) + + g = -gdn.A_log.exp() * F.softplus(alpha.float() + gdn.dt_bias) + beta = beta.sigmoid() + recurrent_out, recurrent_final = _chunk_gated_delta_rule( + query.contiguous(), + key.contiguous(), + value.contiguous(), + g=g.contiguous(), + beta=beta.contiguous(), + initial_state=recurrent_initial, + output_final_state=output_final_state, + use_qk_l2norm_in_kernel=False, + ) + norm_out = _apply_gated_rms_norm(gdn, recurrent_out, gate.contiguous()) + norm_out = norm_out.reshape(batch_size, seq_len, _local_value_dim(gdn)) + norm_out = norm_out.transpose(0, 1).contiguous() + out, out_bias = _out_proj(gdn, norm_out) + return out, out_bias, conv_final, recurrent_final + + +def _dense_causal_conv1d_with_state( + gdn: Any, + qkv: Tensor, + conv_initial: Tensor, + *, + output_final_state: bool, +) -> tuple[Tensor, Tensor | None]: + weight = gdn.conv1d.weight.squeeze(1) + bias = gdn.conv1d.bias + dtype = qkv.dtype + extended = torch.cat([conv_initial, qkv], dim=-1) + out = F.conv1d( + extended, weight.unsqueeze(1), bias, padding=0, groups=extended.shape[1] + ) + out = gdn.act_fn(out[..., : qkv.shape[-1]]).to(dtype=dtype) + tail_width = int(weight.shape[1]) - 1 + final = ( + extended[..., -tail_width:].to(dtype=dtype) + if tail_width + else extended[..., :0].to(dtype=dtype) + ) + return out, final if output_final_state else None + + +def run_real_gdn_flattened_reference( + gdn: Any, + hidden_states: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, + execution_spec: Any | None = None, +) -> Tensor: + spec = execution_spec or parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=1 + ) + output = torch.zeros_like(hidden_states) + for family in spec.families: + row = family.row_index + prefix_hidden = hidden_states[ + family.prefix.start : family.prefix.end, row : row + 1, : + ] + prefix_len = family.prefix.length + for child_index, completion in enumerate(family.completions): + suffix_hidden = hidden_states[ + completion.start : completion.end, row : row + 1, : + ] + flat_hidden = torch.cat([prefix_hidden, suffix_hidden], dim=0) + flat_out, _, _, _ = _run_gdn_segment( + gdn, + flat_hidden, + conv_initial=_zero_conv_state(gdn, hidden_states, row), + recurrent_initial=_zero_recurrent_state(gdn, hidden_states, row), + output_final_state=False, + ) + if child_index == 0: + output[family.prefix.start : family.prefix.end, row : row + 1, :] = ( + flat_out[:prefix_len] + ) + output[completion.start : completion.end, row : row + 1, :] = flat_out[ + prefix_len: + ] + return output + + +def run_real_gdn_physical_stream( + gdn: Any, + hidden_states: Tensor, + *, + group_ids: Tensor, +) -> Tensor: + output = torch.zeros_like(hidden_states) + for row in range(hidden_states.shape[1]): + valid_length = int((group_ids[row] != -1).sum().item()) + if valid_length == 0: + continue + row_out, _, _, _ = _run_gdn_segment( + gdn, + hidden_states[:valid_length, row : row + 1, :], + conv_initial=_zero_conv_state(gdn, hidden_states, row), + recurrent_initial=_zero_recurrent_state(gdn, hidden_states, row), + output_final_state=False, + ) + output[:valid_length, row : row + 1, :] = row_out + return output + + +def run_real_gdn_local_fork_reference( + gdn: Any, + hidden_states: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, + cp_size: int, + attention_token_layout_index: TokenLayoutIndex | None = None, +) -> Tensor: + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + gdn_token_indices_by_rank = _split_gdn_families_by_rank(spec, cp_size=cp_size) + gdn_token_ranges_by_rank = _rank_ranges_from_tokens_by_rank( + gdn_token_indices_by_rank + ) + plan = build_test_gdn_cp_layout_plan( + group_ids=group_ids, + parent_ids=parent_ids, + cp_size=cp_size, + attention_token_layout_index=attention_token_layout_index, + gdn_token_ranges_by_rank=gdn_token_ranges_by_rank, + ) + flat_hidden = hidden_states.transpose(0, 1).reshape(-1, hidden_states.shape[-1]) + attention_inputs = _rank_tensors_from_flat( + flat_hidden, _tokens_by_rank_from_ranges(plan.attention_token_ranges_by_rank) + ) + gdn_inputs = _simulate_all_to_all_single(attention_inputs, plan.attention_to_gdn) + gdn_outputs = tuple( + _run_local_fork_rank(gdn, rank_hidden, spec, local_token_indices) + for rank_hidden, local_token_indices in zip( + gdn_inputs, + _tokens_by_rank_from_ranges(plan.gdn_token_ranges_by_rank), + strict=True, + ) + ) + attention_outputs = _simulate_all_to_all_single(gdn_outputs, plan.gdn_to_attention) + flat_output = flat_hidden.new_zeros(flat_hidden.shape) + for rank_output, token_indices in zip( + attention_outputs, + _tokens_by_rank_from_ranges(plan.attention_token_ranges_by_rank), + strict=True, + ): + if token_indices: + index = torch.tensor( + token_indices, device=rank_output.device, dtype=torch.long + ) + flat_output = flat_output.index_copy(0, index, rank_output) + return ( + flat_output.reshape(group_ids.shape[0], group_ids.shape[1], -1) + .transpose(0, 1) + .contiguous() + ) + + +def _split_gdn_families_by_rank( + spec: Any, + *, + cp_size: int, +) -> tuple[tuple[int, ...], ...]: + if cp_size < 1: + raise ValueError(f"cp_size must be >= 1, got {cp_size}") + ranks: list[list[int]] = [[] for _ in range(cp_size)] + loads = [0] * cp_size + for family in spec.families: + rank = min(range(cp_size), key=lambda index: (loads[index], index)) + family_tokens = tuple( + token + for segment in (family.prefix, *family.completions) + for token in segment.linear_indices(spec.sequence_length) + ) + ranks[rank].extend(family_tokens) + loads[rank] += len(family_tokens) + return tuple(tuple(rank_tokens) for rank_tokens in ranks) + + +def _simulate_all_to_all_single( + tensors_by_rank: tuple[Tensor, ...], + plan: Any, +) -> tuple[Tensor, ...]: + if len(tensors_by_rank) != int(plan.cp_size): + raise ValueError( + f"expected {plan.cp_size} rank tensors, got {len(tensors_by_rank)}" + ) + sample = next((tensor for tensor in tensors_by_rank if tensor.numel()), None) + if sample is None: + sample = tensors_by_rank[0] + outputs = [] + for dest_rank in range(int(plan.cp_size)): + pieces: list[Tensor | None] = [ + None for _ in range(int(plan.dest_token_counts_by_rank[dest_rank])) + ] + for transfer in plan.transfers: + if int(transfer.dest_rank) != dest_rank: + continue + source_tensor = tensors_by_rank[int(transfer.source_rank)] + source_positions = _transfer_positions( + transfer.source_positions_tensor, + count=int(transfer.token_count), + ) + dest_positions = _transfer_positions( + transfer.dest_positions_tensor, + count=int(transfer.token_count), + ) + for source_position, dest_position in zip( + source_positions, + dest_positions, + strict=True, + ): + pieces[dest_position] = source_tensor[source_position] + if not pieces: + outputs.append(sample.new_empty((0, *sample.shape[1:]))) + continue + if any(piece is None for piece in pieces): + raise RuntimeError( + f"exchange plan left holes for destination rank {dest_rank}" + ) + outputs.append(torch.stack([piece for piece in pieces if piece is not None])) + return tuple(outputs) + + +def _transfer_positions(tensor: Tensor | None, *, count: int) -> tuple[int, ...]: + if tensor is None: + return tuple(range(count)) + return tuple(int(value) for value in tensor.cpu().tolist()) + + +def _rank_ranges_from_tokens_by_rank( + tokens_by_rank: tuple[tuple[int, ...], ...], +) -> tuple[tuple[tuple[int, int, int], ...], ...]: + return tuple(_rank_ranges_from_tokens(tokens) for tokens in tokens_by_rank) + + +def _rank_ranges_from_tokens( + tokens: tuple[int, ...], +) -> tuple[tuple[int, int, int], ...]: + if not tokens: + return () + ranges = [] + start = tokens[0] + end = start + 1 + position = 0 + for local_position, token in enumerate(tokens[1:], start=1): + if token == end: + end += 1 + continue + ranges.append((start, end, position)) + start = token + end = token + 1 + position = local_position + ranges.append((start, end, position)) + return tuple(ranges) + + +def _tokens_by_rank_from_ranges( + ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...], +) -> tuple[tuple[int, ...], ...]: + return tuple( + tuple(token for start, end, _ in ranges for token in range(start, end)) + for ranges in ranges_by_rank + ) + + +def run_real_gdn_suffix_only_chain_reference( + gdn: Any, + hidden_states: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, + cp_size: int, + mutation: GdnChainMutation | None = None, + boundary_debug: list[GdnChainBoundaryDebug] | None = None, +) -> Tensor: + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + output = torch.zeros_like(hidden_states) + for family in spec.families: + row = family.row_index + zero_conv = _zero_conv_state(gdn, hidden_states, batch_size=1) + zero_rec = _zero_recurrent_state(gdn, hidden_states, batch_size=1) + prefix_hidden = hidden_states[ + family.prefix.start : family.prefix.end, row : row + 1, : + ] + prefix_out, prefix_conv, prefix_rec = _run_gdn_segment_suffix_only_chain_shards( + gdn, + prefix_hidden, + segment=family.prefix, + cp_size=cp_size, + conv_initial=zero_conv, + recurrent_initial=zero_rec, + mutation=mutation, + boundary_debug=boundary_debug, + ) + output[family.prefix.start : family.prefix.end, row : row + 1, :] = prefix_out + completion_conv = prefix_conv + completion_rec = prefix_rec + if mutation == "detach_prefix_state": + completion_conv = completion_conv.detach() + completion_rec = completion_rec.detach() + for completion in family.completions: + completion_hidden = hidden_states[ + completion.start : completion.end, row : row + 1, : + ] + completion_out, _, _ = _run_gdn_segment_suffix_only_chain_shards( + gdn, + completion_hidden, + segment=completion, + cp_size=cp_size, + conv_initial=completion_conv, + recurrent_initial=completion_rec, + mutation=mutation, + boundary_debug=boundary_debug, + ) + output[completion.start : completion.end, row : row + 1, :] = completion_out + return output + + +def run_real_gdn_chunk_native_reference( + gdn: Any, + hidden_states: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, +) -> Tensor: + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + output = torch.zeros_like(hidden_states) + for family in spec.families: + _scatter_family_output( + output, + family, + _run_gdn_family_chunk_native(gdn, hidden_states, family), + ) + return output + + +def run_real_gdn_mixed_cp_reference( + gdn: Any, + hidden_states: Tensor, + *, + group_ids: Tensor, + parent_ids: Tensor, + cp_size: int, + local_fork_max_tokens: int, +) -> Tensor: + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + output = torch.zeros_like(hidden_states) + local_count = 0 + chain_count = 0 + for family in spec.families: + if family.token_count <= local_fork_max_tokens: + local_count += 1 + _scatter_family_output( + output, + family, + _run_gdn_family_local_fork(gdn, hidden_states, family), + ) + continue + chain_count += 1 + _scatter_family_output( + output, + family, + _run_gdn_family_chunk_native(gdn, hidden_states, family), + ) + if local_count == 0 or chain_count == 0: + raise ValueError("mixed CP reference requires both local-fork and chain work") + return output + + +def _run_gdn_family_chunk_native( + gdn: Any, + hidden_states: Tensor, + family: Any, +) -> Tensor: + row = family.row_index + prefix = family.prefix + boundary_length = (prefix.length // FLA_CHUNK_SIZE) * FLA_CHUNK_SIZE + boundary_end = prefix.start + boundary_length + output = hidden_states.new_zeros((family.token_count, 1, hidden_states.shape[-1])) + boundary_conv = _zero_conv_state(gdn, hidden_states, batch_size=1) + boundary_rec = _zero_recurrent_state(gdn, hidden_states, batch_size=1) + if boundary_length: + boundary_out, _, boundary_conv, boundary_rec = _run_gdn_segment( + gdn, + hidden_states[prefix.start : boundary_end, row : row + 1, :], + conv_initial=boundary_conv, + recurrent_initial=boundary_rec, + output_final_state=True, + ) + if boundary_conv is None or boundary_rec is None: + raise RuntimeError("chunk-native boundary must return final states") + output[:boundary_length] = boundary_out + tail_hidden = hidden_states[boundary_end : prefix.end, row : row + 1, :] + if not family.completions: + if tail_hidden.numel(): + tail_out, _, _, _ = _run_gdn_segment( + gdn, + tail_hidden, + conv_initial=boundary_conv, + recurrent_initial=boundary_rec, + output_final_state=False, + ) + output[boundary_length : boundary_length + int(tail_hidden.shape[0])] = ( + tail_out + ) + return output + cursor = prefix.length + for completion in family.completions: + completion_hidden = hidden_states[ + completion.start : completion.end, row : row + 1, : + ] + segment_hidden = torch.cat((tail_hidden, completion_hidden), dim=0) + segment_out, _, _, _ = _run_gdn_segment( + gdn, + segment_hidden, + conv_initial=boundary_conv, + recurrent_initial=boundary_rec, + output_final_state=False, + ) + tail_length = int(tail_hidden.shape[0]) + if completion.child_index == 0 and tail_length: + output[boundary_length : prefix.length] = segment_out[:tail_length] + next_cursor = cursor + completion.length + output[cursor:next_cursor] = segment_out[tail_length:] + cursor = next_cursor + return output + + +def _masked_quadratic_loss(output: Tensor, assistant_mask: Tensor) -> Tensor: + selected = output.transpose(0, 1)[assistant_mask] + if selected.numel() == 0: + raise ValueError("assistant_mask selects no tokens") + return selected.square().sum() + + +def _run_local_fork_rank( + gdn: Any, + rank_hidden: Tensor, + spec: Any, + local_token_indices: tuple[int, ...], +) -> Tensor: + if not local_token_indices: + return rank_hidden.new_empty(rank_hidden.shape) + local_group_ids, local_parent_ids = _local_fork_group_tensors( + spec, local_token_indices, device=rank_hidden.device + ) + local_output, _ = gdn_shared_prefix_forward( + gdn, + rank_hidden.unsqueeze(1).contiguous(), + group_ids=local_group_ids, + parent_ids=local_parent_ids, + ) + return local_output.squeeze(1) + + +def _run_gdn_family_local_fork( + gdn: Any, + hidden_states: Tensor, + family: Any, +) -> Tensor: + row = family.row_index + segments = (family.prefix, *family.completions) + local_hidden = torch.cat( + [ + hidden_states[segment.start : segment.end, row : row + 1, :] + for segment in segments + ], + dim=0, + ) + local_group_ids, local_parent_ids = _family_group_tensors( + family, device=hidden_states.device + ) + local_output, _ = gdn_shared_prefix_forward( + gdn, + local_hidden, + group_ids=local_group_ids, + parent_ids=local_parent_ids, + ) + return local_output + + +def _scatter_family_output(output: Tensor, family: Any, family_output: Tensor) -> None: + row = family.row_index + cursor = 0 + for segment in (family.prefix, *family.completions): + next_cursor = cursor + segment.length + output[segment.start : segment.end, row : row + 1, :] = family_output[ + cursor:next_cursor + ] + cursor = next_cursor + + +def _family_group_tensors( + family: Any, + *, + device: torch.device, +) -> tuple[Tensor, Tensor]: + group_ids = [] + parent_ids = [] + prefix_group_id = 0 + group_ids.extend([prefix_group_id] * family.prefix.length) + parent_ids.extend([prefix_group_id] * family.prefix.length) + next_group_id = 1 + for completion in family.completions: + group_ids.extend([next_group_id] * completion.length) + parent_ids.extend([prefix_group_id] * completion.length) + next_group_id += 1 + return ( + torch.tensor([group_ids], device=device, dtype=torch.long), + torch.tensor([parent_ids], device=device, dtype=torch.long), + ) + + +def _run_gdn_segment_suffix_only_chain_shards( + gdn: Any, + hidden_states: Tensor, + *, + segment: Any, + cp_size: int, + conv_initial: Tensor, + recurrent_initial: Tensor, + mutation: GdnChainMutation | None, + boundary_debug: list[GdnChainBoundaryDebug] | None, +) -> tuple[Tensor, Tensor, Tensor]: + outputs = [] + conv_state = conv_initial + recurrent_state = recurrent_initial + for shard_index, (start, end) in enumerate( + _non_empty_shard_offsets(segment.length, cp_size) + ): + shard_conv = conv_state + shard_rec = recurrent_state + if mutation == "zero_conv_tail" and shard_index > 0: + shard_conv = torch.zeros_like(shard_conv) + if ( + mutation == "zero_recurrent_parent" + and segment.kind == "completion" + and shard_index == 0 + ): + shard_rec = torch.zeros_like(shard_rec) + _capture_chain_boundary( + boundary_debug, + segment=segment, + shard_index=shard_index, + token_offset=start, + conv_initial=shard_conv, + recurrent_initial=shard_rec, + ) + shard_out, _, conv_final, recurrent_final = _run_gdn_segment( + gdn, + hidden_states[start:end], + conv_initial=shard_conv, + recurrent_initial=shard_rec, + output_final_state=True, + ) + if conv_final is None or recurrent_final is None: + raise RuntimeError("GDN chain shards require final states") + outputs.append(shard_out) + conv_state = conv_final + recurrent_state = recurrent_final + if not outputs: + raise ValueError("GDN chain segment must contain at least one token") + return torch.cat(outputs, dim=0), conv_state, recurrent_state + + +def _capture_chain_boundary( + boundary_debug: list[GdnChainBoundaryDebug] | None, + *, + segment: Any, + shard_index: int, + token_offset: int, + conv_initial: Tensor, + recurrent_initial: Tensor, +) -> None: + if boundary_debug is None: + return + is_parent_boundary = segment.kind == "completion" and shard_index == 0 + is_shard_boundary = shard_index > 0 + if not is_parent_boundary and not is_shard_boundary: + return + if conv_initial.requires_grad: + conv_initial.retain_grad() + if recurrent_initial.requires_grad: + recurrent_initial.retain_grad() + boundary_debug.append( + GdnChainBoundaryDebug( + family_index=segment.family_index, + segment_kind=segment.kind, + child_index=segment.child_index, + boundary_kind="parent" if is_parent_boundary else "shard", + shard_index=shard_index, + token_offset=token_offset, + conv_initial=conv_initial, + recurrent_initial=recurrent_initial, + ) + ) + + +def _non_empty_shard_offsets( + length: int, + cp_size: int, +) -> tuple[tuple[int, int], ...]: + if cp_size < 1: + raise ValueError(f"cp_size must be >= 1, got {cp_size}") + return tuple( + (start, end) + for rank in range(cp_size) + for start, end in [ + ((length * rank) // cp_size, (length * (rank + 1)) // cp_size) + ] + if start < end + ) + + +def _local_fork_group_tensors( + spec: Any, + local_token_indices: tuple[int, ...], + *, + device: torch.device, +) -> tuple[Tensor, Tensor]: + local_position = { + token_index: position + for position, token_index in enumerate(local_token_indices) + } + group_ids = torch.full( + (len(local_token_indices),), -1, device=device, dtype=torch.long + ) + parent_ids = torch.full_like(group_ids, -1) + next_group_id = 0 + for family in spec.families: + family_segments = (family.prefix, *family.completions) + family_tokens = tuple( + token_index + for segment in family_segments + for token_index in segment.linear_indices(spec.sequence_length) + ) + token_is_local = tuple( + token_index in local_position for token_index in family_tokens + ) + if not any(token_is_local): + continue + if not all(token_is_local): + raise ValueError("local-fork execution requires whole prompt families") + + prefix_group_id = next_group_id + next_group_id += 1 + for token_index in family.prefix.linear_indices(spec.sequence_length): + position = local_position[token_index] + group_ids[position] = prefix_group_id + parent_ids[position] = prefix_group_id + for completion in family.completions: + child_group_id = next_group_id + next_group_id += 1 + for token_index in completion.linear_indices(spec.sequence_length): + position = local_position[token_index] + group_ids[position] = child_group_id + parent_ids[position] = prefix_group_id + if torch.any(group_ids == -1): + raise RuntimeError("local-fork metadata left unassigned token rows") + return group_ids.unsqueeze(0), parent_ids.unsqueeze(0) + + +def _rank_tensors_from_flat( + flat: Tensor, + indices_by_rank: tuple[tuple[int, ...], ...], +) -> tuple[Tensor, ...]: + return tuple( + flat.index_select( + 0, + torch.tensor(indices, device=flat.device, dtype=torch.long), + ) + for indices in indices_by_rank + ) + + +def _require_grad(tensor: Tensor) -> Tensor: + if tensor.grad is None: + raise AssertionError("expected tensor.grad to be populated") + return tensor.grad + + +def parameter_grad_mean_abs_pct(left: torch.nn.Module, right: torch.nn.Module) -> float: + return parameter_grad_mean_abs_pct_with_name(left, right)[1] diff --git a/tests/integration/megatron/gdn_shared_prefix/test_fla_cp_native_recurrent.py b/tests/integration/megatron/gdn_shared_prefix/test_fla_cp_native_recurrent.py new file mode 100644 index 000000000..bcf3a0cfb --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_fla_cp_native_recurrent.py @@ -0,0 +1,527 @@ +from __future__ import annotations + +from pathlib import Path +import socket +from typing import Any, cast + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("fla.ops.gated_delta_rule") + +from fla.ops.gated_delta_rule import chunk_gated_delta_rule # noqa: E402 +from torch.distributed import destroy_process_group, init_process_group # noqa: E402 +import torch.multiprocessing as mp # noqa: E402 +import torch.nn.functional as F # noqa: E402 + +from art.megatron.gdn.fla_cp import ( # noqa: E402 + _apply_summary, + _fwd_summary, + chunk_gated_delta_rule_native_cp, +) + +from .metrics import GDN_CORRECTNESS_DTYPE, assert_mean_abs_pct # noqa: E402 + +_CP_SIZES = ( + 2, + 4, + pytest.param( + 8, + marks=pytest.mark.skipif( + torch.cuda.device_count() < 8, + reason="At least eight CUDA devices are required for CP8 coverage.", + ), + ), +) + + +@pytest.mark.skipif( + not torch.cuda.is_available() or torch.cuda.device_count() < 4, + reason="At least four CUDA devices are required for native FLA CP coverage.", +) +@pytest.mark.parametrize("cp_size", _CP_SIZES) +def test_native_fla_cp_recurrent_matches_single_rank( + cp_size: int, tmp_path: Path +) -> None: + port = _find_free_port() + mp.spawn( + _native_fla_cp_worker, + args=(cp_size, port, str(tmp_path)), + nprocs=cp_size, + join=True, + ) + for rank in range(cp_size): + assert (tmp_path / f"rank_{rank}.ok").read_text() == "ok\n" + + +@pytest.mark.skipif( + not torch.cuda.is_available() or torch.cuda.device_count() < 4, + reason="At least four CUDA devices are required for native FLA CP coverage.", +) +@pytest.mark.parametrize("cp_size", (2, 4)) +def test_native_fla_cp_recurrent_varlen_multichain_matches_single_rank( + cp_size: int, tmp_path: Path +) -> None: + port = _find_free_port() + mp.spawn( + _native_fla_cp_varlen_multichain_worker, + args=(cp_size, port, str(tmp_path)), + nprocs=cp_size, + join=True, + ) + for rank in range(cp_size): + assert (tmp_path / f"varlen_rank_{rank}.ok").read_text() == "ok\n" + + +@pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA is required for FLA summary kernels.", +) +def test_native_fla_summary_affine_debug_matches_final_state() -> None: + from fla.ops.common.chunk_scaled_dot_kkt import chunk_scaled_dot_kkt_fwd + from fla.ops.gated_delta_rule.wy_fast import recompute_w_u_fwd + from fla.ops.utils import chunk_local_cumsum, solve_tril + + chunk_local_cumsum = cast(Any, chunk_local_cumsum) + chunk_scaled_dot_kkt_fwd = cast(Any, chunk_scaled_dot_kkt_fwd) + recompute_w_u_fwd = cast(Any, recompute_w_u_fwd) + solve_tril = cast(Any, solve_tril) + q, k, v, g, beta, h0, _, _ = _case_tensors_without_dist(cp_size=1) + g_cumsum = chunk_local_cumsum(g, chunk_size=64) + a = chunk_scaled_dot_kkt_fwd( + k=k, + g=g_cumsum, + beta=beta, + output_dtype=GDN_CORRECTNESS_DTYPE, + ) + a = solve_tril(A=a, output_dtype=k.dtype) + w, u = recompute_w_u_fwd(k=k, v=v, beta=beta, A=a, g=g_cumsum) + summary = _fwd_summary(k=k, w=w, u=u, g=g_cumsum) + + _, ref_ht = chunk_gated_delta_rule( + q, + k, + v, + g=g, + beta=beta, + initial_state=h0, + output_final_state=True, + use_qk_l2norm_in_kernel=False, + ) + assert ref_ht is not None + assert_mean_abs_pct( + ref_ht, + _apply_summary(summary, h0[0]).unsqueeze(0), + "summary_final_state", + ) + + +def _native_fla_cp_worker( + rank: int, + cp_size: int, + port: int, + output_dir: str, +) -> None: + torch.cuda.set_device(rank) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{port}", + rank=rank, + world_size=cp_size, + ) + try: + q, k, v, g, beta, h0, output_grad, ht_grad = _case_tensors(cp_size) + q_ref = q.clone().detach().requires_grad_(True) + k_ref = k.clone().detach().requires_grad_(True) + v_ref = v.clone().detach().requires_grad_(True) + g_ref = g.clone().detach().requires_grad_(True) + beta_ref = beta.clone().detach().requires_grad_(True) + h0_ref = h0.clone().detach().requires_grad_(True) + + ref_out, ref_ht = chunk_gated_delta_rule( + q_ref, + k_ref, + v_ref, + g=g_ref, + beta=beta_ref, + initial_state=h0_ref, + output_final_state=True, + use_qk_l2norm_in_kernel=False, + ) + assert ref_ht is not None + ref_loss = (ref_out * output_grad).sum() + (ref_ht * ht_grad).sum() + ref_loss.backward() + + start = (q.shape[1] * rank) // cp_size + end = (q.shape[1] * (rank + 1)) // cp_size + local_grad = output_grad[:, start:end].contiguous() + q_local = q[:, start:end].clone().detach().requires_grad_(True) + k_local = k[:, start:end].clone().detach().requires_grad_(True) + v_local = v[:, start:end].clone().detach().requires_grad_(True) + g_local = g[:, start:end].clone().detach().requires_grad_(True) + beta_local = beta[:, start:end].clone().detach().requires_grad_(True) + h0_local = h0.clone().detach().requires_grad_(True) + + cp_out, cp_ht = chunk_gated_delta_rule_native_cp( + q_local, + k_local, + v_local, + g=g_local, + beta=beta_local, + initial_state=h0_local, + group=torch.distributed.group.WORLD, + output_final_state=True, + lengths_by_rank_cpu=torch.full( + (cp_size, 1), int(q_local.shape[1]), dtype=torch.long + ), + ) + assert cp_ht is not None + cp_loss = (cp_out * local_grad).sum() + (cp_ht * (ht_grad / cp_size)).sum() + cp_loss.backward() + + assert_mean_abs_pct(ref_out[:, start:end], cp_out, "output") + assert_mean_abs_pct(ref_ht, cp_ht, "final_state") + assert q_ref.grad is not None + assert k_ref.grad is not None + assert v_ref.grad is not None + assert g_ref.grad is not None + assert beta_ref.grad is not None + assert h0_ref.grad is not None + _assert_grad_close(q_local, q_ref.grad[:, start:end], "q") + _assert_grad_close(k_local, k_ref.grad[:, start:end], "k") + _assert_grad_close(v_local, v_ref.grad[:, start:end], "v") + _assert_grad_close(g_local, g_ref.grad[:, start:end], "g") + _assert_grad_close(beta_local, beta_ref.grad[:, start:end], "beta") + _assert_grad_close(h0_local, h0_ref.grad, "h0") + Path(output_dir, f"rank_{rank}.ok").write_text("ok\n") + finally: + destroy_process_group() + + +def _native_fla_cp_varlen_multichain_worker( + rank: int, + cp_size: int, + port: int, + output_dir: str, +) -> None: + torch.cuda.set_device(rank) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{port}", + rank=rank, + world_size=cp_size, + ) + try: + q, k, v, g, beta, h0, output_grad, ht_grad, cu = _varlen_case_tensors(cp_size) + q_ref = q.clone().detach().requires_grad_(True) + k_ref = k.clone().detach().requires_grad_(True) + v_ref = v.clone().detach().requires_grad_(True) + g_ref = g.clone().detach().requires_grad_(True) + beta_ref = beta.clone().detach().requires_grad_(True) + h0_ref = h0.clone().detach().requires_grad_(True) + + ref_out, ref_ht = chunk_gated_delta_rule( + q_ref, + k_ref, + v_ref, + g=g_ref, + beta=beta_ref, + initial_state=h0_ref, + output_final_state=True, + use_qk_l2norm_in_kernel=False, + cu_seqlens=cu, + ) + assert ref_ht is not None + ref_loss = (ref_out * output_grad).sum() + (ref_ht * ht_grad).sum() + ref_loss.backward() + + local_slices = _rank_varlen_slices(cu, rank=rank, cp_size=cp_size) + q_local = ( + _cat_varlen_slices(q, local_slices).clone().detach().requires_grad_(True) + ) + k_local = ( + _cat_varlen_slices(k, local_slices).clone().detach().requires_grad_(True) + ) + v_local = ( + _cat_varlen_slices(v, local_slices).clone().detach().requires_grad_(True) + ) + g_local = ( + _cat_varlen_slices(g, local_slices).clone().detach().requires_grad_(True) + ) + beta_local = ( + _cat_varlen_slices(beta, local_slices).clone().detach().requires_grad_(True) + ) + h0_local = h0.clone().detach().requires_grad_(True) + local_grad = _cat_varlen_slices(output_grad, local_slices).contiguous() + local_cu_cpu = _local_cu_seqlens_cpu(local_slices) + local_cu = local_cu_cpu.to(device=q.device) + + cp_out, cp_ht = chunk_gated_delta_rule_native_cp( + q_local, + k_local, + v_local, + g=g_local, + beta=beta_local, + initial_state=h0_local, + cu_seqlens=local_cu, + cu_seqlens_cpu=local_cu_cpu, + lengths_by_rank_cpu=_lengths_by_rank_cpu(cu, cp_size=cp_size), + group=torch.distributed.group.WORLD, + output_final_state=True, + ) + assert cp_ht is not None + cp_loss = (cp_out * local_grad).sum() + (cp_ht * (ht_grad / cp_size)).sum() + cp_loss.backward() + + assert_mean_abs_pct( + _cat_varlen_slices(ref_out, local_slices), + cp_out, + "varlen_output", + ) + assert_mean_abs_pct(ref_ht, cp_ht, "varlen_final_state") + assert q_ref.grad is not None + assert k_ref.grad is not None + assert v_ref.grad is not None + assert g_ref.grad is not None + assert beta_ref.grad is not None + assert h0_ref.grad is not None + _assert_grad_close(q_local, _cat_varlen_slices(q_ref.grad, local_slices), "q") + _assert_grad_close(k_local, _cat_varlen_slices(k_ref.grad, local_slices), "k") + _assert_grad_close(v_local, _cat_varlen_slices(v_ref.grad, local_slices), "v") + _assert_grad_close(g_local, _cat_varlen_slices(g_ref.grad, local_slices), "g") + _assert_grad_close( + beta_local, + _cat_varlen_slices(beta_ref.grad, local_slices), + "beta", + ) + _assert_grad_close(h0_local, h0_ref.grad, "h0") + Path(output_dir, f"varlen_rank_{rank}.ok").write_text("ok\n") + finally: + destroy_process_group() + + +def _case_tensors(cp_size: int) -> tuple[torch.Tensor, ...]: + tensors = _case_tensors_without_dist(cp_size=cp_size) + for tensor in tensors: + torch.distributed.broadcast(tensor, src=0) + return tensors + + +def _case_tensors_without_dist(cp_size: int) -> tuple[torch.Tensor, ...]: + device = torch.device("cuda") + generator = torch.Generator(device=device).manual_seed(20450426 + cp_size) + token_count = 64 * max(cp_size, 1) + q = F.normalize( + torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ), + p=2, + dim=-1, + ) + k = F.normalize( + torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ), + p=2, + dim=-1, + ) + v = torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + g = -torch.rand( + 1, + token_count, + 2, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + beta = torch.rand( + 1, + token_count, + 2, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ).sigmoid() + h0 = torch.randn( + 1, 2, 8, 8, device=device, dtype=GDN_CORRECTNESS_DTYPE, generator=generator + ) + output_grad = torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + ht_grad = torch.randn( + 1, 2, 8, 8, device=device, dtype=GDN_CORRECTNESS_DTYPE, generator=generator + ) + return q, k, v, g, beta, h0, output_grad, ht_grad + + +def _varlen_case_tensors(cp_size: int) -> tuple[torch.Tensor, ...]: + tensors = _varlen_case_tensors_without_dist(cp_size=cp_size) + for tensor in tensors: + torch.distributed.broadcast(tensor, src=0) + return tensors + + +def _varlen_case_tensors_without_dist(cp_size: int) -> tuple[torch.Tensor, ...]: + device = torch.device("cuda") + generator = torch.Generator(device=device).manual_seed(20480426 + cp_size) + lengths = (128 * cp_size, 192 * cp_size, 256 * cp_size) + token_count = sum(lengths) + q = F.normalize( + torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ), + p=2, + dim=-1, + ) + k = F.normalize( + torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ), + p=2, + dim=-1, + ) + v = torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + g = -torch.rand( + 1, + token_count, + 2, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + beta = torch.rand( + 1, + token_count, + 2, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ).sigmoid() + h0 = torch.randn( + len(lengths), + 2, + 8, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + output_grad = torch.randn( + 1, + token_count, + 2, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + ht_grad = torch.randn( + len(lengths), + 2, + 8, + 8, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + cu = torch.tensor( + [0, *torch.cumsum(torch.tensor(lengths), dim=0).tolist()], + device=device, + dtype=torch.long, + ) + return q, k, v, g, beta, h0, output_grad, ht_grad, cu + + +def _rank_varlen_slices( + cu_seqlens: torch.Tensor, *, rank: int, cp_size: int +) -> tuple[tuple[int, int], ...]: + offsets = [int(value) for value in cu_seqlens.detach().cpu().tolist()] + slices = [] + for start, end in zip(offsets[:-1], offsets[1:], strict=True): + length = end - start + shard_start = start + (length * rank) // cp_size + shard_end = start + (length * (rank + 1)) // cp_size + if shard_start >= shard_end: + raise ValueError("test varlen chain unexpectedly produced an empty shard") + slices.append((shard_start, shard_end)) + return tuple(slices) + + +def _local_cu_seqlens_cpu(slices: tuple[tuple[int, int], ...]) -> torch.Tensor: + lengths = [end - start for start, end in slices] + return torch.tensor( + [0, *torch.cumsum(torch.tensor(lengths), dim=0).tolist()], + dtype=torch.long, + ) + + +def _lengths_by_rank_cpu(cu_seqlens: torch.Tensor, *, cp_size: int) -> torch.Tensor: + rows = [] + for rank in range(cp_size): + rank_slices = _rank_varlen_slices(cu_seqlens, rank=rank, cp_size=cp_size) + rows.append([end - start for start, end in rank_slices]) + return torch.tensor(rows, dtype=torch.long) + + +def _cat_varlen_slices( + tensor: torch.Tensor, + slices: tuple[tuple[int, int], ...], +) -> torch.Tensor: + return torch.cat([tensor[:, start:end] for start, end in slices], dim=1) + + +def _assert_grad_close(left: torch.Tensor, right_grad: torch.Tensor, name: str) -> None: + assert left.grad is not None, name + assert_mean_abs_pct(right_grad, left.grad, name) + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_gdn_conv_gelu.py b/tests/integration/megatron/gdn_shared_prefix/test_gdn_conv_gelu.py new file mode 100644 index 000000000..de791a7f9 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_gdn_conv_gelu.py @@ -0,0 +1,292 @@ +from __future__ import annotations + +import pytest +import torch +from torch import Tensor +import torch.nn.functional as F + +from art.megatron.gdn.conv_gelu import packed_varlen_causal_conv +from tests.integration.megatron.gdn_shared_prefix.metrics import assert_mean_abs_pct + +pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA required") + + +def test_packed_varlen_causal_conv_gelu_matches_reference_with_final_grads() -> None: + _run_packed_case( + lengths=(1, 2, 4, 7), + channels=9, + kernel_width=4, + has_bias=True, + activation="gelu", + ) + + +def test_packed_varlen_causal_conv_gelu_matches_reference_without_bias() -> None: + _run_packed_case( + lengths=(1, 3, 5), + channels=7, + kernel_width=3, + has_bias=False, + activation="gelu", + ) + + +def test_packed_varlen_causal_conv_silu_and_swish_match_reference() -> None: + for activation in ("silu", "swish"): + _run_packed_case( + lengths=(1, 4, 6), + channels=5, + kernel_width=5, + has_bias=True, + activation=activation, + ) + + +def test_packed_varlen_causal_conv_supports_unit_kernel() -> None: + _run_packed_case( + lengths=(1, 5), + channels=4, + kernel_width=1, + has_bias=True, + activation="none", + ) + + +def test_packed_varlen_causal_conv_rejects_unsupported_activation() -> None: + conv_in, cu_seqlens, conv_initial, weight, bias, _, _ = _packed_inputs( + lengths=(2,), + channels=3, + kernel_width=2, + has_bias=True, + seed=17, + ) + with pytest.raises(ValueError, match="activation"): + packed_varlen_causal_conv( + conv_in, + cu_seqlens, + conv_initial, + weight, + bias, + activation="relu", + ) + + +def _run_packed_case( + *, + lengths: tuple[int, ...], + channels: int, + kernel_width: int, + has_bias: bool, + activation: str, +) -> None: + inputs = _packed_inputs( + lengths=lengths, + channels=channels, + kernel_width=kernel_width, + has_bias=has_bias, + seed=kernel_width * 100 + channels + len(lengths), + ) + conv_in, cu_seqlens, conv_initial, weight, bias, out_grad, final_grad = inputs + ref = _run_packed_reference( + conv_in, + cu_seqlens, + conv_initial, + weight, + bias, + out_grad, + final_grad, + activation=activation, + ) + cand = _run_packed_fused( + conv_in, + cu_seqlens, + conv_initial, + weight, + bias, + out_grad, + final_grad, + activation=activation, + ) + _assert_packed_results_close(ref, cand) + + +def _packed_inputs( + *, + lengths: tuple[int, ...], + channels: int, + kernel_width: int, + has_bias: bool, + seed: int, +) -> tuple[Tensor, Tensor, Tensor, Tensor, Tensor | None, Tensor, Tensor]: + generator = torch.Generator(device="cuda").manual_seed(seed) + cu_values = [0] + for length in lengths: + cu_values.append(cu_values[-1] + length) + total_tokens = cu_values[-1] + conv_in = torch.randn( + total_tokens, + channels, + device="cuda", + dtype=torch.float32, + generator=generator, + ) + conv_initial = torch.randn( + len(lengths), + channels, + kernel_width - 1, + device="cuda", + dtype=torch.float32, + generator=generator, + ) + weight = torch.randn( + channels, kernel_width, device="cuda", dtype=torch.float32, generator=generator + ) + bias = ( + torch.randn(channels, device="cuda", dtype=torch.float32, generator=generator) + if has_bias + else None + ) + out_grad = torch.randn( + total_tokens, + channels, + device="cuda", + dtype=torch.float32, + generator=generator, + ) + final_grad = torch.randn( + len(lengths), + channels, + kernel_width - 1, + device="cuda", + dtype=torch.float32, + generator=generator, + ) + cu_seqlens = torch.tensor(cu_values, device="cuda", dtype=torch.int32) + return conv_in, cu_seqlens, conv_initial, weight, bias, out_grad, final_grad + + +def _run_packed_reference( + conv_in: Tensor, + cu_seqlens: Tensor, + conv_initial: Tensor, + weight: Tensor, + bias: Tensor | None, + out_grad: Tensor, + final_grad: Tensor, + *, + activation: str, +) -> dict[str, Tensor | None]: + conv_in = conv_in.detach().clone().requires_grad_(True) + conv_initial = conv_initial.detach().clone().requires_grad_(True) + weight = weight.detach().clone().requires_grad_(True) + bias = None if bias is None else bias.detach().clone().requires_grad_(True) + pieces = [] + for segment in range(int(cu_seqlens.numel()) - 1): + start = int(cu_seqlens[segment].item()) + end = int(cu_seqlens[segment + 1].item()) + segment_in = conv_in[start:end].transpose(0, 1).unsqueeze(0) + extended = torch.cat([conv_initial[segment : segment + 1], segment_in], dim=-1) + out = F.conv1d(extended, weight.unsqueeze(1), bias, groups=conv_in.shape[1]) + pieces.append(_torch_activation(out.squeeze(0).transpose(0, 1), activation)) + out = torch.cat(pieces, dim=0) + final = _packed_reference_final(conv_in, cu_seqlens, conv_initial) + ((out * out_grad).sum() + (final * final_grad).sum()).backward() + return _packed_result(conv_in, conv_initial, weight, bias, out, final) + + +def _run_packed_fused( + conv_in: Tensor, + cu_seqlens: Tensor, + conv_initial: Tensor, + weight: Tensor, + bias: Tensor | None, + out_grad: Tensor, + final_grad: Tensor, + *, + activation: str, +) -> dict[str, Tensor | None]: + conv_in = conv_in.detach().clone().requires_grad_(True) + conv_initial = conv_initial.detach().clone().requires_grad_(True) + weight = weight.detach().clone().requires_grad_(True) + bias = None if bias is None else bias.detach().clone().requires_grad_(True) + out, final = packed_varlen_causal_conv( + conv_in, + cu_seqlens, + conv_initial, + weight, + bias, + activation=activation, + output_final_state=True, + ) + assert final is not None + ((out * out_grad).sum() + (final * final_grad).sum()).backward() + return _packed_result(conv_in, conv_initial, weight, bias, out, final) + + +def _packed_reference_final( + conv_in: Tensor, cu_seqlens: Tensor, conv_initial: Tensor +) -> Tensor: + tail_width = int(conv_initial.shape[-1]) + if tail_width == 0: + return conv_initial + pieces = [] + for segment in range(int(cu_seqlens.numel()) - 1): + start = int(cu_seqlens[segment].item()) + end = int(cu_seqlens[segment + 1].item()) + extended = torch.cat([conv_initial[segment], conv_in[start:end].T], dim=-1) + length = end - start + pieces.append(extended[:, length : length + tail_width]) + return torch.stack(pieces, dim=0) + + +def _torch_activation(tensor: Tensor, activation: str) -> Tensor: + if activation == "none": + return tensor + if activation in ("silu", "swish"): + return F.silu(tensor) + if activation == "gelu": + return F.gelu(tensor) + raise ValueError(activation) + + +def _packed_result( + conv_in: Tensor, + conv_initial: Tensor, + weight: Tensor, + bias: Tensor | None, + out: Tensor, + final: Tensor, +) -> dict[str, Tensor | None]: + return { + "out": out.detach(), + "final": final.detach(), + "conv_in_grad": _required_grad(conv_in.grad), + "conv_initial_grad": _required_grad(conv_initial.grad), + "weight_grad": _required_grad(weight.grad), + "bias_grad": None if bias is None else _required_grad(bias.grad), + } + + +def _required_grad(grad: Tensor | None) -> Tensor: + if grad is None: + raise AssertionError("missing gradient") + return grad.detach() + + +def _assert_packed_results_close( + reference: dict[str, Tensor | None], candidate: dict[str, Tensor | None] +) -> None: + for name in ("out", "final", "conv_in_grad", "conv_initial_grad", "weight_grad"): + ref_tensor = reference[name] + cand_tensor = candidate[name] + assert ref_tensor is not None and cand_tensor is not None + assert ref_tensor.dtype == torch.float32 + assert cand_tensor.dtype == torch.float32 + if ref_tensor.numel() > 0: + assert torch.any(ref_tensor != 0), f"{name} reference is all zero" + assert_mean_abs_pct(ref_tensor, cand_tensor, name) + if reference["bias_grad"] is not None: + assert candidate["bias_grad"] is not None + assert reference["bias_grad"].dtype == torch.float32 + assert candidate["bias_grad"].dtype == torch.float32 + assert_mean_abs_pct(reference["bias_grad"], candidate["bias_grad"], "bias_grad") diff --git a/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_layout_distributed.py b/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_layout_distributed.py new file mode 100644 index 000000000..4f8cbadc6 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_layout_distributed.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +import os +from pathlib import Path + +import pytest +import torch +from torch.distributed import destroy_process_group, init_process_group +import torch.multiprocessing as mp + +from art.megatron.context_parallel.layout_index import TokenLayoutIndex +from art.megatron.gdn.layout import ( + build_local_rank_cp_exchange_plan_from_dest_ranges, + exchange_rank_tensor_all_to_all, +) + +from .cases import ( + GdnFamilyShape, + GdnPackedRowShape, + GdnPhase0Case, + default_phase0_cases, +) +from .layout_reference import build_test_gdn_cp_layout_plan +from .metrics import GDN_CORRECTNESS_DTYPE +from .packed_layout import build_phase0_packed_tensors + + +@pytest.mark.parametrize("cp_size", (2, 4, 8)) +def test_distributed_gdn_cp_layout_all_to_all_roundtrips( + cp_size: int, tmp_path: Path +) -> None: + init_path = tmp_path / f"gdn_cp_layout_gloo_{cp_size}" + if init_path.exists(): + init_path.unlink() + mp.start_processes( + _distributed_layout_worker, + args=(cp_size, str(init_path), "ragged_family_mix", True), + nprocs=cp_size, + join=True, + start_method="spawn", + ) + if init_path.exists(): + init_path.unlink() + + +def test_distributed_gdn_cp_layout_handles_empty_ranks(tmp_path: Path) -> None: + cp_size = 8 + init_path = tmp_path / "gdn_cp_layout_gloo_empty" + if init_path.exists(): + init_path.unlink() + mp.start_processes( + _distributed_layout_worker, + args=(cp_size, str(init_path), "tiny_empty_rank", False), + nprocs=cp_size, + join=True, + start_method="spawn", + ) + if init_path.exists(): + init_path.unlink() + + +@pytest.mark.skipif( + not torch.cuda.is_available() or torch.cuda.device_count() < 4, + reason="requires at least four CUDA devices for NCCL zero-token exchange coverage", +) +def test_distributed_gdn_cp_layout_nccl_handles_zero_source_nonzero_dest( + tmp_path: Path, +) -> None: + cp_size = 4 + init_path = tmp_path / "gdn_cp_layout_nccl_zero_source" + if init_path.exists(): + init_path.unlink() + mp.start_processes( + _distributed_zero_source_nccl_worker, + args=(cp_size, str(init_path)), + nprocs=cp_size, + join=True, + start_method="spawn", + ) + if init_path.exists(): + init_path.unlink() + + +def _distributed_layout_worker( + rank: int, + world_size: int, + init_path: str, + case_name: str, + reverse_attention: bool, +) -> None: + os.environ.setdefault("MASTER_ADDR", "127.0.0.1") + os.environ.setdefault("MASTER_PORT", "29591") + init_process_group( + "gloo", + init_method=f"file://{init_path}", + rank=rank, + world_size=world_size, + ) + try: + case = _case_by_name(case_name) + tensors = build_phase0_packed_tensors(case) + real_indices = _real_token_indices(tensors["group_ids"]) + attention_order = ( + tuple(reversed(real_indices)) if reverse_attention else real_indices + ) + plan = build_test_gdn_cp_layout_plan( + group_ids=tensors["group_ids"], + parent_ids=tensors["parent_ids"], + cp_size=world_size, + attention_token_layout_index=_layout_from_tokens_by_rank( + _striped_rank_indices(attention_order, cp_size=world_size) + ), + ) + + flat = torch.arange( + int(tensors["group_ids"].numel()) * 6, + dtype=GDN_CORRECTNESS_DTYPE, + ).reshape(-1, 2, 3) + local_source = flat.index_select( + 0, + torch.tensor( + _tokens_by_rank_from_ranges(plan.attention_token_ranges_by_rank)[rank], + dtype=torch.long, + ), + ) + local_source = local_source.detach().clone().requires_grad_(True) + + gdn_local = exchange_rank_tensor_all_to_all( + local_source, + plan.attention_to_gdn, + rank=rank, + backward_plan=plan.gdn_to_attention, + ) + expected_gdn = flat.index_select( + 0, + torch.tensor( + _tokens_by_rank_from_ranges(plan.gdn_token_ranges_by_rank)[rank], + dtype=torch.long, + ), + ) + torch.testing.assert_close(gdn_local, expected_gdn, rtol=0, atol=0) + + restored = exchange_rank_tensor_all_to_all( + gdn_local, + plan.gdn_to_attention, + rank=rank, + backward_plan=plan.attention_to_gdn, + ) + torch.testing.assert_close(restored, local_source, rtol=0, atol=0) + + weight = torch.arange( + restored.numel(), + dtype=restored.dtype, + device=restored.device, + ).reshape_as(restored) + (restored * weight).sum().backward() + assert local_source.grad is not None + torch.testing.assert_close(local_source.grad, weight, rtol=0, atol=0) + finally: + destroy_process_group() + + +def _distributed_zero_source_nccl_worker( + rank: int, + world_size: int, + init_path: str, +) -> None: + torch.cuda.set_device(rank) + init_process_group( + "nccl", + init_method=f"file://{init_path}", + rank=rank, + world_size=world_size, + ) + try: + source_tokens = (tuple(range(16)), (), (), ()) + dest_tokens = ((), (), (), tuple(range(16))) + source_ranges = _rank_ranges_from_tokens_by_rank(source_tokens) + dest_ranges = _rank_ranges_from_tokens_by_rank(dest_tokens) + forward_plan = build_local_rank_cp_exchange_plan_from_dest_ranges( + source_layout=_layout_from_rank_ranges(source_ranges), + dest_ranges_by_rank=dest_ranges, + device="cuda", + local_rank=rank, + cross_rank_token_count=16, + ) + backward_plan = build_local_rank_cp_exchange_plan_from_dest_ranges( + source_layout=_layout_from_rank_ranges(dest_ranges), + dest_ranges_by_rank=source_ranges, + device="cuda", + local_rank=rank, + cross_rank_token_count=16, + ) + flat = torch.arange(16 * 6, device="cuda", dtype=torch.float32).reshape( + 16, 2, 3 + ) + local_source = flat.index_select( + 0, + torch.tensor(source_tokens[rank], device="cuda", dtype=torch.long), + ) + local_source = local_source.detach().clone().requires_grad_(True) + actual = exchange_rank_tensor_all_to_all( + local_source, + forward_plan, + rank=rank, + backward_plan=backward_plan, + ) + expected = flat.index_select( + 0, + torch.tensor(dest_tokens[rank], device="cuda", dtype=torch.long), + ) + torch.testing.assert_close(actual, expected, rtol=0, atol=0) + + actual.sum().backward() + assert local_source.grad is not None + expected_grad = ( + torch.ones_like(local_source) + if rank == 0 + else torch.empty_like(local_source) + ) + torch.testing.assert_close(local_source.grad, expected_grad, rtol=0, atol=0) + torch.cuda.synchronize() + finally: + destroy_process_group() + + +def _case_by_name(case_name: str) -> GdnPhase0Case: + if case_name == "tiny_empty_rank": + return GdnPhase0Case( + name="tiny_empty_rank", + sequence_length=8, + rows=( + GdnPackedRowShape( + families=(GdnFamilyShape(prefix_length=2, suffix_lengths=(1,)),) + ), + ), + ) + return next( + case for case in default_phase0_cases(conv_width=4) if case.name == case_name + ) + + +def _real_token_indices(group_ids: torch.Tensor) -> tuple[int, ...]: + sequence_length = int(group_ids.shape[1]) + return tuple( + row * sequence_length + position + for row in range(int(group_ids.shape[0])) + for position in torch.nonzero(group_ids[row] != -1, as_tuple=False) + .flatten() + .tolist() + ) + + +def _striped_rank_indices( + token_indices: tuple[int, ...], + *, + cp_size: int, +) -> tuple[tuple[int, ...], ...]: + ranks: list[list[int]] = [[] for _ in range(cp_size)] + for offset, token_index in enumerate(token_indices): + ranks[offset % cp_size].append(token_index) + return tuple(tuple(rank_indices) for rank_indices in ranks) + + +def _layout_from_tokens_by_rank( + tokens_by_rank: tuple[tuple[int, ...], ...], +) -> TokenLayoutIndex: + return _layout_from_rank_ranges(_rank_ranges_from_tokens_by_rank(tokens_by_rank)) + + +def _layout_from_rank_ranges( + ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...], +) -> TokenLayoutIndex: + return TokenLayoutIndex( + ownership_ranges_by_rank=ranges_by_rank, + token_counts_by_rank=tuple( + sum(end - start for start, end, _ in ranges) for ranges in ranges_by_rank + ), + ) + + +def _rank_ranges_from_tokens_by_rank( + tokens_by_rank: tuple[tuple[int, ...], ...], +) -> tuple[tuple[tuple[int, int, int], ...], ...]: + return tuple(_rank_ranges_from_tokens(tokens) for tokens in tokens_by_rank) + + +def _rank_ranges_from_tokens( + tokens: tuple[int, ...], +) -> tuple[tuple[int, int, int], ...]: + if not tokens: + return () + ranges = [] + start = tokens[0] + end = start + 1 + position = 0 + for local_position, token in enumerate(tokens[1:], start=1): + if token == end: + end += 1 + continue + ranges.append((start, end, position)) + start = token + end = token + 1 + position = local_position + ranges.append((start, end, position)) + return tuple(ranges) + + +def _tokens_by_rank_from_ranges( + ranges_by_rank: tuple[tuple[tuple[int, int, int], ...], ...], +) -> tuple[tuple[int, ...], ...]: + return tuple( + tuple(token for start, end, _ in ranges for token in range(start, end)) + for ranges in ranges_by_rank + ) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_packed_correctness.py b/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_packed_correctness.py new file mode 100644 index 000000000..2151b41e1 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_packed_correctness.py @@ -0,0 +1,458 @@ +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path +import socket +from typing import Any + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("megatron.bridge") +pytest.importorskip("megatron.bridge.models.qwen_vl.qwen35_vl_provider") + +from megatron.core import parallel_state as ps # noqa: E402 +from torch.distributed import destroy_process_group, init_process_group # noqa: E402 +import torch.multiprocessing as mp # noqa: E402 + +from art.megatron.gdn.gdn_shared_prefix import ( # noqa: E402 + GdnPlannerConfig, + build_gdn_rank_execution_plan, + parse_gdn_shared_prefix_segments, +) +from art.megatron.gdn.operator import run_gdn_layer # noqa: E402 + +from .cases import ( # noqa: E402 + GdnFamilyShape, + GdnPackedRowShape, + GdnPhase0Case, + default_phase0_cases, +) +from .distributed_grad import all_reduce_parameter_grads_coalesced # noqa: E402 +from .metrics import ( # noqa: E402 + GDN_CORRECTNESS_DTYPE, + REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD, + REAL_GDN_OUTPUT_MEAN_ABS_PCT_THRESHOLD, + assert_mean_abs_pct, + assert_scalar_loss_close, + parameter_grad_mean_abs_pct_with_name, + stable_output_mse_loss, +) +from .packed_layout import build_phase0_packed_tensors # noqa: E402 +from .real_gdn_oracle import zero_parameter_grads # noqa: E402 +from .test_real_gdn_native_fla_cp import _make_matching_gdn_pair # noqa: E402 + +_CP_SIZES = (2, 4, 8) + + +@pytest.mark.parametrize("cp_size", _CP_SIZES) +def test_gdn_cp_packed_matches_cp1_oracle_all_edge_cases( + cp_size: int, tmp_path: Path +) -> None: + _skip_without_gpus(cp_size) + port = _find_free_port() + mp.spawn( + _cp1_oracle_worker, + args=(cp_size, port, str(tmp_path), False), + nprocs=cp_size, + join=True, + ) + for rank in range(cp_size): + assert (tmp_path / f"cp1_oracle_rank_{rank}.ok").read_text() == "ok\n" + + +@pytest.mark.parametrize("cp_size", _CP_SIZES) +def test_gdn_cp_packed_sibling_order_matches_cp1_oracle( + cp_size: int, tmp_path: Path +) -> None: + _skip_without_gpus(cp_size) + port = _find_free_port() + mp.spawn( + _cp1_oracle_worker, + args=(cp_size, port, str(tmp_path), True), + nprocs=cp_size, + join=True, + ) + for rank in range(cp_size): + assert (tmp_path / f"cp1_oracle_sibling_rank_{rank}.ok").read_text() == "ok\n" + + +def _cp1_oracle_worker( + rank: int, + cp_size: int, + port: int, + output_dir: str, + sibling_only: bool, +) -> None: + torch.cuda.set_device(rank) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{port}", + rank=rank, + world_size=cp_size, + ) + try: + ps.initialize_model_parallel( + tensor_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=cp_size, + expert_model_parallel_size=1, + ) + ref_gdn, cp_gdn = _make_matching_gdn_pair(cp_size=cp_size) + if sibling_only: + _assert_sibling_order_matches_cp1( + ref_gdn, + cp_gdn, + rank=rank, + cp_size=cp_size, + ) + Path(output_dir, f"cp1_oracle_sibling_rank_{rank}.ok").write_text("ok\n") + return + for case_index, case in enumerate(_packed_correctness_cases()): + _assert_case_matches_cp1( + ref_gdn, + cp_gdn, + case, + rank=rank, + cp_size=cp_size, + seed=20510426 + 1000 * cp_size + case_index, + planner_config=_planner_config_for_case(case), + ) + torch.distributed.barrier() + Path(output_dir, f"cp1_oracle_rank_{rank}.ok").write_text("ok\n") + finally: + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + ps.destroy_model_parallel() + destroy_process_group() + + +def _assert_case_matches_cp1( + ref_gdn: torch.nn.Module, + cp_gdn: torch.nn.Module, + case: GdnPhase0Case, + *, + rank: int, + cp_size: int, + seed: int, + planner_config: GdnPlannerConfig | None, +) -> None: + zero_parameter_grads(ref_gdn) + zero_parameter_grads(cp_gdn) + tensors = build_phase0_packed_tensors(case) + group_ids = tensors["group_ids"].cuda() + parent_ids = tensors["parent_ids"].cuda() + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + plan = build_gdn_rank_execution_plan( + spec, + device=group_ids.device, + cp_rank=rank, + cp_size=cp_size, + planner_config=planner_config or GdnPlannerConfig(), + ) + hidden, output_grad = _hidden_and_grad(case, seed=seed) + real_mask = (group_ids != -1).transpose(0, 1).unsqueeze(-1) + output_grad = output_grad * real_mask + loss_denominator = real_mask.expand_as(output_grad).sum() + ref_hidden = hidden.clone().detach().requires_grad_(True) + ref_out, _ = run_gdn_layer( + ref_gdn, + ref_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + ref_loss = stable_output_mse_loss( + ref_out, + output_grad, + mask=real_mask, + denominator=loss_denominator, + ) + ref_loss.backward() + + flat_hidden = hidden.transpose(0, 1).reshape(-1, hidden.shape[-1]) + flat_grad = output_grad.transpose(0, 1).reshape(-1, output_grad.shape[-1]) + local_index = torch.tensor( + plan.attention_token_indices, device=hidden.device, dtype=torch.long + ) + local_hidden = ( + flat_hidden.index_select(0, local_index) + .unsqueeze(1) + .contiguous() + .detach() + .requires_grad_(True) + ) + local_output_grad = flat_grad.index_select(0, local_index).unsqueeze(1).contiguous() + cp_out, _ = run_gdn_layer( + cp_gdn, + local_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + execution_spec=spec, + execution_plan=plan, + cp_group=torch.distributed.group.WORLD, + ) + cp_loss = stable_output_mse_loss( + cp_out, + local_output_grad, + denominator=loss_denominator, + ) + cp_loss.backward() + _assert_cp_matches_reference( + case.name, + ref_gdn, + cp_gdn, + ref_hidden, + ref_out, + ref_loss.detach(), + local_hidden, + cp_out, + cp_loss.detach(), + local_index, + ) + + +def _assert_sibling_order_matches_cp1( + ref_gdn: torch.nn.Module, + cp_gdn: torch.nn.Module, + *, + rank: int, + cp_size: int, +) -> None: + case = _sibling_case() + zero_parameter_grads(ref_gdn) + zero_parameter_grads(cp_gdn) + tensors = build_phase0_packed_tensors(case) + group_ids = tensors["group_ids"].cuda() + parent_ids = tensors["parent_ids"].cuda() + swapped_group_ids = torch.full_like(group_ids, -1) + swapped_parent_ids = torch.full_like(parent_ids, -1) + swapped_group_ids[0, :5] = 0 + swapped_parent_ids[0, :5] = 0 + swapped_group_ids[0, 5:9] = 1 + swapped_parent_ids[0, 5:9] = 0 + swapped_group_ids[0, 9:12] = 2 + swapped_parent_ids[0, 9:12] = 0 + spec = parse_gdn_shared_prefix_segments( + swapped_group_ids, swapped_parent_ids, min_completions_per_family=0 + ) + plan = build_gdn_rank_execution_plan( + spec, + device=group_ids.device, + cp_rank=rank, + cp_size=cp_size, + planner_config=GdnPlannerConfig(), + ) + hidden, output_grad = _hidden_and_grad(case, seed=20520426 + cp_size) + real_mask = (group_ids != -1).transpose(0, 1).unsqueeze(-1) + output_grad = output_grad * real_mask + loss_denominator = real_mask.expand_as(output_grad).sum() + swapped_hidden = _swap_siblings(hidden) + swapped_grad = _swap_siblings(output_grad) + + ref_hidden = hidden.clone().detach().requires_grad_(True) + ref_out, _ = run_gdn_layer( + ref_gdn, + ref_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + ref_loss = stable_output_mse_loss( + ref_out, + output_grad, + mask=real_mask, + denominator=loss_denominator, + ) + ref_loss.backward() + + flat_hidden = swapped_hidden.transpose(0, 1).reshape(-1, hidden.shape[-1]) + flat_grad = swapped_grad.transpose(0, 1).reshape(-1, output_grad.shape[-1]) + local_index = torch.tensor( + plan.attention_token_indices, device=hidden.device, dtype=torch.long + ) + local_hidden = ( + flat_hidden.index_select(0, local_index) + .unsqueeze(1) + .contiguous() + .detach() + .requires_grad_(True) + ) + local_output_grad = flat_grad.index_select(0, local_index).unsqueeze(1).contiguous() + cp_out, _ = run_gdn_layer( + cp_gdn, + local_hidden, + group_ids=swapped_group_ids, + parent_ids=swapped_parent_ids, + execution_spec=spec, + execution_plan=plan, + cp_group=torch.distributed.group.WORLD, + ) + cp_loss = stable_output_mse_loss( + cp_out, + local_output_grad, + denominator=loss_denominator, + ) + cp_loss.backward() + expected_out = _swap_siblings(ref_out) + assert ref_hidden.grad is not None + expected_grad = _swap_siblings(ref_hidden.grad) + _assert_cp_matches_reference( + case.name, + ref_gdn, + cp_gdn, + _TensorGradView(expected_grad), + expected_out, + ref_loss.detach(), + local_hidden, + cp_out, + cp_loss.detach(), + local_index, + ) + + +def _assert_cp_matches_reference( + name: str, + ref_gdn: torch.nn.Module, + cp_gdn: torch.nn.Module, + ref_hidden: Any, + ref_out: torch.Tensor, + ref_loss: torch.Tensor, + local_hidden: torch.Tensor, + cp_out: torch.Tensor, + cp_loss: torch.Tensor, + local_index: torch.Tensor, +) -> None: + torch.distributed.all_reduce(cp_loss, op=torch.distributed.ReduceOp.SUM) + all_reduce_parameter_grads_coalesced(cp_gdn) + torch.cuda.synchronize() + flat_ref_out = ref_out.detach().transpose(0, 1).reshape(-1, ref_out.shape[-1]) + assert_scalar_loss_close(ref_loss, cp_loss, f"{name}:loss") + if int(local_index.numel()) != 0: + assert_mean_abs_pct( + flat_ref_out.index_select(0, local_index), + cp_out.detach().squeeze(1), + f"{name}:output", + threshold=REAL_GDN_OUTPUT_MEAN_ABS_PCT_THRESHOLD, + ) + assert local_hidden.grad is not None + flat_ref_grad = ref_hidden.grad.transpose(0, 1).reshape( + -1, local_hidden.shape[-1] + ) + assert_mean_abs_pct( + flat_ref_grad.index_select(0, local_index), + local_hidden.grad.squeeze(1), + f"{name}:hidden_grad", + threshold=REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD, + ) + param_name, param_pct = parameter_grad_mean_abs_pct_with_name(ref_gdn, cp_gdn) + assert param_pct <= REAL_GDN_GRAD_MEAN_ABS_PCT_THRESHOLD, f"{name}:{param_name}" + torch.cuda.synchronize() + + +class _TensorGradView: + def __init__(self, grad: torch.Tensor) -> None: + self.grad = grad + + +def _hidden_and_grad( + case: GdnPhase0Case, *, seed: int +) -> tuple[torch.Tensor, torch.Tensor]: + generator = torch.Generator(device="cuda").manual_seed(seed) + hidden = torch.randn( + case.sequence_length, + len(case.rows), + 64, + device="cuda", + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + grad = torch.randn( + hidden.shape, + device="cuda", + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + torch.distributed.broadcast(hidden, src=0) + torch.distributed.broadcast(grad, src=0) + return hidden, grad + + +def _packed_correctness_cases() -> tuple[GdnPhase0Case, ...]: + return ( + *default_phase0_cases(conv_width=2), + _mixed_local_chain_case(), + _local_prefix_chain_completion_case(), + ) + + +def _planner_config_for_case(case: GdnPhase0Case) -> GdnPlannerConfig | None: + if case.name != "mixed_local_chain_edge": + return None + return GdnPlannerConfig( + cp_chain_min_tokens_per_rank=16, + cp_chain_min_total_tokens=128, + cp_chain_min_prefix_only_tokens=128, + ) + + +def _mixed_local_chain_case() -> GdnPhase0Case: + return GdnPhase0Case( + name="mixed_local_chain_edge", + sequence_length=960, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=256, suffix_lengths=(320, 64)), + GdnFamilyShape(prefix_length=12, suffix_lengths=(7, 5, 9)), + GdnFamilyShape(prefix_length=128, suffix_lengths=(80, 32)), + ) + ), + ), + seed=67, + description="One row mixing long native CP-chain work and short local-fork siblings.", + ) + + +def _local_prefix_chain_completion_case() -> GdnPhase0Case: + return GdnPhase0Case( + name="local_prefix_chain_completion_edge", + sequence_length=768, + rows=( + GdnPackedRowShape( + families=(GdnFamilyShape(prefix_length=96, suffix_lengths=(640, 17)),) + ), + ), + seed=71, + description="Short local prefix feeding a long native CP-chain completion.", + ) + + +def _sibling_case() -> GdnPhase0Case: + return GdnPhase0Case( + name="sibling_order_edge", + sequence_length=16, + rows=( + GdnPackedRowShape( + families=(GdnFamilyShape(prefix_length=5, suffix_lengths=(3, 4)),) + ), + ), + seed=59, + ) + + +def _swap_siblings(tensor: torch.Tensor) -> torch.Tensor: + swapped = tensor.clone() + swapped[5:9] = tensor[8:12] + swapped[9:12] = tensor[5:8] + return swapped + + +def _skip_without_gpus(cp_size: int) -> None: + if not torch.cuda.is_available() or torch.cuda.device_count() < cp_size: + pytest.skip(f"Need {cp_size} CUDA devices for CP{cp_size} packed GDN.") + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_train_prepare.py b/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_train_prepare.py new file mode 100644 index 000000000..e0d2e831f --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_gdn_cp_train_prepare.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from pathlib import Path +import socket +from typing import Any, cast + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("megatron.bridge") + +from megatron.core import parallel_state as ps # noqa: E402 +from torch.distributed import destroy_process_group, init_process_group # noqa: E402 +import torch.multiprocessing as mp # noqa: E402 + +from art.loss import LossInputs, loss_fn, shift_tensor # noqa: E402 +from art.megatron.context_parallel.runtime import prepare_cp_micro # noqa: E402 +from art.megatron.context_parallel.types import ( # noqa: E402 + ArtContextParallelState, + ContextParallelConfig, + DispatchedPackedTensors, + ParallelTopology, +) +from art.preprocessing.pack import PackedTensors # noqa: E402 + +from .cases import default_phase0_cases # noqa: E402 +from .packed_layout import build_phase0_packed_tensors # noqa: E402 + + +def test_gdn_cp_training_batch_carries_prebuilt_rank_plan(tmp_path: Path) -> None: + cp_size = 2 + if not torch.cuda.is_available() or torch.cuda.device_count() < cp_size: + pytest.skip(f"requires {cp_size} CUDA devices") + port = _find_free_port() + mp.spawn( + _worker, + args=(cp_size, port, str(tmp_path)), + nprocs=cp_size, + join=True, + ) + for rank in range(cp_size): + assert (tmp_path / f"rank_{rank}.ok").read_text() == "ok\n" + + +def _worker(rank: int, cp_size: int, port: int, output_dir: str) -> None: + torch.cuda.set_device(rank) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{port}", + rank=rank, + world_size=cp_size, + ) + try: + ps.initialize_model_parallel( + tensor_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=cp_size, + expert_model_parallel_size=1, + ) + micro = cast( + PackedTensors, + { + key: value.cuda() if isinstance(value, torch.Tensor) else value + for key, value in build_phase0_packed_tensors( + default_phase0_cases()[1] + ).items() + }, + ) + cast(Any, micro)["original_logprobs"] = micro["logprobs"] + 0.125 + ref_logprobs = torch.full_like(micro["logprobs"], -0.25) + prepared = prepare_cp_micro( + micro=micro, + topology=ParallelTopology(cp=cp_size), + config=ContextParallelConfig(), + cp_group=ps.get_context_parallel_group(check_initialized=False), + cp_rank=ps.get_context_parallel_rank(), + build_gdn_execution_spec=True, + ref_logprobs=ref_logprobs, + ) + state = prepared.attention_state + assert isinstance(state, ArtContextParallelState) + plan = state.gdn_execution_plan + assert plan is not None + assert plan.cp_rank == rank + assert plan.cp_size == cp_size + assert state.gdn_execution_spec is not None + assert prepared.tensors.tokens.shape == (1, int(plan.attention_token_count)) + assert prepared.tensors.labels.shape == prepared.tensors.tokens.shape + assert prepared.tensors.input_pos.shape == prepared.tensors.tokens.shape + assert prepared.tensors.group_ids.shape == prepared.tensors.tokens.shape + assert prepared.tensors.original_logprobs is not None + assert prepared.tensors.original_logprobs.shape == prepared.tensors.tokens.shape + assert prepared.tensors.ref_logprobs is not None + assert prepared.tensors.ref_logprobs.shape == prepared.tensors.tokens.shape + assert prepared.tensors.loss_all_reduce_group is not None + assert prepared.tensors.valid_lengths == (int(plan.attention_token_count),) + Path(output_dir, f"rank_{rank}.ok").write_text("ok\n") + finally: + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + ps.destroy_model_parallel() + destroy_process_group() + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) + + +def test_main_loss_matches_shifted_dispatched_loss_inputs() -> None: + packed = cast( + Any, + { + "tokens": torch.tensor([[10, 11, 12, 13, 14, 0]]), + "group_ids": torch.tensor([[1, 1, 2, 2, 2, -1]]), + "parent_ids": torch.tensor([[1, 1, 1, 1, 1, -1]]), + "input_pos": torch.arange(6).reshape(1, 6), + "assistant_mask": torch.tensor([[False, True, True, True, True, False]]), + "logprobs": torch.tensor( + [[float("nan"), -0.72, -0.65, -0.81, -0.52, float("nan")]] + ), + "original_logprobs": torch.tensor( + [[float("nan"), -0.70, -0.60, -0.80, -0.55, float("nan")]] + ), + "advantages": torch.tensor([[0.0, 0.3, -0.2, 0.4, -0.5, 0.0]]), + "weights": torch.tensor([[0.0, 1.0, 1.2, 0.8, 1.1, 0.0]]), + "pixel_values": [None], + "image_grid_thw": [None], + }, + ) + ref_logprobs = torch.tensor([[-0.9, -0.7, -0.6, -0.8, -0.55, -0.5]]) + entropies = torch.tensor([[0.0, 0.2, 0.4, 0.6, 0.8, 0.0]]) + dispatched = DispatchedPackedTensors( + tokens=packed["tokens"], + labels=shift_tensor(packed["tokens"], -100), + input_pos=packed["input_pos"], + assistant_mask=shift_tensor(packed["assistant_mask"], False), + group_ids=shift_tensor(packed["group_ids"], 0), + old_logprobs=shift_tensor(packed["logprobs"], float("nan")), + advantages=shift_tensor(packed["advantages"], 0.0), + weights=shift_tensor(packed["weights"], 0.0), + valid_lengths=(6,), + original_logprobs=shift_tensor(packed["original_logprobs"], 0.0), + ref_logprobs=ref_logprobs, + ) + config = cast( + Any, + { + "importance_sampling_level": "sequence", + "truncated_importance_sampling": 1.4, + "kl_penalty_coef": 0.15, + }, + ) + dense_new_logprobs = torch.tensor( + [[-0.85, -0.69, -0.66, -0.75, -0.51, -0.4]], requires_grad=True + ) + dispatched_new_logprobs = dense_new_logprobs.detach().clone().requires_grad_() + + dense_loss = loss_fn( + LossInputs(inputs=packed), + new_logprobs=dense_new_logprobs, + ref_logprobs=ref_logprobs, + entropies=entropies, + experimental_config=config, + reduction="sum", + ) + dispatched_loss = loss_fn( + dispatched, + new_logprobs=dispatched_new_logprobs, + ref_logprobs=dispatched.ref_logprobs, + entropies=shift_tensor(entropies, 0.0), + experimental_config=config, + reduction="sum", + ) + dense_loss.policy_loss.backward() + dispatched_loss.policy_loss.backward() + + torch.testing.assert_close(dispatched_loss.policy_loss, dense_loss.policy_loss) + torch.testing.assert_close( + dispatched_loss.policy_loss_sum, + dense_loss.policy_loss_sum, + ) + assert dispatched_loss.entropy is not None and dense_loss.entropy is not None + torch.testing.assert_close(dispatched_loss.entropy, dense_loss.entropy) + assert ( + dispatched_loss.kl_policy_ref is not None + and dense_loss.kl_policy_ref is not None + ) + torch.testing.assert_close( + dispatched_loss.kl_policy_ref, + dense_loss.kl_policy_ref, + ) + torch.testing.assert_close( + dispatched_new_logprobs.grad, + dense_new_logprobs.grad, + ) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_qwen35_full_model_cp1_packed_vs_flattened.py b/tests/integration/megatron/gdn_shared_prefix/test_qwen35_full_model_cp1_packed_vs_flattened.py new file mode 100644 index 000000000..19f33970c --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_qwen35_full_model_cp1_packed_vs_flattened.py @@ -0,0 +1,451 @@ +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import ExitStack, contextmanager +import socket +from typing import Any + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("megatron.bridge") +pytest.importorskip("megatron.bridge.models.qwen_vl.qwen35_vl_provider") + +from megatron.bridge.models.qwen_vl.qwen35_vl_provider import ( + Qwen3_5MoeVisionConfig, + Qwen35VLMoEModelProvider, +) +from megatron.core import parallel_state as ps +from megatron.core.tensor_parallel.random import model_parallel_cuda_manual_seed +from torch.distributed import destroy_process_group, init_process_group, is_initialized +import torch.nn.functional as F + +from art.loss import shift_tensor +from art.megatron.model_support.handlers.qwen3_5 import QWEN3_5_MOE_HANDLER +from art.megatron.shared_prefix_state import create_shared_prefix_state + +from ..model_support.oracle_harness import TEST_DEFAULT_FLEX_BACKEND +from ..model_support.oracle_worker import ( + _apply_requested_flex_backend_patch, + _apply_test_attention_full_fp32_patch, + _apply_test_flex_inner_fp32_patch, +) +from .cases import default_phase0_cases +from .metrics import ( + GDN_CORRECTNESS_DTYPE, + MEAN_ABS_PCT_THRESHOLD, + assert_mean_abs_pct, + mean_abs_pct, + parameter_grad_mean_abs_pct_with_name, + stable_output_mse_loss, +) +from .packed_layout import build_phase0_packed_tensors +from .parser_import import parse_gdn_shared_prefix_segments +from .real_gdn_oracle import ( + attach_main_grads, + zero_parameter_grads, +) + + +@pytest.fixture(autouse=True) +def _fp32_test_flex_backend() -> Iterator[None]: + with ExitStack() as stack: + stack.enter_context( + _apply_requested_flex_backend_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + stack.enter_context( + _apply_test_flex_inner_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + stack.enter_context( + _apply_test_attention_full_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + yield + + +@pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA is required for Qwen3.5 full-model shared-prefix oracle coverage.", +) +def test_qwen35_full_model_cp1_matches_flattened_grad_accumulation() -> None: + with _single_rank_model_parallel(): + packed_model, flat_model = _make_matching_models() + case = next( + item + for item in default_phase0_cases(conv_width=2) + if item.name == "ragged_family_mix" + ) + tensors = build_phase0_packed_tensors(case) + device = torch.device("cuda") + tokens = tensors["tokens"].remainder(128).to(device) + input_pos = tensors["input_pos"].to(device) + group_ids = tensors["group_ids"].to(device) + parent_ids = tensors["parent_ids"].to(device) + assistant_mask = tensors["assistant_mask"].to(device) + + zero_parameter_grads(packed_model) + zero_parameter_grads(flat_model) + packed_logits, packed_loss = _run_model_loss( + packed_model, + tokens=tokens, + input_pos=input_pos, + group_ids=group_ids, + parent_ids=parent_ids, + assistant_mask=assistant_mask, + ) + packed_loss.backward() + + flat_loss_sum: torch.Tensor | None = None + logits_mean_abs_pct = 0.0 + spec = parse_gdn_shared_prefix_segments( + group_ids.cpu(), parent_ids.cpu(), min_completions_per_family=1 + ) + for family in spec.families: + row = family.row_index + prefix = family.prefix + for completion in family.completions: + ref_tokens = torch.cat( + [ + tokens[row : row + 1, prefix.start : prefix.end], + tokens[row : row + 1, completion.start : completion.end], + ], + dim=1, + ) + ref_pos = torch.cat( + [ + input_pos[row : row + 1, prefix.start : prefix.end], + input_pos[row : row + 1, completion.start : completion.end], + ], + dim=1, + ) + ref_assistant_mask = torch.cat( + [ + torch.zeros( + (1, prefix.length), dtype=torch.bool, device=device + ), + assistant_mask[ + row : row + 1, completion.start : completion.end + ], + ], + dim=1, + ) + ref_group_ids = torch.zeros_like(ref_tokens) + ref_parent_ids = torch.zeros_like(ref_tokens) + ref_logits, ref_loss = _run_model_loss( + flat_model, + tokens=ref_tokens, + input_pos=ref_pos, + group_ids=ref_group_ids, + parent_ids=ref_parent_ids, + assistant_mask=ref_assistant_mask, + ) + ref_loss.backward() + flat_loss_sum = ( + ref_loss.detach() + if flat_loss_sum is None + else flat_loss_sum + ref_loss.detach() + ) + + if completion.length > 1: + packed_slice = packed_logits[ + row : row + 1, completion.start : completion.end - 1 + ] + ref_slice = ref_logits[ + :, prefix.length : prefix.length + completion.length - 1 + ] + logits_mean_abs_pct = max( + logits_mean_abs_pct, + mean_abs_pct(ref_slice, packed_slice), + ) + + assert flat_loss_sum is not None + grad_name, grad_pct = parameter_grad_mean_abs_pct_with_name( + flat_model, packed_model + ) + assert_mean_abs_pct(flat_loss_sum, packed_loss.detach(), "loss") + assert logits_mean_abs_pct <= MEAN_ABS_PCT_THRESHOLD + assert grad_pct <= MEAN_ABS_PCT_THRESHOLD, grad_name + + _assert_logits_vjp_equivalence( + packed_model=packed_model, + flat_model=flat_model, + tokens=tokens, + input_pos=input_pos, + group_ids=group_ids, + parent_ids=parent_ids, + assistant_mask=assistant_mask, + ) + + +def _assert_logits_vjp_equivalence( + *, + packed_model: torch.nn.Module, + flat_model: torch.nn.Module, + tokens: torch.Tensor, + input_pos: torch.Tensor, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + assistant_mask: torch.Tensor, +) -> None: + zero_parameter_grads(packed_model) + zero_parameter_grads(flat_model) + packed_logits = _run_model_logits( + packed_model, + tokens=tokens, + input_pos=input_pos, + group_ids=group_ids, + parent_ids=parent_ids, + ) + shifted_assistant_mask = shift_tensor(assistant_mask, False) + output_grad = torch.randn( + packed_logits.shape, + device=packed_logits.device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=torch.Generator(device=packed_logits.device).manual_seed(20280425), + ) + output_grad = output_grad * shifted_assistant_mask.unsqueeze(-1) * 0.1 + loss_denominator = shifted_assistant_mask.unsqueeze(-1).expand_as(output_grad).sum() + packed_loss = stable_output_mse_loss( + packed_logits, + output_grad, + mask=shifted_assistant_mask.unsqueeze(-1), + denominator=loss_denominator, + ) + packed_loss.backward() + + flat_loss_sum: torch.Tensor | None = None + logits_mean_abs_pct = 0.0 + spec = parse_gdn_shared_prefix_segments( + group_ids.cpu(), parent_ids.cpu(), min_completions_per_family=1 + ) + for family in spec.families: + row = family.row_index + prefix = family.prefix + for completion in family.completions: + ref_tokens = torch.cat( + [ + tokens[row : row + 1, prefix.start : prefix.end], + tokens[row : row + 1, completion.start : completion.end], + ], + dim=1, + ) + ref_pos = torch.cat( + [ + input_pos[row : row + 1, prefix.start : prefix.end], + input_pos[row : row + 1, completion.start : completion.end], + ], + dim=1, + ) + ref_logits = _run_model_logits( + flat_model, + tokens=ref_tokens, + input_pos=ref_pos, + group_ids=torch.zeros_like(ref_tokens), + parent_ids=torch.zeros_like(ref_tokens), + ) + ref_output_grad = torch.zeros_like(ref_logits) + ref_output_mask = torch.zeros( + ref_logits.shape[:2], + device=ref_logits.device, + dtype=torch.bool, + ) + if completion.length > 1: + ref_output_grad[ + :, prefix.length : prefix.length + completion.length - 1 + ] = output_grad[row : row + 1, completion.start : completion.end - 1] + ref_output_mask[ + :, prefix.length : prefix.length + completion.length - 1 + ] = True + ref_loss = stable_output_mse_loss( + ref_logits, + ref_output_grad, + mask=ref_output_mask.unsqueeze(-1), + denominator=loss_denominator, + ) + ref_loss.backward() + flat_loss_sum = ( + ref_loss.detach() + if flat_loss_sum is None + else flat_loss_sum + ref_loss.detach() + ) + if completion.length > 1: + packed_slice = packed_logits[ + row : row + 1, completion.start : completion.end - 1 + ] + ref_slice = ref_logits[ + :, prefix.length : prefix.length + completion.length - 1 + ] + logits_mean_abs_pct = max( + logits_mean_abs_pct, + mean_abs_pct(ref_slice, packed_slice), + ) + + assert flat_loss_sum is not None + grad_name, grad_pct = parameter_grad_mean_abs_pct_with_name( + flat_model, packed_model + ) + assert_mean_abs_pct(flat_loss_sum, packed_loss.detach(), "stable_loss") + assert logits_mean_abs_pct <= MEAN_ABS_PCT_THRESHOLD + assert grad_pct <= MEAN_ABS_PCT_THRESHOLD, grad_name + + +def _run_model_loss( + model: torch.nn.Module, + *, + tokens: torch.Tensor, + input_pos: torch.Tensor, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, + assistant_mask: torch.Tensor, +) -> tuple[torch.Tensor, torch.Tensor]: + logits = _run_model_logits( + model, + tokens=tokens, + input_pos=input_pos, + group_ids=group_ids, + parent_ids=parent_ids, + ) + attention_state = create_shared_prefix_state( + group_ids=group_ids, + parent_ids=parent_ids, + build_gdn_execution_spec=True, + ) + forward_kwargs = QWEN3_5_MOE_HANDLER.get_forward_kwargs( + model, + attention_bias=attention_state, + ) + attention_mask = torch.zeros((1, 1, 1, 1), dtype=torch.bool, device=tokens.device) + shifted_labels = shift_tensor(tokens, -100) + shifted_mask = shift_tensor(assistant_mask, False) + shifted_labels = torch.where( + shifted_mask, + shifted_labels, + torch.full_like(shifted_labels, -100), + ) + per_token_loss = model( + input_ids=tokens, + position_ids=input_pos, + attention_mask=attention_mask, + labels=shifted_labels, + **forward_kwargs, + ) + return logits.detach(), per_token_loss[shifted_mask].sum() + + +def _run_model_logits( + model: torch.nn.Module, + *, + tokens: torch.Tensor, + input_pos: torch.Tensor, + group_ids: torch.Tensor, + parent_ids: torch.Tensor, +) -> torch.Tensor: + attention_state = create_shared_prefix_state( + group_ids=group_ids, + parent_ids=parent_ids, + build_gdn_execution_spec=True, + ) + forward_kwargs = QWEN3_5_MOE_HANDLER.get_forward_kwargs( + model, + attention_bias=attention_state, + ) + attention_mask = torch.zeros((1, 1, 1, 1), dtype=torch.bool, device=tokens.device) + logits = model( + input_ids=tokens, + position_ids=input_pos, + attention_mask=attention_mask, + labels=None, + **forward_kwargs, + ) + return logits + + +def _make_matching_models() -> tuple[torch.nn.Module, torch.nn.Module]: + model_parallel_cuda_manual_seed(1234) + packed = _make_model() + model_parallel_cuda_manual_seed(5678) + flat = _make_model() + flat.load_state_dict(packed.state_dict()) + attach_main_grads(packed) + attach_main_grads(flat) + return packed, flat + + +def _make_model() -> torch.nn.Module: + assert Qwen3_5MoeVisionConfig is not None + provider = Qwen35VLMoEModelProvider( + num_layers=4, + hidden_size=64, + ffn_hidden_size=128, + moe_ffn_hidden_size=32, + moe_shared_expert_intermediate_size=16, + num_attention_heads=4, + num_query_groups=1, + kv_channels=16, + linear_key_head_dim=8, + linear_value_head_dim=16, + linear_num_key_heads=2, + linear_num_value_heads=4, + num_moe_experts=4, + moe_router_topk=2, + moe_aux_loss_coeff=0.0, + normalization="RMSNorm", + gated_linear_unit=True, + activation_func=F.silu, + add_bias_linear=False, + add_qkv_bias=False, + qk_layernorm=True, + hidden_dropout=0.0, + attention_dropout=0.0, + attention_output_gate=True, + experimental_attention_variant="gated_delta_net", + linear_attention_freq=4, + linear_conv_kernel_dim=2, + vocab_size=128, + seq_length=128, + position_embedding_type="mrope", + mrope_section=[1, 1, 0], + vision_config=Qwen3_5MoeVisionConfig(), + tensor_model_parallel_size=1, + expert_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=1, + params_dtype=GDN_CORRECTNESS_DTYPE, + ) + QWEN3_5_MOE_HANDLER.configure_provider_for_runtime(provider) + QWEN3_5_MOE_HANDLER.patch_provider(provider, None) + provider.finalize() + model = provider.provide_language_model(pre_process=True, post_process=True).cuda() + QWEN3_5_MOE_HANDLER.install_preprocess_patch([model]) + return model + + +@contextmanager +def _single_rank_model_parallel() -> Iterator[None]: + if is_initialized(): + pytest.skip("torch.distributed is already initialized in this process.") + torch.cuda.set_device(0) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{_find_free_port()}", + rank=0, + world_size=1, + ) + try: + ps.initialize_model_parallel( + tensor_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=1, + expert_model_parallel_size=1, + ) + yield + finally: + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + ps.destroy_model_parallel() + if is_initialized(): + destroy_process_group() + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_qwen35_gdn_topology_oracle.py b/tests/integration/megatron/gdn_shared_prefix/test_qwen35_gdn_topology_oracle.py new file mode 100644 index 000000000..2eff1264f --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_qwen35_gdn_topology_oracle.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from contextlib import redirect_stderr, redirect_stdout +import json +from pathlib import Path + +import pytest + +from ..model_support.oracle_harness import ( + LoraConfig, + PackedTensorConfig, + Topology, + VariantRunner, + VariantSpec, + _prune_case_artifacts, + _prune_topology_artifacts, + available_gpu_count, + case_config, +) + +REPO_ROOT = Path(__file__).resolve().parents[4] +LOG_PATH = REPO_ROOT / ".local" / "qwen35_gdn_cp_topology_oracle.log" + + +_CP_SIZES = (2, 4, 8) + + +@pytest.mark.parametrize("cp_size", _CP_SIZES) +def test_qwen35_gdn_shared_prefix_cp_topology_oracle( + cp_size: int, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Runs a real Qwen3.5 GDN-only RL stack under CP without self-attn CP.""" + gpu_count = available_gpu_count() + if gpu_count < cp_size: + pytest.skip(f"Need {cp_size} GPUs for CP{cp_size}; found {gpu_count}.") + + topology = Topology(tp=1, ep=1, etp=1, dp=1, sp=False, cp=cp_size) + config = case_config(base_model="Qwen/Qwen3.5-35B-A3B").model_copy( + update={ + "num_layers": 1, + "precision": "bf16", + "grad_accumulation_sequences": 1, + "lora": LoraConfig( + rank=1, + alpha=32, + target_modules=[ + "in_proj_qkv", + "in_proj_z", + "out_proj", + ], + ), + "packed_tensors": PackedTensorConfig( + num_sequences=2, + sequence_length=24, + prefill_tokens=4, + completion_branches_per_prefix=2, + decode_tokens=3, + decode_tokens_jitter=1, + vocab_high=128, + ), + } + ) + variant = VariantSpec( + name=f"qwen35_gdn_shared_prefix_cp{cp_size}", + objective="rl", + topology=topology, + ) + + monkeypatch.setenv("ART_MEGATRON_RECOMPUTE_GRANULARITY", "disabled") + monkeypatch.setenv("ART_MEGATRON_RECOMPUTE_METHOD", "disabled") + monkeypatch.setenv("ART_MEGATRON_RECOMPUTE_NUM_LAYERS", "disabled") + monkeypatch.setenv("ART_MEGATRON_RECOMPUTE_MODULES", "disabled") + + LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + with capsys.disabled(): + print(f"\nQwen3.5 GDN CP topology oracle log: {LOG_PATH}", flush=True) + with LOG_PATH.open("w", encoding="utf-8") as log_file: + with redirect_stdout(log_file), redirect_stderr(log_file): + runner = VariantRunner(objective="rl", case_config=config) + topology_dir = runner._run_topology( + topology=topology, + output_slug=variant.resolved_output_slug(), + mutation=None, + replay_bundle_dir=None, + capture_bundle_dir=None, + regenerate=True, + ) + manifest = json.loads((topology_dir / "manifest.json").read_text()) + assert manifest["topology"] == topology.slug() + assert manifest["num_layers"] == 1 + assert len(manifest["steps"]) == config.num_steps + assert "finished step_index=0" in (topology_dir / "worker.log").read_text() + _prune_topology_artifacts(topology_dir) + _prune_case_artifacts(Path(runner.case_artifacts.case_dir)) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_cp1_packed_vs_flattened.py b/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_cp1_packed_vs_flattened.py new file mode 100644 index 000000000..de6933582 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_cp1_packed_vs_flattened.py @@ -0,0 +1,275 @@ +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +import socket + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("megatron.bridge") +pytest.importorskip("megatron.bridge.models.qwen_vl.qwen35_vl_provider") + +from megatron.bridge.models.qwen_vl.qwen35_vl_provider import ( + Qwen3_5MoeVisionConfig, + Qwen35VLMoEModelProvider, +) +from megatron.core import parallel_state as ps +from megatron.core.ssm.gated_delta_net import GatedDeltaNet +from megatron.core.tensor_parallel.random import model_parallel_cuda_manual_seed +from torch.distributed import ( + DistNetworkError, + destroy_process_group, + init_process_group, + is_initialized, +) + +from .cases import default_phase0_cases +from .metrics import ( + GDN_CORRECTNESS_DTYPE, + MEAN_ABS_PCT_MISMATCH_THRESHOLD, + assert_real_gdn_metrics, + mean_abs_pct, +) +from .packed_layout import build_phase0_packed_tensors +from .real_gdn_oracle import ( + attach_main_grads, + compare_real_gdn_cp1_to_flattened, + compare_real_gdn_cp1_to_flattened_with_output_grad, + run_real_gdn_flattened_reference, + run_real_gdn_physical_stream, + zero_parameter_grads, +) + + +@pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA is required for real Megatron/FLA GDN oracle coverage.", +) +def test_real_qwen35_gdn_cp1_matches_flattened_and_rejects_physical() -> None: + with _single_rank_model_parallel(): + packed_gdn, flat_gdn = _make_matching_qwen35_gdn_pair() + device = torch.device("cuda") + for case_index, case in enumerate(default_phase0_cases(conv_width=2)): + zero_parameter_grads(packed_gdn) + zero_parameter_grads(flat_gdn) + tensors = build_phase0_packed_tensors(case) + group_ids = tensors["group_ids"].to(device) + parent_ids = tensors["parent_ids"].to(device) + assistant_mask = tensors["assistant_mask"].to(device) + hidden_states = torch.randn( + case.sequence_length, + len(case.rows), + 64, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=torch.Generator(device=device).manual_seed( + 20260424 + case_index + ), + ) + + metrics = compare_real_gdn_cp1_to_flattened( + packed_gdn=packed_gdn, + flat_gdn=flat_gdn, + hidden_states=hidden_states, + group_ids=group_ids, + parent_ids=parent_ids, + assistant_mask=assistant_mask, + ) + + assert_real_gdn_metrics(metrics, case.name) + + real_token_mask = (group_ids != -1).transpose(0, 1).unsqueeze(-1) + output_grads = { + "random_all_real_tokens": ( + torch.randn( + hidden_states.shape, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=torch.Generator(device=device).manual_seed( + 20270424 + case_index + ), + ) + * real_token_mask + ) + } + if case.name == "ragged_family_mix": + output_grads.update( + { + "prefix_only": _expanded_output_mask( + group_ids == parent_ids, hidden_states.shape[-1] + ), + "suffix_only": _expanded_output_mask( + group_ids != parent_ids, hidden_states.shape[-1] + ), + "single_token_channel": _single_token_channel_grad( + hidden_states, group_ids != -1 + ), + } + ) + for name, output_grad in output_grads.items(): + zero_parameter_grads(packed_gdn) + zero_parameter_grads(flat_gdn) + upstream_metrics = compare_real_gdn_cp1_to_flattened_with_output_grad( + packed_gdn=packed_gdn, + flat_gdn=flat_gdn, + hidden_states=hidden_states, + group_ids=group_ids, + parent_ids=parent_ids, + output_grad=output_grad, + ) + + assert_real_gdn_metrics(upstream_metrics, f"{case.name}:{name}") + + if case.name == "ragged_family_mix": + with torch.no_grad(): + flattened = run_real_gdn_flattened_reference( + flat_gdn, + hidden_states, + group_ids=group_ids, + parent_ids=parent_ids, + ) + physical = run_real_gdn_physical_stream( + flat_gdn, + hidden_states, + group_ids=group_ids, + ) + assert ( + mean_abs_pct( + flattened.transpose(0, 1)[assistant_mask], + physical.transpose(0, 1)[assistant_mask], + ) + > MEAN_ABS_PCT_MISMATCH_THRESHOLD + ), case.name + + +def _make_matching_qwen35_gdn_pair( + *, params_dtype: torch.dtype = GDN_CORRECTNESS_DTYPE +) -> tuple[GatedDeltaNet, GatedDeltaNet]: + model_parallel_cuda_manual_seed(1234) + packed_model = _make_qwen35_language_model(params_dtype=params_dtype) + model_parallel_cuda_manual_seed(5678) + flat_model = _make_qwen35_language_model(params_dtype=params_dtype) + packed_gdn = _first_gdn(packed_model) + flat_gdn = _first_gdn(flat_model) + flat_gdn.load_state_dict(packed_gdn.state_dict()) + attach_main_grads(packed_gdn) + attach_main_grads(flat_gdn) + return packed_gdn, flat_gdn + + +def _make_qwen35_language_model( + *, params_dtype: torch.dtype = GDN_CORRECTNESS_DTYPE +) -> torch.nn.Module: + assert Qwen3_5MoeVisionConfig is not None + provider = Qwen35VLMoEModelProvider( + num_layers=4, + hidden_size=64, + ffn_hidden_size=128, + moe_ffn_hidden_size=32, + moe_shared_expert_intermediate_size=16, + num_attention_heads=4, + num_query_groups=1, + kv_channels=16, + linear_key_head_dim=8, + linear_value_head_dim=16, + linear_num_key_heads=2, + linear_num_value_heads=4, + num_moe_experts=4, + moe_router_topk=2, + normalization="RMSNorm", + gated_linear_unit=True, + add_bias_linear=False, + add_qkv_bias=False, + qk_layernorm=True, + hidden_dropout=0.0, + attention_dropout=0.0, + attention_output_gate=True, + experimental_attention_variant="gated_delta_net", + linear_attention_freq=4, + linear_conv_kernel_dim=2, + vocab_size=128, + seq_length=128, + position_embedding_type="mrope", + vision_config=Qwen3_5MoeVisionConfig(), + tensor_model_parallel_size=1, + expert_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=1, + params_dtype=params_dtype, + ) + provider.finalize() + return provider.provide_language_model(pre_process=True, post_process=True).cuda() + + +def _first_gdn(model: torch.nn.Module) -> GatedDeltaNet: + for module in model.modules(): + if isinstance(module, GatedDeltaNet): + return module + raise AssertionError("expected Qwen3.5 provider to build at least one GDN layer") + + +def _expanded_output_mask(mask: torch.Tensor, hidden_size: int) -> torch.Tensor: + return ( + mask.transpose(0, 1) + .unsqueeze(-1) + .expand(mask.shape[1], mask.shape[0], hidden_size) + .to(dtype=GDN_CORRECTNESS_DTYPE) + ) + + +def _single_token_channel_grad( + hidden_states: torch.Tensor, real_mask: torch.Tensor +) -> torch.Tensor: + row, position = real_mask.nonzero()[real_mask.sum() // 2].tolist() + output_grad = torch.zeros_like(hidden_states) + output_grad[position, row, 0] = 1.0 + return output_grad + + +@contextmanager +def _single_rank_model_parallel() -> Iterator[None]: + if is_initialized(): + pytest.skip("torch.distributed is already initialized in this process.") + torch.cuda.set_device(0) + _init_single_rank_process_group() + try: + ps.initialize_model_parallel( + tensor_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=1, + expert_model_parallel_size=1, + ) + yield + finally: + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + ps.destroy_model_parallel() + if is_initialized(): + destroy_process_group() + + +def _init_single_rank_process_group() -> None: + last_error: DistNetworkError | None = None + for _ in range(16): + try: + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{_find_free_port()}", + rank=0, + world_size=1, + ) + return + except DistNetworkError as error: + if "EADDRINUSE" not in str(error): + raise + last_error = error + if is_initialized(): + destroy_process_group() + if last_error is not None: + raise last_error + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_native_fla_cp.py b/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_native_fla_cp.py new file mode 100644 index 000000000..e0d164c56 --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_native_fla_cp.py @@ -0,0 +1,607 @@ +from __future__ import annotations + +from pathlib import Path +import socket +from typing import cast + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("megatron.bridge") +pytest.importorskip("megatron.bridge.models.qwen_vl.qwen35_vl_provider") + +from megatron.bridge.models.qwen_vl.qwen35_vl_provider import ( # noqa: E402 + Qwen3_5MoeVisionConfig, + Qwen35VLMoEModelProvider, +) +from megatron.core import parallel_state as ps # noqa: E402 +from megatron.core.ssm.gated_delta_net import GatedDeltaNet # noqa: E402 +from megatron.core.tensor_parallel.random import ( # noqa: E402 + model_parallel_cuda_manual_seed, +) +from torch.distributed import destroy_process_group, init_process_group # noqa: E402 +import torch.multiprocessing as mp # noqa: E402 + +from art.megatron.gdn.gdn_shared_prefix import ( # noqa: E402 + GdnPlannerConfig, + GdnSegmentBucketPlan, + build_gdn_rank_execution_plan, + parse_gdn_shared_prefix_segments, +) +from art.megatron.gdn.operator import ( # noqa: E402 + _project_gdn_inputs, + _zero_conv_state, + _zero_recurrent_state, + run_gdn_bucket, + run_gdn_layer, +) + +from .cases import GdnFamilyShape, GdnPackedRowShape, GdnPhase0Case # noqa: E402 +from .metrics import ( # noqa: E402 + GDN_CORRECTNESS_DTYPE, + MEAN_ABS_PCT_THRESHOLD, + assert_mean_abs_pct, + parameter_grad_mean_abs_pct_with_name, +) +from .packed_layout import build_phase0_packed_tensors # noqa: E402 +from .real_gdn_oracle import ( # noqa: E402 + attach_main_grads, + zero_parameter_grads, +) + +_CP_SIZES = ( + 2, + 4, + pytest.param( + 8, + marks=pytest.mark.skipif( + torch.cuda.device_count() < 8, + reason="At least eight CUDA devices are required for CP8 coverage.", + ), + ), +) + + +@pytest.mark.skipif( + not torch.cuda.is_available() or torch.cuda.device_count() < 4, + reason="At least four CUDA devices are required for native FLA CP GDN coverage.", +) +@pytest.mark.parametrize("cp_size", _CP_SIZES) +def test_real_qwen35_gdn_native_fla_cp_prepared_varlen_batch_matches_single_rank( + cp_size: int, tmp_path: Path +) -> None: + port = _find_free_port() + mp.spawn( + _native_gdn_cp_prepared_varlen_worker, + args=(cp_size, port, str(tmp_path)), + nprocs=cp_size, + join=True, + ) + for rank in range(cp_size): + assert (tmp_path / f"prepared_varlen_rank_{rank}.ok").read_text() == "ok\n" + + +@pytest.mark.skipif( + not torch.cuda.is_available() or torch.cuda.device_count() < 4, + reason="At least four CUDA devices are required for native packed CP GDN coverage.", +) +@pytest.mark.parametrize("cp_size", _CP_SIZES) +def test_real_qwen35_gdn_native_cp_packed_layer_matches_cp1( + cp_size: int, tmp_path: Path +) -> None: + port = _find_free_port() + mp.spawn( + _native_gdn_cp_packed_layer_worker, + args=(cp_size, port, str(tmp_path)), + nprocs=cp_size, + join=True, + ) + for rank in range(cp_size): + assert (tmp_path / f"packed_layer_rank_{rank}.ok").read_text() == "ok\n" + + +def _native_gdn_cp_packed_layer_worker( + rank: int, + cp_size: int, + port: int, + output_dir: str, +) -> None: + torch.cuda.set_device(rank) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{port}", + rank=rank, + world_size=cp_size, + ) + try: + ps.initialize_model_parallel( + tensor_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=cp_size, + expert_model_parallel_size=1, + ) + ref_gdn, cp_gdn = _make_matching_gdn_pair(cp_size=cp_size) + zero_parameter_grads(ref_gdn) + zero_parameter_grads(cp_gdn) + case = _packed_native_cp_case() + tensors = build_phase0_packed_tensors(case) + group_ids = tensors["group_ids"].cuda() + parent_ids = tensors["parent_ids"].cuda() + spec = parse_gdn_shared_prefix_segments( + group_ids, parent_ids, min_completions_per_family=0 + ) + plan = build_gdn_rank_execution_plan( + spec, + device=group_ids.device, + cp_rank=rank, + cp_size=cp_size, + planner_config=GdnPlannerConfig( + cp_chain_min_tokens_per_rank=16, + cp_chain_min_total_tokens=128, + cp_chain_min_prefix_only_tokens=128, + # This test is the native chain correctness guard, so force the + # planner onto chain prefix and completion buckets. + planner_chain_bucket_ms=0.0, + planner_chain_token_ms=0.0, + planner_local_bucket_ms=1.0, + planner_local_token_ms=1.0, + cp_chain_min_score_delta_ms=0.0, + ), + ) + assert plan.chain_prefix_buckets + assert plan.chain_completion_buckets + hidden, output_grad = _packed_hidden_and_grad(case, cp_size) + ref_hidden = hidden.clone().detach().requires_grad_(True) + ref_out, _ = run_gdn_layer( + ref_gdn, + ref_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + ) + ref_loss = (ref_out * output_grad).sum() + ref_loss.backward() + + flat_hidden = hidden.transpose(0, 1).reshape(-1, hidden.shape[-1]) + flat_grad = output_grad.transpose(0, 1).reshape(-1, output_grad.shape[-1]) + local_index = torch.tensor( + plan.attention_token_indices, device=hidden.device, dtype=torch.long + ) + local_hidden = ( + flat_hidden.index_select(0, local_index) + .unsqueeze(1) + .contiguous() + .detach() + .requires_grad_(True) + ) + local_output_grad = ( + flat_grad.index_select(0, local_index).unsqueeze(1).contiguous() + ) + cp_out, _ = run_gdn_layer( + cp_gdn, + local_hidden, + group_ids=group_ids, + parent_ids=parent_ids, + execution_spec=spec, + execution_plan=plan, + cp_group=torch.distributed.group.WORLD, + ) + cp_loss = (cp_out * local_output_grad).sum() + cp_loss.backward() + _all_reduce_parameter_grads(cp_gdn) + + flat_ref_out = ref_out.detach().transpose(0, 1).reshape(-1, ref_out.shape[-1]) + assert_mean_abs_pct( + flat_ref_out.index_select(0, local_index), + cp_out.detach().squeeze(1), + "packed_output", + ) + assert local_hidden.grad is not None + assert ref_hidden.grad is not None + flat_ref_grad = ref_hidden.grad.transpose(0, 1).reshape(-1, hidden.shape[-1]) + assert_mean_abs_pct( + flat_ref_grad.index_select(0, local_index), + local_hidden.grad.squeeze(1), + "packed_hidden_grad", + ) + param_name, param_pct = parameter_grad_mean_abs_pct_with_name(ref_gdn, cp_gdn) + assert param_pct <= MEAN_ABS_PCT_THRESHOLD, param_name + Path(output_dir, f"packed_layer_rank_{rank}.ok").write_text("ok\n") + finally: + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + ps.destroy_model_parallel() + destroy_process_group() + + +def _native_gdn_cp_prepared_varlen_worker( + rank: int, + cp_size: int, + port: int, + output_dir: str, +) -> None: + torch.cuda.set_device(rank) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{port}", + rank=rank, + world_size=cp_size, + ) + try: + ps.initialize_model_parallel( + tensor_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=cp_size, + expert_model_parallel_size=1, + ) + ref_gdn, cp_gdn = _make_matching_gdn_pair(cp_size=cp_size) + zero_parameter_grads(ref_gdn) + zero_parameter_grads(cp_gdn) + hidden, lengths = _varlen_hidden_and_lengths(cp_size) + with torch.no_grad(): + qkv_full, _, beta_full, recurrent_g_full = _project_gdn_inputs( + ref_gdn, hidden + ) + bucket = _varlen_bucket(lengths, device=hidden.device) + conv0_ref = _zero_conv_state( + ref_gdn, hidden, batch_size=int(lengths.numel()) + ).requires_grad_(True) + rec0_ref = _zero_recurrent_state( + ref_gdn, hidden, batch_size=int(lengths.numel()) + ).requires_grad_(True) + conv_grad = torch.randn_like(conv0_ref) + rec_grad = torch.randn_like(rec0_ref) + output_grad = torch.randn( + 1, + int(lengths.sum().item()), + cast(int, cp_gdn.num_value_heads) // cast(int, cp_gdn.tp_size), + cast(int, cp_gdn.value_head_dim), + device=hidden.device, + dtype=GDN_CORRECTNESS_DTYPE, + ) + torch.distributed.broadcast(conv_grad, src=0) + torch.distributed.broadcast(rec_grad, src=0) + torch.distributed.broadcast(output_grad, src=0) + + full_offsets = tuple((0, int(length.item())) for length in lengths) + ref_qkv = _cat_time_slices(qkv_full, full_offsets).requires_grad_(True) + ref_beta = _cat_time_slices(beta_full, full_offsets).requires_grad_(True) + ref_g = _cat_time_slices(recurrent_g_full, full_offsets).requires_grad_(True) + ref_out, ref_conv, ref_rec = run_gdn_bucket( + bucket, + (ref_qkv, ref_beta, ref_g), + (conv0_ref, rec0_ref), + gdn=ref_gdn, + output_final_state=True, + ) + assert ref_conv is not None + assert ref_rec is not None + ref_loss = ( + (ref_out * output_grad).sum() + + (ref_conv * conv_grad).sum() + + (ref_rec * rec_grad).sum() + ) + ref_loss.backward() + + local_offsets = _rank_varlen_offsets(lengths, rank=rank, cp_size=cp_size) + local_lengths = torch.tensor( + [end - start for start, end in local_offsets], + device=hidden.device, + dtype=torch.long, + ) + local_bucket = _varlen_bucket( + local_lengths, + device=hidden.device, + lengths_by_rank_cpu=_varlen_lengths_by_rank_cpu( + lengths, + cp_size=cp_size, + ), + ) + local_qkv = _cat_time_slices(qkv_full, local_offsets).requires_grad_(True) + local_beta = _cat_time_slices(beta_full, local_offsets).requires_grad_(True) + local_g = _cat_time_slices(recurrent_g_full, local_offsets).requires_grad_(True) + conv0_cp = conv0_ref.detach().clone().requires_grad_(True) + rec0_cp = rec0_ref.detach().clone().requires_grad_(True) + cp_out, cp_conv, cp_rec = run_gdn_bucket( + local_bucket, + (local_qkv, local_beta, local_g), + (conv0_cp, rec0_cp), + gdn=cp_gdn, + group=torch.distributed.group.WORLD, + recurrent_cp=True, + output_final_state=True, + ) + assert cp_conv is not None + assert cp_rec is not None + local_output_grad = _cat_flat_slices( + output_grad, bucket.cu_seqlens, local_offsets + ) + cp_loss = ( + (cp_out * local_output_grad).sum() + + (cp_conv * (conv_grad / cp_size)).sum() + + (cp_rec * (rec_grad / cp_size)).sum() + ) + cp_loss.backward() + _all_reduce_parameter_grads(cp_gdn) + + assert_mean_abs_pct( + _cat_flat_slices(ref_out, bucket.cu_seqlens, local_offsets), + cp_out, + "prepared_varlen_output", + ) + assert_mean_abs_pct(ref_conv, cp_conv, "prepared_varlen_conv_final") + assert_mean_abs_pct(ref_rec, cp_rec, "prepared_varlen_recurrent_final") + assert ref_qkv.grad is not None + assert ref_beta.grad is not None + assert ref_g.grad is not None + _assert_compact_grad_slices( + local_qkv, ref_qkv.grad, bucket.cu_seqlens, local_offsets, "qkv" + ) + _assert_compact_grad_slices( + local_beta, ref_beta.grad, bucket.cu_seqlens, local_offsets, "beta" + ) + _assert_compact_grad_slices( + local_g, ref_g.grad, bucket.cu_seqlens, local_offsets, "g" + ) + assert conv0_cp.grad is not None + assert conv0_ref.grad is not None + assert rec0_cp.grad is not None + assert rec0_ref.grad is not None + assert_mean_abs_pct(conv0_ref.grad, conv0_cp.grad, "prepared_conv_grad") + assert_mean_abs_pct(rec0_ref.grad, rec0_cp.grad, "prepared_rec_grad") + param_name, param_pct = parameter_grad_mean_abs_pct_with_name(ref_gdn, cp_gdn) + assert param_pct <= MEAN_ABS_PCT_THRESHOLD, param_name + Path(output_dir, f"prepared_varlen_rank_{rank}.ok").write_text("ok\n") + finally: + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + ps.destroy_model_parallel() + destroy_process_group() + + +def _make_matching_gdn_pair( + *, cp_size: int, params_dtype: torch.dtype = GDN_CORRECTNESS_DTYPE +) -> tuple[GatedDeltaNet, GatedDeltaNet]: + model_parallel_cuda_manual_seed(1234) + ref_model = _make_model(cp_size=cp_size, params_dtype=params_dtype) + model_parallel_cuda_manual_seed(5678) + cp_model = _make_model(cp_size=cp_size, params_dtype=params_dtype) + ref_gdn = _first_gdn(ref_model) + cp_gdn = _first_gdn(cp_model) + cp_gdn.load_state_dict(ref_gdn.state_dict()) + attach_main_grads(ref_gdn) + attach_main_grads(cp_gdn) + return ref_gdn, cp_gdn + + +def _make_model( + *, cp_size: int, params_dtype: torch.dtype = GDN_CORRECTNESS_DTYPE +) -> torch.nn.Module: + assert Qwen3_5MoeVisionConfig is not None + provider = Qwen35VLMoEModelProvider( + num_layers=4, + hidden_size=64, + ffn_hidden_size=128, + moe_ffn_hidden_size=32, + moe_shared_expert_intermediate_size=16, + num_attention_heads=4, + num_query_groups=1, + kv_channels=16, + linear_key_head_dim=8, + linear_value_head_dim=16, + linear_num_key_heads=2, + linear_num_value_heads=4, + num_moe_experts=4, + moe_router_topk=2, + normalization="RMSNorm", + gated_linear_unit=True, + add_bias_linear=False, + add_qkv_bias=False, + qk_layernorm=True, + hidden_dropout=0.0, + attention_dropout=0.0, + attention_output_gate=True, + experimental_attention_variant="gated_delta_net", + linear_attention_freq=4, + linear_conv_kernel_dim=2, + vocab_size=128, + seq_length=128, + position_embedding_type="mrope", + vision_config=Qwen3_5MoeVisionConfig(), + tensor_model_parallel_size=1, + expert_model_parallel_size=1, + pipeline_model_parallel_size=1, + # Megatron's stock GDN config still rejects CP. This test owns CP at the + # ART wrapper boundary and uses the distributed WORLD group explicitly. + context_parallel_size=1, + params_dtype=params_dtype, + ) + provider.finalize() + return provider.provide_language_model(pre_process=True, post_process=True).cuda() + + +def _first_gdn(model: torch.nn.Module) -> GatedDeltaNet: + for module in model.modules(): + if isinstance(module, GatedDeltaNet): + return module + raise AssertionError("expected Qwen3.5 provider to build a GDN layer") + + +def _packed_native_cp_case() -> GdnPhase0Case: + return GdnPhase0Case( + name="native_cp_packed_varying", + sequence_length=3072, + rows=( + GdnPackedRowShape( + families=( + GdnFamilyShape(prefix_length=1024, suffix_lengths=(512, 512)), + GdnFamilyShape(prefix_length=512, suffix_lengths=(512,)), + ) + ), + ), + seed=67, + description="Mixed long CP-chain and short local-fork GDN segments.", + ) + + +def _packed_hidden_and_grad( + case: GdnPhase0Case, cp_size: int, *, dtype: torch.dtype = GDN_CORRECTNESS_DTYPE +) -> tuple[torch.Tensor, torch.Tensor]: + device = torch.device("cuda") + generator = torch.Generator(device=device).manual_seed(20490426 + cp_size) + hidden = torch.randn( + case.sequence_length, + len(case.rows), + 64, + device=device, + dtype=dtype, + generator=generator, + ) + output_grad = torch.randn( + hidden.shape, + device=device, + dtype=dtype, + generator=generator, + ) + torch.distributed.broadcast(hidden, src=0) + torch.distributed.broadcast(output_grad, src=0) + return hidden, output_grad + + +def _varlen_hidden_and_lengths(cp_size: int) -> tuple[torch.Tensor, torch.Tensor]: + device = torch.device("cuda") + lengths = torch.tensor((512, 1024, 1536), device=device, dtype=torch.long) + generator = torch.Generator(device=device).manual_seed(20480426 + cp_size) + hidden = torch.randn( + int(lengths.max().item()), + int(lengths.numel()), + 64, + device=device, + dtype=GDN_CORRECTNESS_DTYPE, + generator=generator, + ) + torch.distributed.broadcast(hidden, src=0) + return hidden, lengths + + +def _varlen_bucket( + lengths: torch.Tensor, + *, + device: torch.device, + lengths_by_rank_cpu: torch.Tensor | None = None, +) -> GdnSegmentBucketPlan: + max_len = int(lengths.max().item()) + lengths_cpu = lengths.detach().cpu() + cu_seqlens_cpu = torch.cat( + [lengths_cpu.new_zeros(1), torch.cumsum(lengths_cpu, dim=0)] + ) + offsets = torch.arange(max_len, device=device, dtype=torch.long).unsqueeze(1) + real_mask = offsets < lengths.unsqueeze(0) + return GdnSegmentBucketPlan( + length=max_len, + lengths=lengths, + lengths_cpu=lengths_cpu, + lengths_by_rank_cpu=lengths_by_rank_cpu, + real_mask=real_mask, + cu_seqlens=cu_seqlens_cpu.to(device=device), + cu_seqlens_cpu=cu_seqlens_cpu, + row_indices=torch.arange(int(lengths.numel()), device=device, dtype=torch.long) + .unsqueeze(0) + .expand(max_len, -1) + .contiguous(), + position_indices=offsets.expand(-1, int(lengths.numel())).contiguous(), + family_indices=torch.arange( + int(lengths.numel()), device=device, dtype=torch.long + ), + real_token_count_static=int(lengths.sum().item()), + ) + + +def _rank_varlen_offsets( + lengths: torch.Tensor, *, rank: int, cp_size: int +) -> tuple[tuple[int, int], ...]: + offsets = [] + for length in (int(value) for value in lengths.detach().cpu().tolist()): + start = (length * rank) // cp_size + end = (length * (rank + 1)) // cp_size + if start >= end: + raise ValueError("test varlen chain unexpectedly produced an empty shard") + offsets.append((start, end)) + return tuple(offsets) + + +def _varlen_lengths_by_rank_cpu(lengths: torch.Tensor, *, cp_size: int) -> torch.Tensor: + return torch.tensor( + [ + [ + end - start + for start, end in _rank_varlen_offsets( + lengths, + rank=rank, + cp_size=cp_size, + ) + ] + for rank in range(cp_size) + ], + dtype=torch.long, + ) + + +def _cat_time_slices( + tensor: torch.Tensor, offsets: tuple[tuple[int, int], ...] +) -> torch.Tensor: + return torch.cat( + [tensor[index, start:end] for index, (start, end) in enumerate(offsets)], + dim=0, + ).contiguous() + + +def _cat_flat_slices( + tensor: torch.Tensor, + cu_seqlens: torch.Tensor, + offsets: tuple[tuple[int, int], ...], +) -> torch.Tensor: + pieces = [] + for chain, (start, end) in enumerate(offsets): + base = int(cu_seqlens[chain].item()) + pieces.append(tensor[:, base + start : base + end]) + return torch.cat(pieces, dim=1).contiguous() + + +def _assert_compact_grad_slices( + local: torch.Tensor, + reference_grad: torch.Tensor, + cu_seqlens: torch.Tensor, + offsets: tuple[tuple[int, int], ...], + name: str, +) -> None: + assert local.grad is not None, name + expected = _cat_flat_slices( + reference_grad.unsqueeze(0), cu_seqlens, offsets + ).squeeze(0) + assert_mean_abs_pct(expected, local.grad, name) + + +def _all_reduce_parameter_grads(module: torch.nn.Module) -> None: + world_size = torch.distributed.get_world_size() + for parameter in module.parameters(): + has_grad = torch.tensor( + 1 if parameter.grad is not None else 0, + device=parameter.device, + dtype=torch.int32, + ) + torch.distributed.all_reduce(has_grad) + grad_ranks = int(has_grad.item()) + if grad_ranks == world_size: + assert parameter.grad is not None + torch.distributed.all_reduce(parameter.grad) + elif grad_ranks: + if parameter.grad is None: + parameter.grad = torch.zeros_like(parameter) + torch.distributed.all_reduce(parameter.grad) + main_grad = getattr(parameter, "main_grad", None) + if main_grad is not None: + torch.distributed.all_reduce(main_grad) + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) diff --git a/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_tp_lora.py b/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_tp_lora.py new file mode 100644 index 000000000..c4bd99abc --- /dev/null +++ b/tests/integration/megatron/gdn_shared_prefix/test_real_gdn_tp_lora.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +from pathlib import Path +import socket + +import pytest + +torch = pytest.importorskip("torch") +pytest.importorskip("megatron.bridge") +pytest.importorskip("megatron.bridge.models.qwen_vl.qwen35_vl_provider") + +from megatron.bridge.models.qwen_vl.qwen35_vl_provider import ( # noqa: E402 + Qwen3_5MoeVisionConfig, + Qwen35VLMoEModelProvider, +) +from megatron.core import parallel_state as ps # noqa: E402 +from megatron.core.ssm.gated_delta_net import GatedDeltaNet # noqa: E402 +from megatron.core.tensor_parallel.random import ( # noqa: E402 + model_parallel_cuda_manual_seed, +) +from torch.distributed import destroy_process_group, init_process_group # noqa: E402 +import torch.multiprocessing as mp # noqa: E402 + +from art.megatron.lora import apply_lora_adapters # noqa: E402 +from art.megatron.model_support import QWEN3_5_MOE_SPEC # noqa: E402 +from art.megatron.model_support.handlers import QWEN3_5_MOE_HANDLER # noqa: E402 + +from .cases import GdnPhase0Case, default_phase0_cases # noqa: E402 +from .metrics import GDN_CORRECTNESS_DTYPE, assert_real_gdn_metrics # noqa: E402 +from .packed_layout import build_phase0_packed_tensors # noqa: E402 +from .real_gdn_oracle import ( # noqa: E402 + attach_main_grads, + compare_real_gdn_cp1_to_flattened, + zero_parameter_grads, +) +from .test_real_gdn_cp1_packed_vs_flattened import ( # noqa: E402 + _single_rank_model_parallel, +) + + +@pytest.mark.skipif( + not torch.cuda.is_available(), + reason="CUDA is required for real Megatron/FLA GDN LoRA coverage.", +) +def test_real_qwen35_gdn_lora_gradients_match_flattened() -> None: + case = next( + case + for case in default_phase0_cases(conv_width=2) + if case.name == "ragged_family_mix" + ) + with _single_rank_model_parallel(): + packed_gdn, flat_gdn = _make_matching_gdn_pair(tp_size=1, lora=True) + tensors = build_phase0_packed_tensors(case) + metrics = compare_real_gdn_cp1_to_flattened( + packed_gdn=packed_gdn, + flat_gdn=flat_gdn, + hidden_states=_hidden(case), + group_ids=tensors["group_ids"].cuda(), + parent_ids=tensors["parent_ids"].cuda(), + assistant_mask=tensors["assistant_mask"].cuda(), + ) + assert_real_gdn_metrics(metrics, "lora") + assert _gdn_lora_grad_names(packed_gdn) + + +@pytest.mark.skipif( + not torch.cuda.is_available() or torch.cuda.device_count() < 2, + reason="At least two CUDA devices are required for TP2 GDN coverage.", +) +def test_real_qwen35_gdn_tp2_gradients_match_flattened(tmp_path: Path) -> None: + port = _find_free_port() + mp.spawn( + _tp2_worker, + args=(port, str(tmp_path)), + nprocs=2, + join=True, + ) + for rank in range(2): + assert (tmp_path / f"rank_{rank}.ok").read_text() == "ok\n" + + +def _tp2_worker(rank: int, port: int, output_dir: str) -> None: + torch.cuda.set_device(rank) + init_process_group( + backend="nccl", + init_method=f"tcp://127.0.0.1:{port}", + rank=rank, + world_size=2, + ) + try: + ps.initialize_model_parallel( + tensor_model_parallel_size=2, + pipeline_model_parallel_size=1, + context_parallel_size=1, + expert_model_parallel_size=1, + ) + case = next( + case + for case in default_phase0_cases(conv_width=2) + if case.name == "multi_family_repeated" + ) + packed_gdn, flat_gdn = _make_matching_gdn_pair(tp_size=2, lora=False) + tensors = build_phase0_packed_tensors(case) + metrics = compare_real_gdn_cp1_to_flattened( + packed_gdn=packed_gdn, + flat_gdn=flat_gdn, + hidden_states=_hidden(case, seed=20410426 + rank), + group_ids=tensors["group_ids"].cuda(), + parent_ids=tensors["parent_ids"].cuda(), + assistant_mask=tensors["assistant_mask"].cuda(), + ) + assert_real_gdn_metrics(metrics, "tp2") + Path(output_dir, f"rank_{rank}.ok").write_text("ok\n") + finally: + if getattr(ps, "model_parallel_is_initialized", lambda: False)(): + ps.destroy_model_parallel() + destroy_process_group() + + +def _make_matching_gdn_pair( + *, tp_size: int, lora: bool +) -> tuple[GatedDeltaNet, GatedDeltaNet]: + model_parallel_cuda_manual_seed(1234) + packed_model = _make_model(tp_size=tp_size) + model_parallel_cuda_manual_seed(5678) + flat_model = _make_model(tp_size=tp_size) + if lora: + apply_lora_adapters([packed_model], _make_provider(tp_size=tp_size)) + apply_lora_adapters([flat_model], _make_provider(tp_size=tp_size)) + _randomize_lora_parameters(packed_model) + flat_model.load_state_dict(packed_model.state_dict()) + packed_gdn = _first_gdn(packed_model) + flat_gdn = _first_gdn(flat_model) + attach_main_grads(packed_gdn) + attach_main_grads(flat_gdn) + zero_parameter_grads(packed_gdn) + zero_parameter_grads(flat_gdn) + return packed_gdn, flat_gdn + + +def _make_model(*, tp_size: int) -> torch.nn.Module: + return ( + _make_provider(tp_size=tp_size) + .provide_language_model(pre_process=True, post_process=True) + .cuda() + ) + + +def _make_provider(*, tp_size: int) -> Qwen35VLMoEModelProvider: + assert Qwen3_5MoeVisionConfig is not None + provider = Qwen35VLMoEModelProvider( + num_layers=4, + hidden_size=64, + ffn_hidden_size=128, + moe_ffn_hidden_size=32, + moe_shared_expert_intermediate_size=16, + num_attention_heads=4, + num_query_groups=tp_size, + kv_channels=16, + linear_key_head_dim=8, + linear_value_head_dim=16, + linear_num_key_heads=2, + linear_num_value_heads=4, + num_moe_experts=4, + moe_router_topk=2, + normalization="RMSNorm", + gated_linear_unit=True, + add_bias_linear=False, + add_qkv_bias=False, + qk_layernorm=True, + hidden_dropout=0.0, + attention_dropout=0.0, + attention_output_gate=True, + experimental_attention_variant="gated_delta_net", + linear_attention_freq=4, + linear_conv_kernel_dim=2, + vocab_size=128, + seq_length=128, + position_embedding_type="mrope", + vision_config=Qwen3_5MoeVisionConfig(), + tensor_model_parallel_size=tp_size, + expert_model_parallel_size=1, + pipeline_model_parallel_size=1, + context_parallel_size=1, + params_dtype=GDN_CORRECTNESS_DTYPE, + ) + provider.finalize() + setattr(provider, "_art_model_support_handler", QWEN3_5_MOE_HANDLER) + setattr(provider, "_art_model_support_spec", QWEN3_5_MOE_SPEC) + return provider + + +def _first_gdn(model: torch.nn.Module) -> GatedDeltaNet: + for module in model.modules(): + if isinstance(module, GatedDeltaNet): + return module + raise AssertionError("expected Qwen3.5 provider to build a GDN layer") + + +def _hidden(case: GdnPhase0Case, seed: int = 20400426) -> torch.Tensor: + return torch.randn( + case.sequence_length, + len(case.rows), + 64, + device="cuda", + dtype=GDN_CORRECTNESS_DTYPE, + generator=torch.Generator(device="cuda").manual_seed(seed), + ) + + +def _randomize_lora_parameters(model: torch.nn.Module) -> None: + generator = torch.Generator(device="cuda").manual_seed(20420426) + with torch.no_grad(): + for name, parameter in model.named_parameters(): + if name.endswith(("A_T", "B_T")): + parameter.copy_( + torch.randn( + parameter.shape, device=parameter.device, generator=generator + ) + * 0.03 + ) + + +def _gdn_lora_grad_names(gdn: torch.nn.Module) -> tuple[str, ...]: + return tuple( + name + for name, parameter in gdn.named_parameters() + if name.endswith(("A_T", "B_T")) + and parameter.grad is not None + and bool(parameter.grad.abs().max().item() > 0) + ) + + +def _find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) diff --git a/tests/integration/megatron/lora/test_lora_disk_codecs.py b/tests/integration/megatron/lora/test_lora_disk_codecs.py index 05fe4457e..0dd8418fb 100644 --- a/tests/integration/megatron/lora/test_lora_disk_codecs.py +++ b/tests/integration/megatron/lora/test_lora_disk_codecs.py @@ -2,17 +2,29 @@ from pathlib import Path import subprocess import sys +from typing import Any, cast from safetensors.torch import load_file, save_file import torch +from art.megatron import lora as lora_module +from art.megatron.lora import LoRA, LoRAParallelSpec, LoRAPublishPlanner from art.megatron.model_support.handlers import ( DEFAULT_DENSE_HANDLER, QWEN3_5_MOE_HANDLER, QWEN3_MOE_HANDLER, ) -from art.megatron.model_support.lora_disk import normalize_lora_checkpoint_to_vllm -from art.megatron.weights.merge import load_lora_adapter_state_dict, merge_lora_adapter +from art.megatron.model_support.lora_disk import ( + load_lora_tensors_for_megatron, + normalize_lora_checkpoint_to_vllm, + save_vllm_lora_tensors, +) +from art.megatron.weights import lora_publish +from art.megatron.weights.lora_publish import ( + LoraShardMeta, + merge_sharded_adapter_entries, + save_vllm_lora_from_model, +) from art.utils.convert_moe_lora import convert_checkpoint_if_needed REPO_ROOT = Path(__file__).parents[4] @@ -67,6 +79,37 @@ def _save_adapter(path: Path, tensors: dict[str, torch.Tensor], config: dict) -> (path / "adapter_config.json").write_text(json.dumps(config), encoding="utf-8") +def _old_merge_shard_files_to_vllm( + lora_path: Path, + *, + handler, + adapter_config: dict, +) -> None: + entries_by_key: dict[str, list[tuple[dict, torch.Tensor]]] = {} + shard_paths = sorted(lora_path.glob("adapter_model-*-of-*.safetensors")) + manifest_paths = sorted(lora_path.glob("adapter_manifest-*-of-*.json")) + for shard_path in shard_paths: + suffix = shard_path.name.removeprefix("adapter_model-").removesuffix( + ".safetensors" + ) + manifest = json.loads( + (lora_path / f"adapter_manifest-{suffix}.json").read_text() + ) + shard_tensors = load_file(shard_path) + assert set(shard_tensors) == set(manifest) + for key, tensor in shard_tensors.items(): + entries_by_key.setdefault(key, []).append((manifest[key], tensor)) + + merged = merge_sharded_adapter_entries(entries_by_key) + vllm_tensors, adapter_config = handler.to_vllm_lora_tensors( + merged, + adapter_config=adapter_config, + ) + save_vllm_lora_tensors(lora_path, vllm_tensors, adapter_config) + for path in [*shard_paths, *manifest_paths]: + path.unlink() + + def _assert_stock_vllm_loads( path: Path, *, @@ -146,6 +189,45 @@ def _qwen35_moe_art_tensors(prefix: str, *, rank: int = 2) -> dict[str, torch.Te return tensors +def _pack_qwen35_vllm_lora_b(blocks: list[torch.Tensor]) -> torch.Tensor: + stacked = torch.stack(blocks, dim=0) + return stacked.permute(1, 2, 0).reshape(stacked.shape[1], -1).contiguous() + + +def _qwen35_fused_expert_vllm_tensors( + original: dict[str, torch.Tensor], + art_prefix: str, +) -> dict[str, torch.Tensor]: + vllm_prefix = art_prefix.replace( + "base_model.model.model.layers.", + "base_model.model.model.language_model.layers.", + 1, + ) + expert_prefix = f"{vllm_prefix}.mlp.experts" + art_expert_prefix = f"{art_prefix}.mlp.experts" + gate_up_a: list[torch.Tensor] = [] + gate_up_b: list[torch.Tensor] = [] + down_a: list[torch.Tensor] = [] + down_b: list[torch.Tensor] = [] + for expert in range(2): + prefix = f"{art_expert_prefix}.{expert}" + gate_up_a.append(original[f"{prefix}.gate_up_proj.lora_A.weight"]) + gate_up_b.append(original[f"{prefix}.gate_up_proj.lora_B.weight"]) + down_a.append(original[f"{prefix}.down_proj.lora_A.weight"]) + down_b.append(original[f"{prefix}.down_proj.lora_B.weight"]) + return { + f"{expert_prefix}.base_layer.lora_A.weight": torch.cat( + gate_up_a, + dim=0, + ).contiguous(), + f"{expert_prefix}.base_layer.lora_B.weight": _pack_qwen35_vllm_lora_b( + gate_up_b + ), + f"{expert_prefix}.lora_A.weight": torch.cat(down_a, dim=0).contiguous(), + f"{expert_prefix}.lora_B.weight": _pack_qwen35_vllm_lora_b(down_b), + } + + def _qwen3_dense_lora_tensors(prefix: str, *, rank: int = 2) -> dict[str, torch.Tensor]: module_dims = { "self_attn.q_proj": (rank, 3, 3), @@ -366,6 +448,75 @@ def test_qwen3_fused_identity_normalizes_to_per_expert_vllm_layout( _assert_tensors_equal(converted, expected) adapter_config = json.loads((tmp_path / "adapter_config.json").read_text()) assert "experts" in adapter_config["target_modules"] + + +def test_qwen3_target_parameter_identity_normalizes_to_per_expert_vllm_layout( + tmp_path: Path, +) -> None: + prefix = "base_model.model.model.layers.0.mlp.experts" + rank = 2 + hidden = 3 + intermediate = 4 + num_experts = 2 + gate_up_a = torch.arange( + num_experts * rank * 2 * intermediate, + dtype=torch.float32, + ).reshape(num_experts * rank, 2 * intermediate) + gate_up_b = ( + torch.arange(hidden * num_experts * rank, dtype=torch.float32).reshape( + hidden, num_experts * rank + ) + + 100 + ) + down_a = ( + torch.arange(num_experts * rank * hidden, dtype=torch.float32).reshape( + num_experts * rank, hidden + ) + + 200 + ) + down_b = ( + torch.arange(intermediate * num_experts * rank, dtype=torch.float32).reshape( + intermediate, num_experts * rank + ) + + 300 + ) + _save_adapter( + tmp_path, + { + f"{prefix}.base_layer.lora_A.weight": gate_up_a, + f"{prefix}.base_layer.lora_B.weight": gate_up_b, + f"{prefix}.lora_A.weight": down_a, + f"{prefix}.lora_B.weight": down_b, + }, + _config("Qwen/Qwen3-30B-A3B", rank=rank), + ) + + normalize_lora_checkpoint_to_vllm( + tmp_path, + handler=QWEN3_MOE_HANDLER, + adapter_config=_config("Qwen/Qwen3-30B-A3B", rank=rank), + ) + + expected: dict[str, torch.Tensor] = {} + for expert in range(num_experts): + rows = slice(expert * rank, (expert + 1) * rank) + gate_a, up_a = gate_up_a[rows].split(intermediate, dim=1) + expert_prefix = f"{prefix}.{expert}" + expected[f"{expert_prefix}.gate_proj.lora_A.weight"] = gate_up_b[ + :, rows + ].T.contiguous() + expected[f"{expert_prefix}.gate_proj.lora_B.weight"] = gate_a.T.contiguous() + expected[f"{expert_prefix}.up_proj.lora_A.weight"] = gate_up_b[ + :, rows + ].T.contiguous() + expected[f"{expert_prefix}.up_proj.lora_B.weight"] = up_a.T.contiguous() + expected[f"{expert_prefix}.down_proj.lora_A.weight"] = down_b[ + :, rows + ].T.contiguous() + expected[f"{expert_prefix}.down_proj.lora_B.weight"] = down_a[ + rows + ].T.contiguous() + _assert_tensors_equal(load_file(tmp_path / "adapter_model.safetensors"), expected) loaded_modules = _assert_stock_vllm_loads( tmp_path, expected_modules={ @@ -390,6 +541,7 @@ def test_qwen3_fused_identity_normalizes_to_per_expert_vllm_layout( def test_qwen35_and_qwen36_vllm_canonical_roundtrip_and_stock_loader(tmp_path: Path): art_prefix = "base_model.model.model.layers.0" original = _qwen35_moe_art_tensors(art_prefix) + expected_experts = _qwen35_fused_expert_vllm_tensors(original, art_prefix) for base_model in ("Qwen/Qwen3.5-35B-A3B", "Qwen/Qwen3.6-35B-A3B"): vllm_tensors, vllm_config = QWEN3_5_MOE_HANDLER.to_vllm_lora_tensors( original, @@ -408,6 +560,9 @@ def test_qwen35_and_qwen36_vllm_canonical_roundtrip_and_stock_loader(tmp_path: P "experts", ] assert all("language_model.layers" in key for key in vllm_tensors) + assert not any(".mlp.experts.0." in key for key in vllm_tensors) + for key, tensor in expected_experts.items(): + assert torch.equal(vllm_tensors[key], tensor), key roundtrip = QWEN3_5_MOE_HANDLER.from_vllm_lora_tensors( vllm_tensors, adapter_config=vllm_config, @@ -417,13 +572,48 @@ def test_qwen35_and_qwen36_vllm_canonical_roundtrip_and_stock_loader(tmp_path: P _save_adapter(adapter_dir, vllm_tensors, vllm_config) loaded_modules = _assert_stock_vllm_loads( adapter_dir, - expected_modules=set(vllm_config["target_modules"]), + expected_modules={"q_proj", "experts"}, mapper="qwen35", ) assert "language_model.model.layers.0.mlp.experts" in loaded_modules assert "language_model.model.layers.0.mlp.experts.base_layer" in loaded_modules +def test_qwen35_target_parameter_identity_normalizes_to_fused_vllm_layout( + tmp_path: Path, +) -> None: + art_prefix = "base_model.model.model.layers.0" + original = _qwen35_moe_art_tensors(art_prefix) + expected = _qwen35_fused_expert_vllm_tensors(original, art_prefix) + raw = { + key.replace( + "base_model.model.model.language_model.layers.", + "base_model.model.model.layers.", + 1, + ): tensor + for key, tensor in expected.items() + } + _save_adapter( + tmp_path, + raw, + { + **_qwen35_config("Qwen/Qwen3.5-35B-A3B"), + "target_parameters": [ + "model.layers.0.mlp.experts.gate_up_proj", + "model.layers.0.mlp.experts.down_proj", + ], + }, + ) + + normalize_lora_checkpoint_to_vllm( + tmp_path, + handler=QWEN3_5_MOE_HANDLER, + adapter_config=_qwen35_config("Qwen/Qwen3.5-35B-A3B"), + ) + + _assert_tensors_equal(load_file(tmp_path / "adapter_model.safetensors"), expected) + + def test_qwen35_and_qwen36_dense_prefix_roundtrip_and_stock_loader(tmp_path: Path): original = { "base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight": torch.ones( @@ -600,27 +790,18 @@ def sharded(rank_id: int, dim: int) -> dict: f"{prefix}.down_proj.lora_A.weight": sharded(1, 1), } adapter_dir = tmp_path / "qwen35_megatron_shards" - adapter_dir.mkdir() - (adapter_dir / "adapter_config.json").write_text( - json.dumps(_config("Qwen/Qwen3.5-35B-A3B", rank=rank, alpha=rank)), - encoding="utf-8", - ) - save_file(shard0, adapter_dir / "adapter_model-01-of-02.safetensors") - save_file(shard1, adapter_dir / "adapter_model-02-of-02.safetensors") - (adapter_dir / "adapter_manifest-01-of-02.json").write_text( - json.dumps(manifest0), - encoding="utf-8", + adapter_config = _config("Qwen/Qwen3.5-35B-A3B", rank=rank, alpha=rank) + entries_by_key = {key: [(manifest0[key], tensor)] for key, tensor in shard0.items()} + for key, tensor in shard1.items(): + entries_by_key.setdefault(key, []).append((manifest1[key], tensor)) + merged = merge_sharded_adapter_entries(entries_by_key) + vllm_tensors, adapter_config = QWEN3_5_MOE_HANDLER.to_vllm_lora_tensors( + merged, + adapter_config=adapter_config, ) - (adapter_dir / "adapter_manifest-02-of-02.json").write_text( - json.dumps(manifest1), - encoding="utf-8", - ) - - merge_lora_adapter(str(adapter_dir)) + save_vllm_lora_tensors(adapter_dir, vllm_tensors, adapter_config) - assert not list(adapter_dir.glob("adapter_model-*-of-*.safetensors")) - assert not list(adapter_dir.glob("adapter_manifest-*-of-*.json")) - roundtrip = load_lora_adapter_state_dict( + roundtrip = load_lora_tensors_for_megatron( str(adapter_dir), handler=QWEN3_5_MOE_HANDLER, ) @@ -628,13 +809,419 @@ def sharded(rank_id: int, dim: int) -> dict: final_config = json.loads((adapter_dir / "adapter_config.json").read_text()) loaded_modules = _assert_stock_vllm_loads( adapter_dir, - expected_modules=set(final_config["target_modules"]), + expected_modules={"experts"}, mapper="qwen35", ) assert "language_model.model.layers.0.mlp.experts" in loaded_modules assert "language_model.model.layers.0.mlp.experts.base_layer" in loaded_modules +def test_lora_publish_keeps_same_key_shards_separate(): + key = "base_model.model.model.layers.0.self_attn.q_proj.lora_B.weight" + manifest = { + "sharded": True, + "shard_world_size": 2, + "export_shard_dim": 0, + "export_shard_strategy": "uniform", + } + shard0 = torch.tensor([[1.0], [2.0]]) + shard1 = torch.tensor([[3.0], [4.0]]) + metadata = [ + LoraShardMeta( + key=key, + owner_rank=0, + shape=tuple(shard0.shape), + dtype_name="float32", + manifest={**manifest, "shard_rank": 0}, + block="base_model.model.model.layers.0", + ), + LoraShardMeta( + key=key, + owner_rank=1, + shape=tuple(shard1.shape), + dtype_name="float32", + manifest={**manifest, "shard_rank": 1}, + block="base_model.model.model.layers.0", + ), + ] + entries = lora_publish._entries_by_key( + metadata, + { + (0, key): shard0, + (1, key): shard1, + }, + ) + + merged = merge_sharded_adapter_entries(entries) + + assert torch.equal(merged[key], torch.tensor([[1.0], [2.0], [3.0], [4.0]])) + + +def test_lora_publish_planner_derives_metadata_from_lora_modules(): + prefix = "base_model.model.model.layers.0.self_attn.q_proj" + b_parallel_spec = LoRAParallelSpec(sharded=True, shard_dim=-1) + lora = LoRA( + adapter_model_prefix=prefix, + in_features=4, + out_features=6, + rank=2, + alpha=4, + dtype=torch.bfloat16, + device=torch.device("cpu"), + b_parallel_spec=b_parallel_spec, + ) + adapter_model = { + f"{prefix}.lora_A.weight": torch.empty(2, 4, dtype=torch.float32), + f"{prefix}.lora_B.weight": torch.empty(6, 2, dtype=torch.float32), + } + + metadata = LoRAPublishPlanner([torch.nn.Sequential(lora)]).global_metadata( + adapter_model + ) + by_key = {meta.key: meta for meta in metadata} + + a_meta = by_key[f"{prefix}.lora_A.weight"] + assert a_meta.shape == (2, 4) + assert a_meta.dtype_name == "float32" + assert a_meta.owner_rank == 0 + assert a_meta.manifest == { + "sharded": False, + "shard_world_size": 1, + "shard_rank": 0, + } + assert a_meta.block == "base_model.model.model.layers.0" + + b_meta = by_key[f"{prefix}.lora_B.weight"] + assert b_meta.shape == (6, 2) + assert b_meta.dtype_name == "float32" + assert b_meta.owner_rank == 0 + assert b_meta.manifest == { + "sharded": True, + "shard_world_size": 1, + "shard_rank": 0, + "export_shard_dim": 0, + "export_shard_strategy": "uniform", + } + + +def test_lora_publish_planner_maps_expert_owner_ranks(monkeypatch): + monkeypatch.setattr(lora_module, "_distributed_initialized", lambda: True) + monkeypatch.setattr( + lora_module, + "_get_shard_world_size", + lambda domain: 2 if domain == "expert_tp" else 1, + ) + monkeypatch.setattr( + lora_module.ps, + "get_expert_model_parallel_world_size", + lambda: 4, + ) + monkeypatch.setattr( + lora_module.ps, + "get_expert_tensor_and_model_parallel_group", + lambda check_initialized=False: "joint", + ) + monkeypatch.setattr( + lora_module.ps, + "get_expert_model_parallel_group", + lambda: "ep", + ) + monkeypatch.setattr( + lora_module.ps, + "get_expert_tensor_parallel_group", + lambda check_initialized=False: "etp", + ) + + row_major = {"joint": (0, 1, 2, 3, 4, 5, 6, 7), "ep": (0, 2, 4, 6), "etp": (0, 1)} + monkeypatch.setattr( + lora_module, + "_process_group_ranks", + lambda group: row_major[group], + ) + assert LoRAPublishPlanner._expert_owner_rank(ep_rank=3, shard_rank=1) == 7 + + column_major = { + "joint": (0, 1, 2, 3, 4, 5, 6, 7), + "ep": (0, 1, 2, 3), + "etp": (0, 4), + } + monkeypatch.setattr( + lora_module, + "_process_group_ranks", + lambda group: column_major[group], + ) + assert LoRAPublishPlanner._expert_owner_rank(ep_rank=3, shard_rank=1) == 7 + + +def test_batched_lora_publish_matches_old_shard_merge_exactly(tmp_path: Path): + uniform_key = "base_model.model.model.layers.0.self_attn.q_proj.lora_B.weight" + componentwise_key = ( + "base_model.model.model.layers.0.mlp.experts.gate_up_proj.lora_B.weight" + ) + unsharded_key = "base_model.model.model.layers.0.self_attn.q_proj.lora_A.weight" + full_uniform = torch.arange(8, dtype=torch.float32).reshape(4, 2) + full_componentwise = torch.tensor( + [[0.0], [1.0], [10.0], [11.0], [2.0], [3.0], [12.0], [13.0]] + ) + shard0 = { + unsharded_key: torch.arange(4, dtype=torch.float32).reshape(2, 2) + 100, + uniform_key: full_uniform[:2], + componentwise_key: torch.tensor([[0.0], [1.0], [2.0], [3.0]]), + } + shard1 = { + uniform_key: full_uniform[2:], + componentwise_key: torch.tensor([[10.0], [11.0], [12.0], [13.0]]), + } + unsharded_manifest = {"sharded": False, "shard_world_size": 1, "shard_rank": 0} + uniform_manifest = { + "sharded": True, + "shard_world_size": 2, + "export_shard_dim": 0, + "export_shard_strategy": "uniform", + } + componentwise_manifest = { + "sharded": True, + "shard_world_size": 2, + "export_shard_dim": 0, + "export_shard_strategy": "componentwise", + "component_sizes": [4, 4], + } + manifest0 = { + unsharded_key: unsharded_manifest, + uniform_key: {**uniform_manifest, "shard_rank": 0}, + componentwise_key: {**componentwise_manifest, "shard_rank": 0}, + } + manifest1 = { + uniform_key: {**uniform_manifest, "shard_rank": 1}, + componentwise_key: {**componentwise_manifest, "shard_rank": 1}, + } + + class IdentityHandler: + def to_vllm_lora_tensors(self, tensors, *, adapter_config): + return dict(tensors), dict(adapter_config) + + old_dir = tmp_path / "old" + current_dir = tmp_path / "current" + old_dir.mkdir() + save_file(shard0, old_dir / "adapter_model-01-of-02.safetensors") + save_file(shard1, old_dir / "adapter_model-02-of-02.safetensors") + (old_dir / "adapter_manifest-01-of-02.json").write_text( + json.dumps(manifest0, sort_keys=True) + ) + (old_dir / "adapter_manifest-02-of-02.json").write_text( + json.dumps(manifest1, sort_keys=True) + ) + adapter_config = _config("Qwen/Qwen3-30B-A3B") + handler = IdentityHandler() + _old_merge_shard_files_to_vllm( + old_dir, + handler=handler, + adapter_config=adapter_config, + ) + + metadata = [ + LoraShardMeta( + key=key, + owner_rank=0, + shape=tuple(tensor.shape), + dtype_name=str(tensor.dtype).removeprefix("torch."), + manifest=manifest0[key], + block="base_model.model.model.layers.0", + ) + for key, tensor in shard0.items() + ] + [ + LoraShardMeta( + key=key, + owner_rank=1, + shape=tuple(tensor.shape), + dtype_name=str(tensor.dtype).removeprefix("torch."), + manifest=manifest1[key], + block="base_model.model.model.layers.0", + ) + for key, tensor in shard1.items() + ] + lora_publish._save_rank0_vllm_lora( + metadata=metadata, + tensors_by_owner_key={ + **{(0, key): tensor for key, tensor in shard0.items()}, + **{(1, key): tensor for key, tensor in shard1.items()}, + }, + handler=handler, + adapter_config=adapter_config, + output_dir=str(current_dir), + ) + + old_tensors = load_file(old_dir / "adapter_model.safetensors") + current_tensors = load_file(current_dir / "adapter_model.safetensors") + _assert_tensors_equal(current_tensors, old_tensors) + assert torch.equal(current_tensors[uniform_key], full_uniform) + assert torch.equal(current_tensors[componentwise_key], full_componentwise) + assert (current_dir / "adapter_model.safetensors").read_bytes() == ( + old_dir / "adapter_model.safetensors" + ).read_bytes() + assert json.loads((current_dir / "adapter_config.json").read_text()) == json.loads( + (old_dir / "adapter_config.json").read_text() + ) + + +def test_save_vllm_lora_from_model_writes_single_vllm_checkpoint(tmp_path: Path): + prefix = "base_model.model.model.layers.0.mlp.experts.0" + full = { + f"{prefix}.gate_up_proj.lora_A.weight": torch.tensor([[1.0, 2.0]]), + f"{prefix}.gate_up_proj.lora_B.weight": torch.arange( + 8, + dtype=torch.float32, + ).reshape(8, 1), + f"{prefix}.down_proj.lora_A.weight": torch.arange( + 4, + dtype=torch.float32, + ).reshape(1, 4), + f"{prefix}.down_proj.lora_B.weight": torch.arange( + 2, + dtype=torch.float32, + ).reshape(2, 1), + } + + gate_up_lora = LoRA( + adapter_model_prefix=f"{prefix}.gate_up_proj", + in_features=2, + out_features=8, + rank=1, + alpha=1, + dtype=torch.float32, + device=torch.device("cpu"), + ) + gate_up_lora.A_T.data.copy_(full[f"{prefix}.gate_up_proj.lora_A.weight"].T) + gate_up_lora.B_T.data.copy_(full[f"{prefix}.gate_up_proj.lora_B.weight"].T) + down_lora = LoRA( + adapter_model_prefix=f"{prefix}.down_proj", + in_features=4, + out_features=2, + rank=1, + alpha=1, + dtype=torch.float32, + device=torch.device("cpu"), + ) + down_lora.A_T.data.copy_(full[f"{prefix}.down_proj.lora_A.weight"].T) + down_lora.B_T.data.copy_(full[f"{prefix}.down_proj.lora_B.weight"].T) + + publish_dir = tmp_path / "published_from_model" + save_vllm_lora_from_model( + model=cast(Any, [torch.nn.Sequential(gate_up_lora, down_lora)]), + adapter_model=full, + handler=QWEN3_5_MOE_HANDLER, + adapter_config=_config("Qwen/Qwen3.5-35B-A3B", rank=1, alpha=1), + output_dir=str(publish_dir), + rank=0, + world_size=1, + ) + + assert not list(publish_dir.glob("adapter_model-*-of-*.safetensors")) + roundtrip = load_lora_tensors_for_megatron( + str(publish_dir), + handler=QWEN3_5_MOE_HANDLER, + ) + _assert_tensors_equal(roundtrip, full) + + +def test_direct_qwen35_packed_expert_publish_matches_old_vllm_exactly( + tmp_path: Path, + monkeypatch, +): + monkeypatch.setattr(lora_module.ps, "get_expert_model_parallel_rank", lambda: 0) + monkeypatch.setattr(lora_module.ps, "get_expert_data_parallel_rank", lambda: 0) + + rank = 2 + hidden = 3 + intermediate = 4 + group_prefix = "base_model.model.model.layers.0.mlp.experts" + full: dict[str, torch.Tensor] = {} + gate_up_lora = LoRA( + adapter_model_prefix=f"{group_prefix}.{{expert}}.gate_up_proj", + in_features=hidden, + out_features=2 * intermediate, + rank=rank, + alpha=rank, + dtype=torch.float32, + device=torch.device("cpu"), + num_local_experts=2, + ) + down_lora = LoRA( + adapter_model_prefix=f"{group_prefix}.{{expert}}.down_proj", + in_features=intermediate, + out_features=hidden, + rank=rank, + alpha=rank, + dtype=torch.float32, + device=torch.device("cpu"), + num_local_experts=2, + ) + offset = 0 + for expert in range(2): + expert_prefix = f"{group_prefix}.{expert}" + tensors = { + "gate_up_proj.lora_A.weight": torch.arange( + rank * hidden, + dtype=torch.float32, + ).reshape(rank, hidden) + + offset, + "gate_up_proj.lora_B.weight": torch.arange( + 2 * intermediate * rank, + dtype=torch.float32, + ).reshape(2 * intermediate, rank) + + offset + + 100, + "down_proj.lora_A.weight": torch.arange( + rank * intermediate, + dtype=torch.float32, + ).reshape(rank, intermediate) + + offset + + 200, + "down_proj.lora_B.weight": torch.arange( + hidden * rank, + dtype=torch.float32, + ).reshape(hidden, rank) + + offset + + 300, + } + for suffix, tensor in tensors.items(): + full[f"{expert_prefix}.{suffix}"] = tensor + gate_up_lora.A_T.data[expert].copy_(tensors["gate_up_proj.lora_A.weight"].T) + gate_up_lora.B_T.data[expert].copy_(tensors["gate_up_proj.lora_B.weight"].T) + down_lora.A_T.data[expert].copy_(tensors["down_proj.lora_A.weight"].T) + down_lora.B_T.data[expert].copy_(tensors["down_proj.lora_B.weight"].T) + offset += 1000 + + adapter_config = _config("Qwen/Qwen3.5-35B-A3B", rank=rank, alpha=rank) + old_dir = tmp_path / "old" + current_dir = tmp_path / "current" + old_tensors, old_config = QWEN3_5_MOE_HANDLER.to_vllm_lora_tensors( + full, + adapter_config=dict(adapter_config), + ) + save_vllm_lora_tensors(old_dir, old_tensors, old_config) + save_vllm_lora_from_model( + model=cast(Any, [torch.nn.Sequential(gate_up_lora, down_lora)]), + adapter_model=full, + handler=QWEN3_5_MOE_HANDLER, + adapter_config=dict(adapter_config), + output_dir=str(current_dir), + rank=0, + world_size=1, + ) + + _assert_tensors_equal( + load_file(current_dir / "adapter_model.safetensors"), + load_file(old_dir / "adapter_model.safetensors"), + ) + assert (current_dir / "adapter_model.safetensors").read_bytes() == ( + old_dir / "adapter_model.safetensors" + ).read_bytes() + assert json.loads((current_dir / "adapter_config.json").read_text()) == json.loads( + (old_dir / "adapter_config.json").read_text() + ) + + def test_qwen35_megatron_shards_can_merge_to_separate_vllm_checkpoint( tmp_path: Path, ): @@ -654,29 +1241,21 @@ def test_qwen35_megatron_shards_can_merge_to_separate_vllm_checkpoint( dtype=torch.float32, ).reshape(2, 1), } - shard_dir = tmp_path / "staging" publish_dir = tmp_path / "published" - shard_dir.mkdir() - (shard_dir / "adapter_config.json").write_text( - json.dumps(_config("Qwen/Qwen3.5-35B-A3B", rank=1, alpha=1)), - encoding="utf-8", - ) - save_file(full, shard_dir / "adapter_model-01-of-01.safetensors") - (shard_dir / "adapter_manifest-01-of-01.json").write_text( - json.dumps( - { - key: {"sharded": False, "shard_world_size": 1, "shard_rank": 0} - for key in full - } - ), - encoding="utf-8", + adapter_config = _config("Qwen/Qwen3.5-35B-A3B", rank=1, alpha=1) + entries_by_key = { + key: [({"sharded": False, "shard_world_size": 1, "shard_rank": 0}, tensor)] + for key, tensor in full.items() + } + merged = merge_sharded_adapter_entries(entries_by_key) + vllm_tensors, adapter_config = QWEN3_5_MOE_HANDLER.to_vllm_lora_tensors( + merged, + adapter_config=adapter_config, ) + save_vllm_lora_tensors(publish_dir, vllm_tensors, adapter_config) - merge_lora_adapter(str(shard_dir), output_dir=publish_dir) - - assert not (shard_dir / "adapter_model.safetensors").exists() assert (publish_dir / "adapter_model.safetensors").exists() - roundtrip = load_lora_adapter_state_dict( + roundtrip = load_lora_tensors_for_megatron( str(publish_dir), handler=QWEN3_5_MOE_HANDLER, ) diff --git a/tests/integration/megatron/lora/test_weight_transfer_bootstrap_contract.py b/tests/integration/megatron/lora/test_weight_transfer_bootstrap_contract.py index 69a2b6bc1..38b9a80ae 100644 --- a/tests/integration/megatron/lora/test_weight_transfer_bootstrap_contract.py +++ b/tests/integration/megatron/lora/test_weight_transfer_bootstrap_contract.py @@ -1,5 +1,6 @@ from contextlib import nullcontext from types import SimpleNamespace +from typing import Any, cast import pytest import torch @@ -97,3 +98,63 @@ def fake_communicator(**kwargs): "device": 3, "nccl_so_path": "/runtime/libnccl.so.2", } + + +def test_trainer_nccl_communicator_closes_nccl_and_bootstrap_group() -> None: + communicator = object.__new__(nccl.TrainerNcclCommunicator) + calls: list[str] = [] + communicator._comm = "comm" + communicator._nccl = SimpleNamespace( + destroy_comm=lambda comm: calls.append(f"destroy:{comm}") + ) + communicator._bootstrap_group = SimpleNamespace( + close=lambda: calls.append("bootstrap_close") + ) + + communicator.close() + communicator.close() + + assert calls == ["destroy:comm", "bootstrap_close"] + assert communicator._comm is None + + +def test_trainer_nccl_communicator_aborts_nccl_and_bootstrap_group() -> None: + communicator = object.__new__(nccl.TrainerNcclCommunicator) + calls: list[str] = [] + communicator._comm = "comm" + communicator._nccl = SimpleNamespace( + abort_comm=lambda comm: calls.append(f"abort:{comm}") + ) + communicator._bootstrap_group = SimpleNamespace( + close=lambda: calls.append("bootstrap_close") + ) + + communicator.abort() + communicator.abort() + + assert calls == ["abort:comm", "bootstrap_close"] + assert communicator._comm is None + + +def test_trainer_nccl_communicator_rejects_invalid_collective_tensors() -> None: + communicator = object.__new__(nccl.TrainerNcclCommunicator) + communicator.device = torch.device("cuda:0") + + with pytest.raises(RuntimeError, match="requires a CUDA tensor"): + communicator._validate_collective_tensor(torch.empty(1)) + + wrong_device = SimpleNamespace( + is_cuda=True, + device=torch.device("cuda:1"), + is_contiguous=lambda: True, + ) + with pytest.raises(RuntimeError, match="tensor device mismatch"): + communicator._validate_collective_tensor(cast(Any, wrong_device)) + + non_contiguous = SimpleNamespace( + is_cuda=True, + device=torch.device("cuda:0"), + is_contiguous=lambda: False, + ) + with pytest.raises(RuntimeError, match="requires contiguous tensors"): + communicator._validate_collective_tensor(cast(Any, non_contiguous)) diff --git a/tests/integration/megatron/metrics.py b/tests/integration/megatron/metrics.py new file mode 100644 index 000000000..7719312ae --- /dev/null +++ b/tests/integration/megatron/metrics.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import torch +from torch import Tensor + +DEFAULT_MEAN_ABS_PCT_THRESHOLD = 1.0 +MEAN_ABS_PCT_DENOMINATOR_EPS = 1e-18 + + +def mean_abs_pct_from_sums( + abs_diff_sum: float, + reference_abs_sum: float, + numel: int, +) -> float: + if numel == 0: + return 0.0 + mean_abs_diff = abs_diff_sum / numel + mean_abs_reference = reference_abs_sum / numel + return (mean_abs_diff / (mean_abs_reference + MEAN_ABS_PCT_DENOMINATOR_EPS)) * 100.0 + + +def mean_abs_pct(reference: Tensor, candidate: Tensor) -> float: + reference_fp32 = reference.detach().float() + candidate_fp32 = candidate.detach().float() + diff = (candidate_fp32 - reference_fp32).abs() + return mean_abs_pct_from_sums( + float(diff.sum().item()), + float(reference_fp32.abs().sum().item()), + int(diff.numel()), + ) diff --git a/tests/integration/megatron/model_support/forward_trace.py b/tests/integration/megatron/model_support/forward_trace.py index 41bbd0d38..289b8b7a6 100644 --- a/tests/integration/megatron/model_support/forward_trace.py +++ b/tests/integration/megatron/model_support/forward_trace.py @@ -6,6 +6,12 @@ import torch +from .trace_uids import ( + expand_token_uids_for_heads, + extract_tensor_attr, + row_token_uids_from_trace_sources, +) + CAPTURE_NAME_TOKENS = ( ".self_attention", ".self_attention.in_proj", @@ -38,6 +44,15 @@ PRIMARY_OUTPUT_CANONICAL_KEY = "primary_output__is_canonical" +def _trace_hook(fn: Callable[..., Any]) -> Callable[..., Any]: + return torch.compiler.disable(fn) + + +def _normalize_trace_module_name(module_name: str) -> str: + """Strips compile-wrapper path segments from trace module names.""" + return module_name.replace("._orig_mod", "") + + def _safe_int(value: Any, default: int = 0) -> int: """Coerces scalar values to int for trace metadata.""" try: @@ -69,6 +84,8 @@ def _rank_metadata() -> dict[str, int]: "world_size": world_size, "tp_rank": _safe_ps_stat("get_tensor_model_parallel_rank", 0), "tp_world_size": _safe_ps_stat("get_tensor_model_parallel_world_size", 1), + "cp_rank": _safe_ps_stat("get_context_parallel_rank", 0), + "cp_world_size": _safe_ps_stat("get_context_parallel_world_size", 1), "ep_rank": _safe_ps_stat("get_expert_model_parallel_rank", 0), "ep_world_size": _safe_ps_stat("get_expert_model_parallel_world_size", 1), "etp_rank": _safe_ps_stat("get_expert_tensor_parallel_rank", 0), @@ -180,36 +197,9 @@ def _materialize_tensor(tensor: torch.Tensor) -> torch.Tensor: return tensor.detach().cpu() -def _materialize_trace_value(value: Any) -> Any: - if isinstance(value, torch.Tensor): - return _materialize_tensor(value) - if isinstance(value, dict): - return {key: _materialize_trace_value(item) for key, item in value.items()} - if isinstance(value, list): - return [_materialize_trace_value(item) for item in value] - if isinstance(value, tuple): - return tuple(_materialize_trace_value(item) for item in value) - return value - - -def _extract_tensor_attr(value: Any, attr_name: str) -> Any: - if isinstance(value, torch.Tensor): - return getattr(value, attr_name, None) - if isinstance(value, dict): - for item in value.values(): - attr_value = _extract_tensor_attr(item, attr_name) - if attr_value is not None: - return attr_value - if isinstance(value, (list, tuple)): - for item in value: - attr_value = _extract_tensor_attr(item, attr_name) - if attr_value is not None: - return attr_value - return None - - -@torch._dynamo.disable -def _extract_router_topk(output: Any) -> tuple[torch.Tensor, torch.Tensor] | None: +def _extract_router_topk( + output: Any, *, topk_hint: int | None = None +) -> tuple[torch.Tensor, torch.Tensor] | None: if not isinstance(output, tuple) or len(output) < 2: return None probs = output[0] @@ -218,7 +208,10 @@ def _extract_router_topk(output: Any) -> tuple[torch.Tensor, torch.Tensor] | Non return None probs = _materialize_tensor(probs.float()) routing_map = _materialize_tensor(routing_map) - topk = int(routing_map.sum(dim=-1).max().item()) + if int(routing_map.shape[0]) == 0: + topk = int(topk_hint or 0) + else: + topk = int(routing_map.sum(dim=-1).max().item()) if topk < 0: raise RuntimeError(f"Invalid router topk={topk}") if topk == 0: @@ -229,6 +222,19 @@ def _extract_router_topk(output: Any) -> tuple[torch.Tensor, torch.Tensor] | Non return topk_ids.contiguous(), topk_scores.contiguous() +def _extract_router_output(output: Any) -> dict[str, torch.Tensor] | None: + if not isinstance(output, tuple) or len(output) < 2: + return None + probs = output[0] + routing_map = output[1] + if not isinstance(probs, torch.Tensor) or not isinstance(routing_map, torch.Tensor): + return None + return { + "probs": _materialize_tensor(probs.float()), + "routing_map": _materialize_tensor(routing_map.bool()), + } + + class ForwardTraceCapture: def __init__( self, @@ -250,10 +256,11 @@ def __init__( self.current_micro_module_call_counts: dict[str, int] = {} self.current_step_sample_indices: list[int | None] = [] self.current_step_outputs: list[ - tuple[int | None, int, int | None, torch.Tensor] + tuple[int | None, int, int | None, torch.Tensor, torch.Tensor | None] ] = [] self._trace_metadata_by_name: dict[str, dict[str, Any]] = {} self._next_micro_order = 0 + self._inside_root_forward = False self._hook_handles: list[Any] = [] if not enabled: return @@ -264,16 +271,18 @@ def _register_hooks(self, model_chunks: list[Any]) -> None: raise RuntimeError("Expected at least one model chunk for forward tracing") root_module = model_chunks[0] self._hook_handles.append( - root_module.register_forward_pre_hook(self._root_pre_hook) + root_module.register_forward_pre_hook(_trace_hook(self._root_pre_hook)) ) self._hook_handles.append( - root_module.register_forward_hook(self._root_post_hook) + root_module.register_forward_hook(_trace_hook(self._root_post_hook)) ) for chunk_index, chunk in enumerate(model_chunks): named_modules = list(chunk.named_modules()) module_by_name = dict(named_modules) for module_name, module in named_modules: - trace_module_name = f"chunk{chunk_index}.{module_name}" + trace_module_name = _normalize_trace_module_name( + f"chunk{chunk_index}.{module_name}" + ) metadata = self._build_module_trace_metadata( module_name=module_name, module=module, @@ -291,7 +300,7 @@ def _register_hooks(self, model_chunks: list[Any]) -> None: continue self._hook_handles.append( module.register_forward_hook( - self._make_hook(trace_module_name, module) + _trace_hook(self._make_hook(trace_module_name, module)) ) ) @@ -304,14 +313,10 @@ def _build_module_trace_metadata( module_by_name: dict[str, Any], ) -> dict[str, Any]: if module_name.endswith(".self_attention.in_proj"): - return { - "component_sizes": cls._gdn_in_proj_component_sizes(module), - } + return {"component_sizes": cls._gdn_in_proj_component_sizes(module)} if module_name.endswith(".self_attention.in_proj.in_proj"): parent_module = module_by_name[module_name.rsplit(".", 1)[0]] - return { - "component_sizes": cls._gdn_in_proj_component_sizes(parent_module), - } + return {"component_sizes": cls._gdn_in_proj_component_sizes(parent_module)} if module_name.endswith(".self_attention.out_norm"): gdn_module = module_by_name[module_name.removesuffix(".out_norm")] return { @@ -439,6 +444,21 @@ def _infer_primary_output_merge_hint( return {"op": "sum"} return {"op": "concat", "dim": 0} + gather_output = getattr(module, "gather_output", None) + if isinstance(gather_output, bool) and not gather_output: + return {"op": "concat", "dim": -1} + + if ".self_attention.linear_qkv" in name: + return {"op": "concat", "dim": -1} + if name.endswith(".self_attention.in_proj"): + return {"op": "concat", "dim": -1} + if name.endswith( + ".self_attention.out_proj" + ) and self._sequence_parallel_enabled(module): + return {"op": "concat", "dim": 0} + if name.endswith(".self_attention") and self._sequence_parallel_enabled(module): + return {"op": "concat", "dim": 0} + if ".mlp.linear_fc1" in name and ".lora" not in name: tp_world_size = _safe_ps_stat("get_tensor_model_parallel_world_size", 1) if tp_world_size > 1: @@ -461,21 +481,6 @@ def _infer_primary_output_merge_hint( return {"op": "concat", "dim": 0} return None - gather_output = getattr(module, "gather_output", None) - if isinstance(gather_output, bool) and not gather_output: - return {"op": "concat", "dim": -1} - - if ".self_attention.linear_qkv" in name: - return {"op": "concat", "dim": -1} - if name.endswith(".self_attention.in_proj"): - return {"op": "concat", "dim": -1} - if name.endswith( - ".self_attention.out_proj" - ) and self._sequence_parallel_enabled(module): - return {"op": "concat", "dim": 0} - if name.endswith(".self_attention") and self._sequence_parallel_enabled(module): - return {"op": "concat", "dim": 0} - if ".mlp.experts." in name: return {"op": "concat", "dim": 0} @@ -499,46 +504,70 @@ def _build_merge_hints(self, name: str, module: Any) -> dict[str, dict[str, Any] hints["router_topk_scores"] = concat_dim0 return hints - @torch._dynamo.disable - def _record_module_hook( - self, name: str, module: Any, inputs: Any, output: Any - ) -> None: - if self.current_step_index is None: - return - micro_call_index = self.current_micro_module_call_counts.get(name, 0) - self.current_micro_module_call_counts[name] = micro_call_index + 1 - trace_item: dict[str, Any] = { - "micro_call_index": micro_call_index, - "micro_order": self.current_micro_order, - "micro_sample_index": self.current_micro_sample_index, - "module_type": module.__class__.__name__, - "rank_meta": _rank_metadata(), - "merge_hints": self._build_merge_hints(name, module), - "inputs": _materialize_trace_value(inputs), - "output": _materialize_trace_value(output), - "primary_input": self.guess_primary_tensor(inputs), - "primary_output": self.guess_primary_tensor(output), - } - if ROUTER_NAME_TOKEN in name: - router_topk = _extract_router_topk(output) - if router_topk is not None: - topk_ids, topk_scores = router_topk - trace_item["router_topk_ids"] = topk_ids - trace_item["router_topk_scores"] = topk_scores - trace_items = self._split_expert_trace_items( - module_name=name, - module=module, - inputs=inputs, - trace_item=trace_item, - ) - trace_calls = self.current_step_trace.setdefault(name, []) - for split_item in trace_items: - split_item["call_index"] = len(trace_calls) - trace_calls.append(split_item) - def _make_hook(self, name: str, module: Any): def _hook(_module: Any, inputs: Any, output: Any) -> None: - self._record_module_hook(name, module, inputs, output) + if self.current_step_index is None or not self._inside_root_forward: + return + micro_call_index = self.current_micro_module_call_counts.get(name, 0) + self.current_micro_module_call_counts[name] = micro_call_index + 1 + trace_item: dict[str, Any] = { + "micro_call_index": micro_call_index, + "micro_order": self.current_micro_order, + "micro_sample_index": self.current_micro_sample_index, + "module_type": module.__class__.__name__, + "rank_meta": _rank_metadata(), + "merge_hints": self._build_merge_hints(name, module), + # Keep live trace capture passive. Recursively materializing full + # hook inputs/outputs here performs large device-to-host copies and + # previously perturbed correctness in the real training forward. + "primary_output": self.guess_primary_tensor(output), + } + if ROUTER_NAME_TOKEN in name: + router_output = _extract_router_output(output) + if router_output is not None: + trace_item["output"] = router_output + topk_hint = getattr( + getattr(module, "config", None), "moe_router_topk", None + ) + router_topk = _extract_router_topk( + output, + topk_hint=int(topk_hint) if topk_hint is not None else None, + ) + if router_topk is not None: + topk_ids, topk_scores = router_topk + trace_item["router_topk_ids"] = topk_ids + trace_item["router_topk_scores"] = topk_scores + primary_output = trace_item.get("primary_output") + primary_row_count = ( + int(primary_output.shape[0]) + if isinstance(primary_output, torch.Tensor) and primary_output.ndim > 0 + else None + ) + row_token_uids, _uid_span = self._row_token_uids_for_trace( + inputs=inputs, + output=output, + module=module, + row_count=primary_row_count, + ) + if ( + isinstance(primary_output, torch.Tensor) + and primary_output.ndim > 0 + and isinstance(row_token_uids, torch.Tensor) + and int(row_token_uids.numel()) == int(primary_output.shape[0]) + ): + trace_item["row_token_uids"] = row_token_uids + if isinstance(_uid_span, int) and _uid_span > 0: + trace_item["row_uid_span"] = int(_uid_span) + trace_items = self._split_expert_trace_items( + module_name=name, + module=module, + inputs=inputs, + trace_item=trace_item, + ) + trace_calls = self.current_step_trace.setdefault(name, []) + for split_item in trace_items: + split_item["call_index"] = len(trace_calls) + trace_calls.append(split_item) return _hook @@ -554,15 +583,14 @@ def _sample_index_for_micro(self, micro_order: int) -> int | None: return self.current_step_sample_indices[micro_order] return None - @torch._dynamo.disable def _root_pre_hook(self, _module: Any, _args: Any) -> None: if self.current_step_index is None: return + self._inside_root_forward = True micro_order = self._next_micro_order sample_index = self._sample_index_for_micro(micro_order) self.begin_micro(sample_index=sample_index, micro_order=micro_order) - @torch._dynamo.disable def _root_post_hook(self, _module: Any, _inputs: Any, output: Any) -> None: if self.current_step_index is None: return @@ -581,9 +609,11 @@ def _root_post_hook(self, _module: Any, _inputs: Any, output: Any) -> None: if sample_index is not None else _local_dummy_micro_slot(micro_order), output_tensor.float(), + getattr(_module, "_art_root_output_token_uids", None), ) ) self._next_micro_order = micro_order + 1 + self._inside_root_forward = False def set_step( self, @@ -598,6 +628,7 @@ def set_step( self.current_micro_order = 0 self.current_micro_module_call_counts = {} self._next_micro_order = 0 + self._inside_root_forward = False def begin_micro(self, sample_index: int | None, micro_order: int) -> None: self.current_micro_sample_index = sample_index @@ -610,21 +641,17 @@ def begin_micro(self, sample_index: int | None, micro_order: int) -> None: def _row_token_uids_for_trace( *, inputs: Any, + output: Any = None, module: Any, + row_count: int | None = None, + prefer_uid_span: bool = False, ) -> tuple[torch.Tensor | None, int | None]: - row_token_uids = _extract_tensor_attr(inputs, "_art_trace_row_token_uids") - if row_token_uids is None: - row_token_uids = getattr(module, "_art_trace_row_token_uids", None) - if not isinstance(row_token_uids, torch.Tensor): - return None, None - - uid_span = _extract_tensor_attr(inputs, "_art_trace_uid_span") - if uid_span is None: - uid_span = getattr(module, "_art_trace_uid_span", None) - uid_span_int = uid_span if isinstance(uid_span, int) and uid_span > 0 else None - return ( - row_token_uids.detach().to(device="cpu", dtype=torch.int64).reshape(-1), - uid_span_int, + return row_token_uids_from_trace_sources( + inputs=inputs, + output=output, + module=module, + row_count=row_count, + prefer_uid_span=prefer_uid_span, ) @classmethod @@ -687,6 +714,8 @@ def _split_expert_trace_items( row_token_uids, uid_span = cls._row_token_uids_for_trace( inputs=inputs, module=module, + row_count=int(primary_output.shape[0]), + prefer_uid_span=True, ) if row_token_uids is None: return [trace_item] @@ -695,9 +724,26 @@ def _split_expert_trace_items( if total_rows == 0 or int(primary_output.shape[0]) != total_rows: return [trace_item] + valid_rows = torch.nonzero(row_token_uids >= 0, as_tuple=False).reshape(-1) + if int(valid_rows.numel()) == 0: + return [] + if int(valid_rows.numel()) != total_rows: + trace_item = { + key: cls._slice_row_aligned_value( + value, + row_indices=valid_rows, + total_rows=total_rows, + ) + for key, value in trace_item.items() + if key not in {"call_index", "micro_sample_index", "row_token_uids"} + } + row_token_uids = row_token_uids.index_select(0, valid_rows) + total_rows = int(row_token_uids.numel()) + trace_item["row_token_uids"] = row_token_uids if uid_span is None: return [trace_item] + trace_item["row_uid_span"] = int(uid_span) sample_ids = torch.div(row_token_uids, uid_span, rounding_mode="floor") ordered_sample_ids: list[int] = [] @@ -710,7 +756,9 @@ def _split_expert_trace_items( ordered_sample_ids.append(sample_id_int) if len(ordered_sample_ids) <= 1: - if ordered_sample_ids: + if ordered_sample_ids and not isinstance( + trace_item.get("micro_sample_index"), int + ): trace_item["micro_sample_index"] = ordered_sample_ids[0] return [trace_item] @@ -728,6 +776,7 @@ def _split_expert_trace_items( } split_item["micro_sample_index"] = sample_id split_item["row_token_uids"] = row_token_uids.index_select(0, row_indices) + split_item["row_uid_span"] = int(uid_span) split_items.append(split_item) return split_items @@ -891,6 +940,118 @@ def _canonicalize_rank_blocked_token_heads( "rank_blocked_token_heads expects a 2D [rows, head_dim] tensor, " f"got shape={tuple(tensor.shape)}" ) + tensor = cls._canonicalize_rank_blocked_rows( + tensor, + local_heads=local_heads, + rank_world_size=rank_world_size, + call=call, + ) + row_token_uids = call.get("row_token_uids") + if ( + isinstance(row_token_uids, torch.Tensor) + and row_token_uids.ndim == 1 + and int(row_token_uids.numel()) == int(tensor.shape[0]) + ): + call["row_token_uids"] = cls._canonicalize_rank_blocked_rows( + row_token_uids, + local_heads=local_heads, + rank_world_size=rank_world_size, + call=call, + ) + return tensor + + @classmethod + def _canonicalize_rank_blocked_rows( + cls, + tensor: torch.Tensor, + *, + local_heads: int, + rank_world_size: int, + call: dict[str, Any], + ) -> torch.Tensor: + rank_meta = call.get("rank_meta") + row_splits = call.get("primary_output__row_splits") + if ( + isinstance(rank_meta, list) + and isinstance(row_splits, list) + and len(rank_meta) == len(row_splits) + and sum(int(split) for split in row_splits) == int(tensor.shape[0]) + ): + chunks = list(torch.split(tensor, [int(split) for split in row_splits], 0)) + grouped_indices: dict[int, list[int]] = {} + for index, meta in enumerate(rank_meta): + if not isinstance(meta, dict): + return cls._canonicalize_rank_blocked_rows_without_splits( + tensor, + local_heads=local_heads, + rank_world_size=rank_world_size, + ) + cp_rank = _safe_int(meta.get("cp_rank"), 0) + grouped_indices.setdefault(cp_rank, []).append(index) + ordered_groups: list[torch.Tensor] = [] + for cp_rank in sorted(grouped_indices): + group_indices = sorted( + grouped_indices[cp_rank], + key=lambda index: _safe_int( + cast(dict[str, Any], rank_meta[index]).get("tp_rank"), + index, + ), + ) + group_chunks = [chunks[index] for index in group_indices] + canonical_group = cls._canonicalize_rank_blocked_group_rows( + group_chunks, + local_heads=local_heads, + rank_world_size=rank_world_size, + ) + ordered_groups.append(canonical_group) + return torch.cat(ordered_groups, dim=0).contiguous() + return cls._canonicalize_rank_blocked_rows_without_splits( + tensor, + local_heads=local_heads, + rank_world_size=rank_world_size, + ) + + @staticmethod + def _canonicalize_rank_blocked_group_rows( + chunks: list[torch.Tensor], + *, + local_heads: int, + rank_world_size: int, + ) -> torch.Tensor: + if len(chunks) != rank_world_size: + raise RuntimeError( + "rank_blocked_token_heads expected one chunk per tensor-parallel " + f"rank, got {len(chunks)} for world_size={rank_world_size}" + ) + rows_per_rank = int(chunks[0].shape[0]) + if any(int(chunk.shape[0]) != rows_per_rank for chunk in chunks[1:]): + raise RuntimeError( + "rank_blocked_token_heads CP group rank chunks must have equal rows" + ) + token_count, head_remainder = divmod(rows_per_rank, local_heads) + if head_remainder != 0: + raise RuntimeError( + "rank_blocked_token_heads rows per rank must divide local_heads, got " + f"rows_per_rank={rows_per_rank} local_heads={local_heads}" + ) + if rows_per_rank == 0: + return torch.cat(chunks, dim=0).contiguous() + grouped = torch.cat(chunks, dim=0) + tail_shape = tuple(grouped.shape[1:]) + return ( + grouped.reshape(rank_world_size, token_count, local_heads, *tail_shape) + .permute(1, 0, 2, *range(3, 3 + len(tail_shape))) + .reshape(rows_per_rank * rank_world_size, *tail_shape) + .contiguous() + ) + + @staticmethod + def _canonicalize_rank_blocked_rows_without_splits( + tensor: torch.Tensor, + *, + local_heads: int, + rank_world_size: int, + ) -> torch.Tensor: rows_per_rank, remainder = divmod(int(tensor.shape[0]), rank_world_size) if remainder != 0: raise RuntimeError( @@ -903,38 +1064,121 @@ def _canonicalize_rank_blocked_token_heads( "rank_blocked_token_heads rows per rank must divide local_heads, got " f"rows_per_rank={rows_per_rank} local_heads={local_heads}" ) + tail_shape = tuple(tensor.shape[1:]) return ( - tensor.reshape(rank_world_size, token_count, local_heads, tensor.shape[-1]) - .permute(1, 0, 2, 3) + tensor.reshape(rank_world_size, token_count, local_heads, *tail_shape) + .permute(1, 0, 2, *range(3, 3 + len(tail_shape))) .reshape(tensor.shape) .contiguous() ) @classmethod - def _canonicalize_moe_expert_row_order( + def _canonicalize_row_aligned_value( cls, + value: Any, *, - module_name: str, - tensor: torch.Tensor, - call: dict[str, Any], - ) -> torch.Tensor: - """Canonicalizes MoE expert rows using dispatch-time UID identities.""" - if not cls._is_moe_expert_forward_module(module_name): - return tensor - if tensor.ndim != 2: - return tensor - primary_hint = cls._primary_output_merge_hint(call) - if isinstance(primary_hint, dict) and ( - primary_hint.get("op") != "concat" or primary_hint.get("dim") != 0 - ): - return tensor + order: torch.Tensor, + total_rows: int, + ) -> Any: + """Applies one row-token ordering to every row-aligned tensor value.""" + if isinstance(value, torch.Tensor): + if value.ndim > 0 and int(value.shape[0]) == total_rows: + return value.index_select(0, order).contiguous() + return value + if isinstance(value, dict): + return { + key: cls._canonicalize_row_aligned_value( + item, + order=order, + total_rows=total_rows, + ) + for key, item in value.items() + } + if isinstance(value, list): + return [ + cls._canonicalize_row_aligned_value( + item, + order=order, + total_rows=total_rows, + ) + for item in value + ] + if isinstance(value, tuple): + return tuple( + cls._canonicalize_row_aligned_value( + item, + order=order, + total_rows=total_rows, + ) + for item in value + ) + return value + + @classmethod + def _canonicalize_call_row_token_order(cls, call: dict[str, Any]) -> None: + """Canonicalizes all row-aligned call tensors to global token order.""" + cls._align_exact_zero_padding_row_token_uids(call) row_token_uids = call.get("row_token_uids") - if not isinstance(row_token_uids, torch.Tensor): - return tensor - if int(row_token_uids.numel()) != int(tensor.shape[0]): - return tensor + if not isinstance(row_token_uids, torch.Tensor) or row_token_uids.ndim != 1: + return + total_rows = int(row_token_uids.numel()) + if total_rows <= 1: + return order = torch.argsort(row_token_uids, stable=True) - return tensor.index_select(0, order) + if bool(torch.equal(order, torch.arange(order.numel(), dtype=order.dtype))): + return + original_call = dict(call) + for key, value in original_call.items(): + if key == "row_token_uids": + continue + call[key] = cls._canonicalize_row_aligned_value( + value, + order=order, + total_rows=total_rows, + ) + call["row_token_uids"] = row_token_uids.index_select(0, order).contiguous() + + @staticmethod + def _align_exact_zero_padding_row_token_uids(call: dict[str, Any]) -> None: + """Moves padding UID markers onto exact-zero sequence-parallel pad rows.""" + row_token_uids = call.get("row_token_uids") + tensor = call.get("primary_output") + if ( + not isinstance(row_token_uids, torch.Tensor) + or row_token_uids.ndim != 1 + or not isinstance(tensor, torch.Tensor) + or tensor.ndim == 0 + or int(tensor.shape[0]) != int(row_token_uids.numel()) + ): + return + row_count = int(row_token_uids.numel()) + if row_count <= 1 or not bool((row_token_uids < 0).any().item()): + return + flat = tensor.detach().reshape(row_count, -1) + zero_rows = torch.nonzero( + (flat == 0).all(dim=1) & (row_token_uids >= 0), + as_tuple=False, + ).reshape(-1) + negative_rows = torch.nonzero( + (row_token_uids < 0) & ~(flat == 0).all(dim=1), + as_tuple=False, + ).reshape(-1) + if int(zero_rows.numel()) == 0 or int(zero_rows.numel()) != int( + negative_rows.numel() + ): + return + aligned = row_token_uids.clone() + for zero_pos, negative_pos in zip( + zero_rows.tolist(), negative_rows.tolist(), strict=True + ): + zero_pos = int(zero_pos) + negative_pos = int(negative_pos) + if zero_pos >= negative_pos: + return + shifted = aligned[zero_pos:negative_pos].clone() + aligned[zero_pos] = -1 + aligned[zero_pos + 1 : negative_pos + 1] = shifted + call["row_token_uids"] = aligned @classmethod def _canonicalize_primary_output_tensor( @@ -960,12 +1204,156 @@ def _canonicalize_primary_output_tensor( tensor=tensor, call=call, ) - return cls._canonicalize_moe_expert_row_order( - module_name=module_name, - tensor=tensor, - call=call, + return tensor + + @staticmethod + def _decoder_layer_trace_key( + module_name: str, + call: dict[str, Any], + ) -> tuple[str, int, int, int] | None: + module_name = _normalize_trace_module_name(module_name) + if ".decoder.layers." not in module_name: + return None + tensor = call.get("primary_output") + if not isinstance(tensor, torch.Tensor) or tensor.ndim == 0: + return None + return ( + module_name.split(".self_attention", 1)[0].split(".mlp", 1)[0], + _safe_int(call.get("micro_sample_index"), -1), + _safe_int(call.get("micro_order"), -1), + int(tensor.shape[0]), + ) + + @staticmethod + def _decoder_micro_trace_key( + module_name: str, + call: dict[str, Any], + ) -> tuple[str, int, int] | None: + module_name = _normalize_trace_module_name(module_name) + if ".decoder.layers." not in module_name: + return None + return ( + module_name.split(".self_attention", 1)[0].split(".mlp", 1)[0], + _safe_int(call.get("micro_sample_index"), -1), + _safe_int(call.get("micro_order"), -1), ) + @staticmethod + def _is_attention_output_trace(module_name: str) -> bool: + module_name = _normalize_trace_module_name(module_name) + return module_name.endswith(".self_attention") + + @staticmethod + def _rank_blocked_token_head_count(call: dict[str, Any]) -> int | None: + primary_hint = ForwardTraceCapture._primary_output_merge_hint(call) + if not isinstance(primary_hint, dict): + return None + if primary_hint.get("layout") != "rank_blocked_token_heads": + return None + local_heads = primary_hint.get("local_heads") + world_size_key = primary_hint.get("world_size_key") + if not isinstance(local_heads, int) or local_heads <= 0: + return None + if not isinstance(world_size_key, str): + return None + rank_meta = call.get("rank_meta") + rank_world_size = None + if isinstance(rank_meta, list) and rank_meta: + first_meta = rank_meta[0] + if isinstance(first_meta, dict): + rank_world_size = first_meta.get(world_size_key) + elif isinstance(rank_meta, dict): + rank_world_size = rank_meta.get(world_size_key) + if not isinstance(rank_world_size, int) or rank_world_size <= 0: + return None + return int(local_heads) * int(rank_world_size) + + @classmethod + def _propagate_decoder_row_token_uids( + cls, + trace: dict[str, list[dict[str, Any]]], + ) -> None: + row_uids_by_key: dict[tuple[str, int, int, int], torch.Tensor] = {} + for module_name in sorted(trace.keys()): + for call in trace[module_name]: + row_token_uids = call.get("row_token_uids") + if not isinstance(row_token_uids, torch.Tensor): + continue + key = cls._decoder_layer_trace_key(module_name, call) + if key is None or key in row_uids_by_key: + continue + row_uids_by_key[key] = row_token_uids + for module_name in sorted(trace.keys()): + for call in trace[module_name]: + if isinstance(call.get("row_token_uids"), torch.Tensor): + continue + key = cls._decoder_layer_trace_key(module_name, call) + if key is None: + continue + row_token_uids = row_uids_by_key.get(key) + if row_token_uids is None: + continue + call["row_token_uids"] = row_token_uids + + @classmethod + def _propagate_attention_output_row_token_uids( + cls, + trace: dict[str, list[dict[str, Any]]], + ) -> None: + token_uids_by_key: dict[tuple[str, int, int], torch.Tensor] = {} + for module_name in sorted(trace.keys()): + if not cls._is_attention_output_trace(module_name): + continue + for call in trace[module_name]: + tensor = call.get("primary_output") + row_token_uids = call.get("row_token_uids") + if ( + not isinstance(tensor, torch.Tensor) + or tensor.ndim == 0 + or not isinstance(row_token_uids, torch.Tensor) + or row_token_uids.ndim != 1 + or int(row_token_uids.numel()) != int(tensor.shape[0]) + ): + continue + key = cls._decoder_micro_trace_key(module_name, call) + if key is not None and key not in token_uids_by_key: + token_uids_by_key[key] = row_token_uids + + if not token_uids_by_key: + return + + for module_name in sorted(trace.keys()): + if cls._is_attention_output_trace(module_name): + continue + for call in trace[module_name]: + tensor = call.get("primary_output") + if not isinstance(tensor, torch.Tensor) or tensor.ndim == 0: + continue + key = cls._decoder_micro_trace_key(module_name, call) + if key is None: + continue + token_uids = token_uids_by_key.get(key) + if token_uids is None: + continue + existing_uids = call.get("row_token_uids") + if ( + isinstance(existing_uids, torch.Tensor) + and existing_uids.ndim == 1 + and int(existing_uids.numel()) == int(tensor.shape[0]) + ): + continue + if int(token_uids.numel()) == int(tensor.shape[0]): + call["row_token_uids"] = token_uids + continue + head_count = cls._rank_blocked_token_head_count(call) + if head_count is not None and int( + token_uids.numel() + ) * head_count == int(tensor.shape[0]): + call["row_token_uids"] = expand_token_uids_for_heads( + token_uids, + head_count=head_count, + ) + @classmethod def canonicalize_trace( cls, @@ -975,17 +1363,22 @@ def canonicalize_trace( for module_name in sorted(trace.keys()): calls = trace[module_name] for call_offset, call in enumerate(calls): - if bool(call.get(PRIMARY_OUTPUT_CANONICAL_KEY)): - continue call_index = int(call.get("call_index", call_offset)) tensor = call.get("primary_output") - if isinstance(tensor, torch.Tensor): + if isinstance(tensor, torch.Tensor) and not bool( + call.get(PRIMARY_OUTPUT_CANONICAL_KEY) + ): call["primary_output"] = cls._canonicalize_primary_output_tensor( module_name=module_name, tensor=tensor, call=call, ) call[PRIMARY_OUTPUT_CANONICAL_KEY] = True + cls._propagate_decoder_row_token_uids(trace) + cls._propagate_attention_output_row_token_uids(trace) + for calls in trace.values(): + for call in calls: + cls._canonicalize_call_row_token_order(call) return trace @classmethod @@ -1005,7 +1398,9 @@ def flatten_trace_tensors( if tensor is None: continue call_index = call.get("call_index", call_offset) - flattened[f"{module_name}.call_{call_index}"] = tensor + flattened[ + f"{_normalize_trace_module_name(module_name)}.call_{call_index}" + ] = tensor return flattened @classmethod @@ -1077,25 +1472,145 @@ def _merge_rank_values( return values_by_rank[0] return values_by_rank + @staticmethod + def _expert_parallel_group_key(entry: dict[str, Any]) -> tuple[int, int] | None: + """Returns the expert-data/expert-parallel group for one rank call.""" + rank_meta = entry.get("rank_meta") + if not isinstance(rank_meta, dict): + return None + return ( + _safe_int(rank_meta.get("expert_dp_rank"), 0), + _safe_int(rank_meta.get("ep_rank"), 0), + ) + + @classmethod + def _merge_expert_tensor_parallel_values( + cls, + *, + module_name: str, + key: str, + rank_call_entries: list[dict[str, Any]], + preferred_cat_dim: int | None, + preferred_reduce: str | None, + ) -> Any | None: + """Merges ETP shards before concatenating independent expert rows.""" + if not cls._is_moe_expert_forward_module(module_name): + return None + if preferred_cat_dim != -1 and preferred_reduce != "sum": + return None + entry_values = [ + (entry, entry[key]) for entry in rank_call_entries if key in entry + ] + if not entry_values or not all( + isinstance(value, torch.Tensor) for _, value in entry_values + ): + return None + + grouped: dict[tuple[int, int], list[tuple[dict[str, Any], torch.Tensor]]] = {} + for entry, value in entry_values: + group_key = cls._expert_parallel_group_key(entry) + if group_key is None: + return None + grouped.setdefault(group_key, []).append((entry, cast(torch.Tensor, value))) + + merged_groups: list[torch.Tensor] = [] + for group_key in sorted(grouped): + group_values = [value for _, value in grouped[group_key]] + if key == "row_token_uids": + first = group_values[0] + if not all( + first.shape == value.shape and torch.equal(first, value) + for value in group_values[1:] + ): + raise RuntimeError( + "Expert tensor-parallel trace row UIDs diverged within " + f"group={group_key} module={module_name}" + ) + merged_groups.append(first) + continue + if preferred_reduce == "sum": + merged = cls._merge_rank_values( + group_values, + preferred_reduce="sum", + ) + else: + merged = cls._merge_rank_values( + group_values, + preferred_cat_dim=preferred_cat_dim, + ) + if not isinstance(merged, torch.Tensor): + return None + merged_groups.append(merged) + + if len(merged_groups) == 1: + return merged_groups[0] + if cls._can_cat_along_dim(merged_groups, dim=0): + return torch.cat(merged_groups, dim=0) + return None + @classmethod def _merge_rank_call_entries( cls, + module_name: str, rank_call_entries: list[dict[str, Any]], ) -> dict[str, Any]: """Merges one module call across ranks using per-field merge hints.""" merged_call: dict[str, Any] = {} keys = sorted(set().union(*(entry.keys() for entry in rank_call_entries))) for key in keys: - values = [entry[key] for entry in rank_call_entries if key in entry] + value_entries = [entry for entry in rank_call_entries if key in entry] + values = [entry[key] for entry in value_entries] if key == "rank_meta": merged_call[key] = values continue + if key == "row_token_uids": + primary_hint = next( + ( + cls._primary_output_merge_hint(entry) + for entry in value_entries + if cls._primary_output_merge_hint(entry) is not None + ), + None, + ) + preferred_cat_dim = None + preferred_reduce = None + if isinstance(primary_hint, dict): + if primary_hint.get("op") == "sum": + preferred_reduce = "sum" + elif primary_hint.get("op") == "concat" and isinstance( + primary_hint.get("dim"), int + ): + preferred_cat_dim = int(primary_hint["dim"]) + if primary_hint.get("layout") == "rank_blocked_token_heads": + merged_call[key] = cls._merge_rank_values_with_cp_groups( + values_by_rank=values, + rank_call_entries=value_entries, + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + continue + expert_merged = cls._merge_expert_tensor_parallel_values( + module_name=module_name, + key=key, + rank_call_entries=value_entries, + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + merged_call[key] = ( + expert_merged + if expert_merged is not None + else cls._merge_row_token_uids( + values_by_rank=values, + rank_call_entries=value_entries, + ) + ) + continue preferred_cat_dim: int | None = None preferred_reduce: str | None = None if values and key not in {"merge_hints", "call_index", "module_type"}: hint_values = [ cast(dict[str, Any], entry["merge_hints"]).get(key) - for entry in rank_call_entries + for entry in value_entries if isinstance(entry.get("merge_hints"), dict) ] op_hints = [ @@ -1120,13 +1635,126 @@ def _merge_rank_call_entries( merged_call[f"{key}__row_splits"] = [ int(cast(torch.Tensor, value).shape[0]) for value in values ] - merged_call[key] = cls._merge_rank_values( - values, + expert_merged = cls._merge_expert_tensor_parallel_values( + module_name=module_name, + key=key, + rank_call_entries=value_entries, preferred_cat_dim=preferred_cat_dim, preferred_reduce=preferred_reduce, ) + merged_call[key] = ( + expert_merged + if expert_merged is not None + else cls._merge_rank_values_with_cp_groups( + values_by_rank=values, + rank_call_entries=value_entries, + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + ) return merged_call + @classmethod + def _merge_row_token_uids( + cls, + *, + values_by_rank: list[Any], + rank_call_entries: list[dict[str, Any]], + ) -> Any: + """Preserves row identities across feature-sharded ranks.""" + if not all(isinstance(value, torch.Tensor) for value in values_by_rank): + return cls._merge_rank_values(values_by_rank, preferred_cat_dim=0) + + tensors = cast(list[torch.Tensor], values_by_rank) + grouped_indices: dict[int, list[int]] = {} + for index, entry in enumerate(rank_call_entries): + rank_meta = entry.get("rank_meta") + if not isinstance(rank_meta, dict): + return cls._merge_rank_values(values_by_rank, preferred_cat_dim=0) + cp_rank = _safe_int(rank_meta.get("cp_rank"), 0) + grouped_indices.setdefault(cp_rank, []).append(index) + + merged_by_cp: list[torch.Tensor] = [] + for cp_rank in sorted(grouped_indices): + group_tensors = [tensors[index] for index in grouped_indices[cp_rank]] + first = group_tensors[0] + if all( + first.shape == tensor.shape and torch.equal(first, tensor) + for tensor in group_tensors[1:] + ): + merged_by_cp.append(first) + continue + merged = cls._merge_rank_values(group_tensors, preferred_cat_dim=0) + if not isinstance(merged, torch.Tensor): + return merged + merged_by_cp.append(merged) + + if len(merged_by_cp) == 1: + return merged_by_cp[0] + if cls._can_cat_along_dim(merged_by_cp, dim=0): + return torch.cat(merged_by_cp, dim=0) + return merged_by_cp + + @classmethod + def _merge_rank_values_with_cp_groups( + cls, + *, + values_by_rank: list[Any], + rank_call_entries: list[dict[str, Any]], + preferred_cat_dim: int | None, + preferred_reduce: str | None, + ) -> Any: + """Merges rank values, preserving CP row shards when features are also sharded.""" + cp_world_sizes: set[int] = set() + for entry in rank_call_entries: + rank_meta = entry.get("rank_meta") + if isinstance(rank_meta, dict): + cp_world_sizes.add(_safe_int(rank_meta.get("cp_world_size"), 1)) + if len(cp_world_sizes) != 1 or next(iter(cp_world_sizes), 1) <= 1: + return cls._merge_rank_values( + values_by_rank, + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + if preferred_cat_dim != -1 and preferred_reduce != "sum": + return cls._merge_rank_values( + values_by_rank, + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + + grouped_indices: dict[int, list[int]] = {} + for index, entry in enumerate(rank_call_entries): + rank_meta = entry.get("rank_meta") + if not isinstance(rank_meta, dict): + return cls._merge_rank_values( + values_by_rank, + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + cp_rank = _safe_int(rank_meta.get("cp_rank"), 0) + grouped_indices.setdefault(cp_rank, []).append(index) + if len(grouped_indices) <= 1: + return cls._merge_rank_values( + values_by_rank, + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + + merged_by_cp = [ + cls._merge_rank_values( + [values_by_rank[index] for index in grouped_indices[cp_rank]], + preferred_cat_dim=preferred_cat_dim, + preferred_reduce=preferred_reduce, + ) + for cp_rank in sorted(grouped_indices) + ] + if all(isinstance(value, torch.Tensor) for value in merged_by_cp): + tensors = cast(list[torch.Tensor], merged_by_cp) + if cls._can_cat_along_dim(tensors, dim=0): + return torch.cat(tensors, dim=0) + return merged_by_cp + @staticmethod def _can_cat_along_dim(tensors: list[torch.Tensor], dim: int) -> bool: if not tensors: @@ -1173,7 +1801,10 @@ def _merge_rank_traces( ) grouped_calls.setdefault(merge_key, []).append(call) for merged_index, merge_key in enumerate(sorted(grouped_calls)): - merged_call = cls._merge_rank_call_entries(grouped_calls[merge_key]) + merged_call = cls._merge_rank_call_entries( + module_name, + grouped_calls[merge_key], + ) merged_call["call_index"] = merged_index module_calls.append(merged_call) merged[module_name] = module_calls @@ -1198,63 +1829,173 @@ def _gather_rank_traces( @staticmethod def _merge_group_tensor( - tensors: list[torch.Tensor], *, strict: bool = True + tensors: list[tuple[torch.Tensor, torch.Tensor | None]], ) -> torch.Tensor: if len(tensors) == 1: - return tensors[0] - first = tensors[0] - if all(tensor.shape == first.shape for tensor in tensors[1:]) and all( - torch.equal(first, tensor) for tensor in tensors[1:] + return tensors[0][0] + + tensor_values = [tensor for tensor, _ in tensors] + first = tensor_values[0] + if all(tensor.shape == first.shape for tensor in tensor_values[1:]) and all( + torch.equal(first, tensor) for tensor in tensor_values[1:] ): return first - if not strict: - return first - raise RuntimeError( - "Mismatched output captures for the same micro output across non-DP ranks" - ) + + uid_values = [uids for _, uids in tensors] + if any(uids is None for uids in uid_values): + raise RuntimeError( + "Mismatched output captures for the same micro output across non-DP ranks" + ) + + typed_uid_values = cast(list[torch.Tensor], uid_values) + typed_tensors = [(tensor, cast(torch.Tensor, uids)) for tensor, uids in tensors] + if any(tensor.ndim != 2 for tensor in tensor_values) or any( + uids.ndim != 2 for uids in typed_uid_values + ): + raise RuntimeError( + "Root output UID merge currently requires rank-local 2D tensors" + ) + if any(tensor.shape != uids.shape for tensor, uids in typed_tensors): + raise RuntimeError( + "Root output tensor/token UID shape mismatch during CP merge" + ) + + batch_size = int(first.shape[0]) + max_row_length = 1 + for uids in typed_uid_values: + valid_uids = uids[uids >= 0] + if int(valid_uids.numel()) > 0: + max_row_length = max( + max_row_length, + int(valid_uids.max().item()) + 1, + ) + + merged = first.new_zeros((batch_size, max_row_length)) + filled = torch.zeros((batch_size, max_row_length), dtype=torch.bool) + for tensor, uids in typed_tensors: + for row_index in range(batch_size): + row_uids = uids[row_index] + valid_mask = row_uids >= 0 + if not bool(valid_mask.any()): + continue + row_positions = row_uids[valid_mask].to(dtype=torch.long) + row_values = tensor[row_index, valid_mask] + existing_mask = filled[row_index].index_select(0, row_positions) + if bool(existing_mask.any()): + existing_values = merged[row_index].index_select(0, row_positions) + if not torch.equal(existing_values, row_values): + raise RuntimeError( + "Conflicting CP output values for the same token UID" + ) + merged[row_index].index_copy_(0, row_positions, row_values) + filled[row_index].index_fill_(0, row_positions, True) + + for row_index in range(batch_size): + row_filled = filled[row_index] + present = torch.nonzero(row_filled, as_tuple=False).reshape(-1) + if int(present.numel()) == 0: + continue + expected = torch.arange(int(present.numel()), dtype=torch.long) + if not torch.equal(present, expected): + raise RuntimeError( + "CP output token UIDs did not form a contiguous row-major prefix" + ) + return merged @staticmethod def _gather_rank_outputs( - local_outputs: list[tuple[int | None, int, int | None, torch.Tensor]], - ) -> list[list[tuple[int | None, int, int | None, torch.Tensor]]] | None: + local_outputs: list[ + tuple[int | None, int, int | None, torch.Tensor, torch.Tensor | None] + ], + ) -> ( + list[ + list[ + tuple[ + int | None, + int, + int | None, + torch.Tensor, + torch.Tensor | None, + ] + ] + ] + | None + ): if ( not torch.distributed.is_initialized() # ty: ignore[possibly-missing-attribute] or torch.distributed.get_world_size() == 1 # ty: ignore[possibly-missing-attribute] ): return [local_outputs] gathered: list[ - list[tuple[int | None, int, int | None, torch.Tensor]] | None + list[ + tuple[ + int | None, + int, + int | None, + torch.Tensor, + torch.Tensor | None, + ] + ] + | None ] = [None] * torch.distributed.get_world_size() # ty: ignore[possibly-missing-attribute] torch.distributed.all_gather_object(gathered, local_outputs) # ty: ignore[possibly-missing-attribute] if torch.distributed.get_rank() != 0: # ty: ignore[possibly-missing-attribute] return None return cast( - list[list[tuple[int | None, int, int | None, torch.Tensor]]], + list[ + list[ + tuple[ + int | None, + int, + int | None, + torch.Tensor, + torch.Tensor | None, + ] + ] + ], gathered, ) def ordered_step_outputs(self) -> list[torch.Tensor] | None: + ordered = self.ordered_step_outputs_with_sample_indices() + if ordered is None: + return None + _, outputs = ordered + return outputs + + def ordered_step_outputs_with_sample_indices( + self, + ) -> tuple[list[int | None], list[torch.Tensor]] | None: if not self.enabled: return None gathered_outputs = self._gather_rank_outputs(self.current_step_outputs) if gathered_outputs is None: return None - grouped: dict[tuple[int | None, int | None, int], list[torch.Tensor]] = {} + grouped: dict[ + tuple[int | None, int | None, int], + list[tuple[torch.Tensor, torch.Tensor | None]], + ] = {} for rank_outputs in gathered_outputs: - for sample_index, micro_order, micro_slot, tensor in rank_outputs: + for ( + sample_index, + micro_order, + micro_slot, + tensor, + token_uids, + ) in rank_outputs: group_key = (sample_index, micro_slot, micro_order) - grouped.setdefault(group_key, []).append(tensor) + grouped.setdefault(group_key, []).append((tensor, token_uids)) ordered_group_keys = sorted( grouped, key=lambda item: _captured_output_sort_key(item[0], item[2], item[1]), ) - return [ - self._merge_group_tensor( - grouped[group_key], - strict=self.strict_output_match, - ) - for group_key in ordered_group_keys - ] + return ( + [sample_index for sample_index, _, _ in ordered_group_keys], + [ + self._merge_group_tensor(grouped[group_key]) + for group_key in ordered_group_keys + ], + ) def save_current_step(self, traces_dir: Path) -> Path | None: if not self.enabled or self.current_step_index is None: diff --git a/tests/integration/megatron/model_support/gdn_fp32_reference.py b/tests/integration/megatron/model_support/gdn_fp32_reference.py new file mode 100644 index 000000000..b9049e931 --- /dev/null +++ b/tests/integration/megatron/model_support/gdn_fp32_reference.py @@ -0,0 +1,249 @@ +from __future__ import annotations + +from contextlib import ExitStack +from typing import Any + +import torch +import torch.distributed as dist + + +def _torch_chunk_gated_delta_rule_reference( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + *, + g: torch.Tensor, + beta: torch.Tensor, + initial_state: torch.Tensor | None = None, + output_final_state: bool = False, + use_qk_l2norm_in_kernel: bool = False, + cu_seqlens: torch.Tensor | None = None, + scale: float | None = None, + **kwargs: Any, +) -> tuple[torch.Tensor, torch.Tensor | None]: + from transformers.models.qwen3_5.modeling_qwen3_5 import ( + torch_chunk_gated_delta_rule, + ) + + if kwargs: + raise TypeError( + f"Unsupported Qwen3.5 GDN fp32 reference kwargs: {sorted(kwargs)}" + ) + if scale is not None and scale != float(key.shape[-1] ** -0.5): + raise ValueError( + "Qwen3.5 torch GDN reference only supports the model-default scale" + ) + if cu_seqlens is None: + return torch_chunk_gated_delta_rule( + query, + key, + value, + g=g, + beta=beta, + initial_state=initial_state, + output_final_state=output_final_state, + use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel, + ) + if query.shape[0] != 1: + raise RuntimeError( + "Qwen3.5 packed GDN fp32 reference expects packed batch size 1, " + f"got {query.shape[0]}" + ) + starts = cu_seqlens.detach().cpu().tolist() + outputs: list[torch.Tensor] = [] + finals: list[torch.Tensor] = [] + for index, (start, end) in enumerate(zip(starts, starts[1:])): + state = None if initial_state is None else initial_state[index : index + 1] + output, final = torch_chunk_gated_delta_rule( + query[:, start:end], + key[:, start:end], + value[:, start:end], + g=g[:, start:end], + beta=beta[:, start:end], + initial_state=state, + output_final_state=output_final_state, + use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel, + ) + outputs.append(output) + if final is not None: + finals.append(final) + return torch.cat(outputs, dim=1), torch.cat(finals, dim=0) if finals else None + + +def _pad_sequence_dim(tensor: torch.Tensor, target_tokens: int) -> torch.Tensor: + pad_tokens = target_tokens - int(tensor.shape[1]) + if pad_tokens == 0: + return tensor + if pad_tokens < 0: + raise ValueError( + f"Cannot pad tensor with {int(tensor.shape[1])} tokens to {target_tokens}" + ) + padding = tensor.new_zeros(*tensor.shape[:1], pad_tokens, *tensor.shape[2:]) + return torch.cat((tensor, padding), dim=1) + + +def _autograd_all_gather_varlen( + tensor: torch.Tensor, + *, + group: Any, + token_counts: list[int], +) -> list[torch.Tensor]: + from torch.distributed.nn.functional import all_gather + + max_tokens = max(token_counts) + padded = _pad_sequence_dim(tensor, max_tokens) + gathered = all_gather(padded, group=group) + return [ + rank_tensor[:, :token_count] + for rank_tensor, token_count in zip(gathered, token_counts) + ] + + +def _split_segments_by_rank( + gathered: list[torch.Tensor], + lengths_by_rank_cpu: torch.Tensor, +) -> list[list[torch.Tensor]]: + return [ + list(rank_tensor.split(lengths_by_rank_cpu[rank].tolist(), dim=1)) + for rank, rank_tensor in enumerate(gathered) + ] + + +def _cat_non_empty(tensors: list[torch.Tensor], *, dim: int) -> torch.Tensor: + non_empty = [tensor for tensor in tensors if int(tensor.shape[dim]) != 0] + if non_empty: + return torch.cat(non_empty, dim=dim) + return tensors[0] + + +def _torch_chunk_gated_delta_rule_native_cp_reference( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + *, + g: torch.Tensor, + beta: torch.Tensor, + initial_state: torch.Tensor, + group: Any, + output_final_state: bool, + cu_seqlens: torch.Tensor | None = None, + cu_seqlens_cpu: torch.Tensor | None = None, + lengths_by_rank_cpu: torch.Tensor | None = None, + scale: float | None = None, +) -> tuple[torch.Tensor, torch.Tensor | None]: + if group is None: + raise ValueError("Qwen3.5 GDN fp32 CP reference requires a process group") + if lengths_by_rank_cpu is None: + raise ValueError("Qwen3.5 GDN fp32 CP reference requires all-rank lengths") + if lengths_by_rank_cpu.device.type != "cpu": + raise ValueError("Qwen3.5 GDN fp32 CP reference lengths must stay on CPU") + world_size = dist.get_world_size(group) # ty: ignore[possibly-missing-attribute] + rank = dist.get_rank(group) # ty: ignore[possibly-missing-attribute] + segment_count = int(initial_state.shape[0]) + if tuple(lengths_by_rank_cpu.shape) != (world_size, segment_count): + raise ValueError( + "Qwen3.5 GDN fp32 CP reference lengths must be [world_size, segments], " + f"got {tuple(lengths_by_rank_cpu.shape)}" + ) + local_tokens = int(lengths_by_rank_cpu[rank].sum().item()) + if int(q.shape[1]) != local_tokens: + raise ValueError( + "Qwen3.5 GDN fp32 CP reference local token count mismatch: " + f"q has {int(q.shape[1])}, metadata has {local_tokens}" + ) + if cu_seqlens is not None and cu_seqlens_cpu is None: + raise ValueError("Qwen3.5 GDN fp32 CP reference requires CPU cu_seqlens") + + token_counts = [ + int(lengths_by_rank_cpu[peer].sum().item()) for peer in range(world_size) + ] + q_by_segment = _split_segments_by_rank( + _autograd_all_gather_varlen(q, group=group, token_counts=token_counts), + lengths_by_rank_cpu, + ) + k_by_segment = _split_segments_by_rank( + _autograd_all_gather_varlen(k, group=group, token_counts=token_counts), + lengths_by_rank_cpu, + ) + v_by_segment = _split_segments_by_rank( + _autograd_all_gather_varlen(v, group=group, token_counts=token_counts), + lengths_by_rank_cpu, + ) + g_by_segment = _split_segments_by_rank( + _autograd_all_gather_varlen(g, group=group, token_counts=token_counts), + lengths_by_rank_cpu, + ) + beta_by_segment = _split_segments_by_rank( + _autograd_all_gather_varlen(beta, group=group, token_counts=token_counts), + lengths_by_rank_cpu, + ) + + local_outputs: list[torch.Tensor] = [] + final_states: list[torch.Tensor] = [] + for segment_index in range(segment_count): + full_output, full_final = _torch_chunk_gated_delta_rule_reference( + _cat_non_empty( + [rank_segments[segment_index] for rank_segments in q_by_segment], + dim=1, + ), + _cat_non_empty( + [rank_segments[segment_index] for rank_segments in k_by_segment], + dim=1, + ), + _cat_non_empty( + [rank_segments[segment_index] for rank_segments in v_by_segment], + dim=1, + ), + g=_cat_non_empty( + [rank_segments[segment_index] for rank_segments in g_by_segment], + dim=1, + ), + beta=_cat_non_empty( + [rank_segments[segment_index] for rank_segments in beta_by_segment], + dim=1, + ), + initial_state=initial_state[segment_index : segment_index + 1], + output_final_state=output_final_state, + use_qk_l2norm_in_kernel=False, + scale=scale, + ) + segment_start = int(lengths_by_rank_cpu[:rank, segment_index].sum().item()) + segment_len = int(lengths_by_rank_cpu[rank, segment_index].item()) + local_outputs.append( + full_output[:, segment_start : segment_start + segment_len] + ) + if full_final is not None: + final_states.append(full_final) + output = torch.cat(local_outputs, dim=1) + final_state = torch.cat(final_states, dim=0) if final_states else None + return output, final_state + + +def install_megatron_qwen35_gdn_fp32_reference( + stack: ExitStack, + *, + base_model: str, +) -> None: + model_key = base_model.lower() + if "qwen3.5" not in model_key and "qwen3_5" not in model_key: + return + from art.megatron.gdn import operator as gdn_operator + + original_single_rank = gdn_operator._chunk_gated_delta_rule + original_native_cp = gdn_operator.chunk_gated_delta_rule_native_cp + setattr( + gdn_operator, + "_chunk_gated_delta_rule", + _torch_chunk_gated_delta_rule_reference, + ) + setattr( + gdn_operator, + "chunk_gated_delta_rule_native_cp", + _torch_chunk_gated_delta_rule_native_cp_reference, + ) + stack.callback( + setattr, gdn_operator, "chunk_gated_delta_rule_native_cp", original_native_cp + ) + stack.callback( + setattr, gdn_operator, "_chunk_gated_delta_rule", original_single_rank + ) diff --git a/tests/integration/megatron/model_support/gdn_trace_uids.py b/tests/integration/megatron/model_support/gdn_trace_uids.py new file mode 100644 index 000000000..100dfb656 --- /dev/null +++ b/tests/integration/megatron/model_support/gdn_trace_uids.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +from contextlib import contextmanager +from typing import Any, Iterator + +import torch +from torch import Tensor + +from .trace_uids import TRACE_ROW_TOKEN_UIDS_ATTR, TRACE_UID_SPAN_ATTR + + +class GdnTraceTokenUidHooks: + def attach_token_uids(self, tensor: Tensor, token_uids: Tensor) -> Tensor: + setattr( + tensor, + TRACE_ROW_TOKEN_UIDS_ATTR, + token_uids.detach().to(device="cpu", dtype=torch.int64).reshape(-1), + ) + setattr(tensor, TRACE_UID_SPAN_ATTR, None) + return tensor + + def token_uids_from_tensor(self, tensor: Tensor) -> Tensor | None: + token_uids = getattr(tensor, TRACE_ROW_TOKEN_UIDS_ATTR, None) + if not isinstance(token_uids, Tensor): + return None + return token_uids.detach().to(device="cpu", dtype=torch.int64).reshape(-1) + + def set_module_token_uids(self, module: Any, token_uids: Tensor | None) -> None: + if module is None or token_uids is None: + return + setattr( + module, + TRACE_ROW_TOKEN_UIDS_ATTR, + token_uids.detach().to(device="cpu", dtype=torch.int64).reshape(-1), + ) + if hasattr(module, TRACE_UID_SPAN_ATTR): + delattr(module, TRACE_UID_SPAN_ATTR) + + @torch.compiler.disable + def prepare_in_proj_token_uids(self, gdn: Any, hidden_states: Tensor) -> None: + from art.megatron.gdn import operator as gdn_operator + + token_uids = self.token_uids_from_tensor(hidden_states) + if token_uids is None: + return + projection = gdn_operator._gdn_input_projection(gdn) + if projection is None: + return + output_uids = self.column_parallel_input_token_uids( + token_uids, + hidden_states, + projection, + ) + in_proj = getattr(gdn, "in_proj", None) + self.set_module_token_uids(in_proj, output_uids) + self.set_module_token_uids(projection, output_uids) + self.set_module_token_uids(getattr(in_proj, "qkv_lora", None), output_uids) + self.set_module_token_uids(getattr(in_proj, "z_lora", None), output_uids) + + @torch.compiler.disable + def column_parallel_input_token_uids( + self, token_uids: Tensor, hidden_states: Tensor, projection: Any + ) -> Tensor: + from art.megatron.gdn import operator as gdn_operator + + if not gdn_operator._uses_sequence_parallel(projection): + return token_uids.to(dtype=torch.int64).reshape(-1) + seq_len, batch_size, _hidden_size = hidden_states.shape + expected = int(seq_len) * int(batch_size) + if int(token_uids.numel()) != expected: + return token_uids.to(dtype=torch.int64).reshape(-1) + uid_tensor = ( + token_uids.to(device=hidden_states.device, dtype=torch.int64) + .reshape(batch_size, seq_len) + .transpose(0, 1) + .contiguous() + .unsqueeze(-1) + ) + gathered = gdn_operator._column_parallel_input(uid_tensor, projection) + return ( + gathered.squeeze(-1) + .transpose(0, 1) + .contiguous() + .reshape(-1) + .detach() + .to(device="cpu", dtype=torch.int64) + ) + + def set_out_proj_lora_token_uids(self, gdn: Any, hidden_states: Tensor) -> None: + token_uids = self.token_uids_from_tensor(hidden_states) + if token_uids is None: + return + self.set_module_token_uids( + getattr(getattr(gdn, "out_proj", None), "lora", None), + token_uids, + ) + + def set_out_norm_token_uids(self, gdn: Any, token_uids: Tensor) -> None: + from art.megatron.gdn import operator as gdn_operator + + self.set_module_token_uids( + getattr(gdn, "out_norm", None), + token_uids.repeat_interleave(gdn_operator._local_value_heads(gdn)), + ) + + def set_out_proj_token_uids( + self, + gdn: Any, + hidden_states: Tensor, + *, + sequence_parallel_output: bool, + ) -> None: + from art.megatron.gdn import operator as gdn_operator + + token_uids = self.token_uids_from_tensor(hidden_states) + if token_uids is None: + return + projection = gdn_operator._gdn_output_projection(gdn) + output_uids = self.row_parallel_output_token_uids( + token_uids, + hidden_states, + projection, + sequence_parallel_output=sequence_parallel_output, + ) + self.set_module_token_uids(getattr(gdn, "out_proj", None), output_uids) + self.set_module_token_uids(projection, output_uids) + + def row_parallel_output_token_uids( + self, + token_uids: Tensor, + hidden_states: Tensor, + projection: Any | None, + *, + sequence_parallel_output: bool, + ) -> Tensor: + from art.megatron.gdn import operator as gdn_operator + + token_uids = token_uids.to(dtype=torch.int64).reshape(-1) + if ( + projection is None + or not gdn_operator._uses_sequence_parallel(projection) + or not sequence_parallel_output + ): + return token_uids + token_count = gdn_operator._hidden_token_count(hidden_states) + if token_count <= 0: + return token_uids.new_empty((0,)) + if int(token_uids.numel()) != token_count: + return token_uids + tp_size = gdn_operator._tp_world_size(projection) + tp_rank = gdn_operator._tp_rank(projection) + rows_per_rank, remainder = divmod(token_count, tp_size) + if remainder != 0: + return token_uids + start = tp_rank * rows_per_rank + return token_uids[start : start + rows_per_rank].contiguous() + + def pad_token_uids_for_stream(self, token_uids: Tensor, stream: Tensor) -> Tensor: + from art.megatron.gdn import operator as gdn_operator + + token_count = gdn_operator._hidden_token_count(stream) + if token_count == int(token_uids.numel()): + return token_uids + padded = token_uids.new_full((token_count,), -1) + copied = min(token_count, int(token_uids.numel())) + if copied: + padded[:copied] = token_uids[:copied] + return padded + + +@contextmanager +def install_gdn_trace_token_uid_hooks() -> Iterator[None]: + from art.megatron.gdn import operator as gdn_operator + + previous = gdn_operator.set_gdn_trace_token_uid_hooks(GdnTraceTokenUidHooks()) + try: + yield + finally: + gdn_operator.set_gdn_trace_token_uid_hooks(previous) diff --git a/tests/integration/megatron/model_support/hf_parity.py b/tests/integration/megatron/model_support/hf_parity.py index cdb99d92f..55b355838 100644 --- a/tests/integration/megatron/model_support/hf_parity.py +++ b/tests/integration/megatron/model_support/hf_parity.py @@ -8,28 +8,36 @@ from pydantic import BaseModel, Field -from art.megatron.model_support.spec import MinimalLayerCoverageReport - +from ..artifacts import GitRepoState, pinned_git_state from .oracle_harness import ( NON_FINITE_METRIC_VALUE, ORACLE_TOPOLOGY, DiffAccumulator, DiskPackedTensorsSpec, + MetricThresholdRule, OracleCaseConfig, + PackedTensorConfig, PhasePassFn, - _default_phase_pass_fns, + _prune_case_artifacts, _read_json, _write_json, ensure_case_artifacts, ) from .oracle_worker import provider_topology_env +from .validation_spec import MinimalLayerCoverageReport from .workflow import assess_minimal_layer_coverage HF_PARITY_ENABLE_ENV = "ART_RUN_HF_PARITY" HF_PARITY_OUTPUT_DIRNAME = "hf_parity_sft" HF_PARITY_REPORT_FILENAME = "report.json" +HF_PARITY_PACKED_TENSORS = PackedTensorConfig( + sequence_length=256, + prefill_tokens=64, + decode_tokens=64, +) REPO_ROOT = Path(__file__).resolve().parents[4] +HF_PARITY_ARTIFACT_SUITE_NAME = "Megatron HF parity artifacts" class HfParityMetricRow(BaseModel): @@ -46,6 +54,7 @@ class HfParityMetricRow(BaseModel): class HfParityRunRequest(BaseModel): + git: GitRepoState case_id: str case_config: OracleCaseConfig packed_tensors: DiskPackedTensorsSpec @@ -54,6 +63,7 @@ class HfParityRunRequest(BaseModel): class HfParityReport(BaseModel): + git: GitRepoState case_id: str base_model: str model_key: str @@ -66,7 +76,32 @@ class HfParityReport(BaseModel): def _hf_parity_phase_pass_fns() -> dict[str, PhasePassFn]: - return _default_phase_pass_fns() + non_zero_scales = {"typical_abs_scale": 0.0, "candidate_abs_scale": 0.0} + fwd_out = MetricThresholdRule( + limits={"relative_l2": 1e-2, "mean_abs_pct": 1.0}, + minimums=non_zero_scales, + ) + loss = MetricThresholdRule( + limits={"relative_l2": 2e-2, "mean_abs_pct": 2.0}, + minimums=non_zero_scales, + ) + grads_deltas = MetricThresholdRule( + limits={"mean_abs_pct": 3.0}, + minimums=non_zero_scales, + ) + return { + "forward": fwd_out, + "outputs": fwd_out, + "losses": loss, + "grads": grads_deltas, + "deltas": grads_deltas, + } + + +def hf_parity_case_config(case_config: OracleCaseConfig) -> OracleCaseConfig: + return case_config.model_copy( + update={"packed_tensors": HF_PARITY_PACKED_TENSORS.model_copy(deep=True)} + ) def hf_parity_enabled() -> bool: @@ -93,6 +128,7 @@ def _build_metric_row( param: str, summary: dict[str, float], structural_failure: str | None = None, + phase_pass_fns: dict[str, PhasePassFn] | None = None, ) -> HfParityMetricRow: row = HfParityMetricRow( phase=phase, @@ -104,7 +140,7 @@ def _build_metric_row( candidate_abs_scale=summary["candidate_abs_scale"], mean_abs_pct=summary["mean_abs_pct"], ) - pass_fn = _hf_parity_phase_pass_fns().get(phase) + pass_fn = (phase_pass_fns or _hf_parity_phase_pass_fns()).get(phase) if pass_fn is None: row.pass_signal = structural_failure is None if structural_failure is not None: @@ -133,6 +169,7 @@ def build_tensor_map_metric_rows( phase: str, reference: dict[str, Any], candidate: dict[str, Any], + phase_pass_fns: dict[str, PhasePassFn] | None = None, ) -> list[HfParityMetricRow]: reference_keys = set(reference.keys()) candidate_keys = set(candidate.keys()) @@ -145,6 +182,7 @@ def build_tensor_map_metric_rows( param="__tensor_set__", summary=_inf_summary(), structural_failure=f"missing={missing[:5]} extra={extra[:5]}", + phase_pass_fns=phase_pass_fns, ) ] rows: list[HfParityMetricRow] = [] @@ -156,6 +194,7 @@ def build_tensor_map_metric_rows( param=key, summary=_inf_summary(), structural_failure=f"shape mismatch for '{key}'", + phase_pass_fns=phase_pass_fns, ) ) continue @@ -164,6 +203,7 @@ def build_tensor_map_metric_rows( phase=phase, param=key, summary=summarize_tensor_pair(reference[key], candidate[key]), + phase_pass_fns=phase_pass_fns, ) ) return rows @@ -283,6 +323,7 @@ def run_hf_parity( *, case_config: OracleCaseConfig, ) -> HfParityReport: + case_config = hf_parity_case_config(case_config) if case_config.precision != "fp32": raise ValueError("HF parity currently requires fp32 precision") if case_config.num_steps != 1: @@ -300,6 +341,7 @@ def run_hf_parity( f"risks={coverage.unresolved_risks}" ) + git = pinned_git_state(HF_PARITY_ARTIFACT_SUITE_NAME) case_artifacts = ensure_case_artifacts(case_config) output_dir = Path(case_artifacts.case_dir) / HF_PARITY_OUTPUT_DIRNAME report_path = output_dir / HF_PARITY_REPORT_FILENAME @@ -307,6 +349,7 @@ def run_hf_parity( if report_path.exists(): report_path.unlink() request = HfParityRunRequest( + git=git, case_id=case_artifacts.case_id, case_config=case_config, packed_tensors=case_artifacts.packed_tensors, @@ -317,6 +360,7 @@ def run_hf_parity( run_hf_parity_subprocess(request, output_dir) report = HfParityReport.model_validate(_read_json(report_path)) assert_hf_parity_pass(report, report_path=report_path) + _prune_case_artifacts(Path(case_artifacts.case_dir)) return report @@ -327,22 +371,26 @@ def build_hf_parity_report( loss_summary: dict[str, float], grads_rows: list[HfParityMetricRow], ) -> HfParityReport: + phase_pass_fns = _hf_parity_phase_pass_fns() rows = [ _build_metric_row( phase="outputs", param="trainable_token_losses", summary=outputs_summary, + phase_pass_fns=phase_pass_fns, ), _build_metric_row( phase="losses", param="loss", summary=loss_summary, + phase_pass_fns=phase_pass_fns, ), *grads_rows, ] pass_count = sum(1 for row in rows if row.pass_signal) fail_count = len(rows) - pass_count return HfParityReport( + git=request.git, case_id=request.case_id, base_model=request.case_config.base_model, model_key=request.coverage.model_key, @@ -358,6 +406,7 @@ def build_hf_parity_report( __all__ = [ "HF_PARITY_ENABLE_ENV", "HF_PARITY_OUTPUT_DIRNAME", + "HF_PARITY_PACKED_TENSORS", "HF_PARITY_REPORT_FILENAME", "HfParityMetricRow", "HfParityReport", @@ -366,6 +415,7 @@ def build_hf_parity_report( "build_hf_parity_report", "build_parity_sample_indices", "build_tensor_map_metric_rows", + "hf_parity_case_config", "hf_parity_enabled", "run_hf_parity", "set_hf_config_num_layers", diff --git a/tests/integration/megatron/model_support/hf_parity_canonicalization.py b/tests/integration/megatron/model_support/hf_parity_canonicalization.py new file mode 100644 index 000000000..ba84b1069 --- /dev/null +++ b/tests/integration/megatron/model_support/hf_parity_canonicalization.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import re + +import torch + +_FUSED_MOE_EXPERT_PATTERN = re.compile( + r"^(?P.*\.mlp\.experts)\.(?Pgate_up_proj|down_proj)(?:\.weight)?$" +) + + +def _strip_language_model_prefix(key: str) -> str: + if key.startswith("model.language_model."): + return f"model.{key.removeprefix('model.language_model.')}" + return key + + +def _expected_unfused_experts_for_prefix( + expected_keys: set[str], + prefix: str, + *, + param: str, +) -> bool: + simplified_expected_keys = { + _strip_language_model_prefix(key) for key in expected_keys + } + if param == "gate_up_proj": + return ( + f"{prefix}.0.gate_proj.weight" in simplified_expected_keys + or f"{prefix}.0.up_proj.weight" in simplified_expected_keys + ) + if param == "down_proj": + return f"{prefix}.0.down_proj.weight" in simplified_expected_keys + return False + + +def hf_tensor_map_to_art_canonical( + hf_tensor_map: dict[str, torch.Tensor], + *, + expected_keys: set[str], +) -> dict[str, torch.Tensor]: + canonical: dict[str, torch.Tensor] = {} + for key, value in hf_tensor_map.items(): + match = _FUSED_MOE_EXPERT_PATTERN.match(key) + if match is None: + canonical[key] = value + continue + + prefix = match.group("prefix") + param = match.group("param") + if value.ndim != 3 or not _expected_unfused_experts_for_prefix( + expected_keys, + prefix, + param=param, + ): + canonical[key] = value + continue + + num_experts = int(value.shape[0]) + if param == "gate_up_proj": + if value.shape[1] % 2 != 0: + canonical[key] = value + continue + gate_proj, up_proj = value.chunk(2, dim=1) + for expert in range(num_experts): + canonical[f"{prefix}.{expert}.gate_proj.weight"] = gate_proj[expert] + canonical[f"{prefix}.{expert}.up_proj.weight"] = up_proj[expert] + continue + + for expert in range(num_experts): + canonical[f"{prefix}.{expert}.down_proj.weight"] = value[expert] + + return canonical diff --git a/tests/integration/megatron/model_support/hf_parity_worker.py b/tests/integration/megatron/model_support/hf_parity_worker.py index 35107053c..8279b3abf 100644 --- a/tests/integration/megatron/model_support/hf_parity_worker.py +++ b/tests/integration/megatron/model_support/hf_parity_worker.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +from contextlib import ExitStack import faulthandler import os from pathlib import Path @@ -13,7 +14,6 @@ import torch.nn.functional as F from art.megatron import train as megatron_train -from art.megatron.model_support import get_model_support_handler from art.megatron.routing_replay import ( MoeRoutingReplayBundle, RouterCallRoute, @@ -23,12 +23,15 @@ from art.megatron.routing_replay import ( ParallelTopology as ReplayParallelTopology, ) +from art.megatron.training.trace import prepare_replay_local_input_token_uids from art.megatron.weights.merged_weight_export import build_art_conversion_tasks from art.preprocessing.pack import packed_tensors_from_dir +from .gdn_fp32_reference import install_megatron_qwen35_gdn_fp32_reference from .hf_parity import ( HF_PARITY_REPORT_FILENAME, HfParityRunRequest, + _hf_parity_phase_pass_fns, build_hf_parity_report, build_parity_sample_indices, build_tensor_map_metric_rows, @@ -36,8 +39,17 @@ summarize_tensor_pair, zero_hf_dropout_config, ) -from .oracle_harness import ORACLE_TOPOLOGY, _read_json, _write_json +from .hf_parity_canonicalization import hf_tensor_map_to_art_canonical +from .oracle_harness import ( + ORACLE_TOPOLOGY, + TEST_DEFAULT_FLEX_BACKEND, + _read_json, + _write_json, +) from .oracle_worker import ( + _apply_requested_flex_backend_patch, + _apply_test_attention_full_fp32_patch, + _apply_test_flex_inner_fp32_patch, _assert_runtime_configuration, _build_optimizer_config, _configure_cuda_precision, @@ -156,6 +168,7 @@ def _hook(_module: Any, _inputs: Any, output: Any) -> None: ) route = RouterCallRoute( expert_indices=router_indices.detach().cpu().to(torch.int32), + expert_probs=router_scores.detach().cpu().to(torch.float32), expert_mask=torch.ones_like( router_indices.detach().cpu(), dtype=torch.bool ), @@ -263,6 +276,7 @@ def _load_hf_model( base_model: str, num_layers: int, device: torch.device, + dtype: torch.dtype, ) -> Any: from transformers import AutoConfig, AutoModelForCausalLM @@ -273,7 +287,7 @@ def _load_hf_model( base_model, config=config, trust_remote_code=True, - torch_dtype=torch.float32, + torch_dtype=dtype, low_cpu_mem_usage=True, ) model.train() @@ -434,6 +448,7 @@ def _run_hf_sft_step( sample_indices: list[int | None], topology: ReplayParallelTopology, device: torch.device, + dtype: torch.dtype, ) -> tuple[ torch.Tensor, torch.Tensor, @@ -441,7 +456,14 @@ def _run_hf_sft_step( MoeRoutingReplayBundle | None, ]: _debug("loading HF model") - model = _load_hf_model(base_model=base_model, num_layers=num_layers, device=device) + model = _load_hf_model( + base_model=base_model, + num_layers=num_layers, + device=device, + dtype=dtype, + ) + if dtype == torch.float32: + _install_hf_qwen35_gdn_fp32_reference(model, base_model=base_model) route_capture = _HfMoeRoutingCapture(model) _debug("running HF forward/backward") model.zero_grad(set_to_none=True) @@ -471,7 +493,7 @@ def _run_hf_sft_step( ).logits shifted_labels = megatron_train.shift_tensor(labels, -100) per_token_loss = F.cross_entropy( - logits.reshape(-1, logits.shape[-1]), + logits.float().reshape(-1, logits.shape[-1]), shifted_labels.reshape(-1), reduction="none", ignore_index=-100, @@ -494,6 +516,22 @@ def _run_hf_sft_step( return output_vector, scalar_loss, grads, routing_replay_bundle +def _install_hf_qwen35_gdn_fp32_reference(model: Any, *, base_model: str) -> None: + model_key = base_model.lower() + if "qwen3.5" not in model_key and "qwen3_5" not in model_key: + return + patched = 0 + for module in model.modules(): + module_impl = sys.modules.get(type(module).__module__) + torch_impl = getattr(module_impl, "torch_chunk_gated_delta_rule", None) + if torch_impl is None or not hasattr(module, "chunk_gated_delta_rule"): + continue + module.chunk_gated_delta_rule = torch_impl + patched += 1 + if patched == 0: + raise RuntimeError("Qwen3.5 HF parity found no GDN modules to patch") + + def _build_megatron_runtime( request: HfParityRunRequest, *, @@ -501,7 +539,7 @@ def _build_megatron_runtime( ) -> megatron_train.TrainingRuntime: return megatron_train.build_training_runtime( model_identifier=request.case_config.base_model, - provider_torch_dtype=torch.float32, + provider_torch_dtype=_dtype_for_precision(request.case_config.precision), provider_bundle_configure=_install_bridge_timing_debug, provider_configure=lambda provider: _configure_provider( provider, ORACLE_TOPOLOGY, request.case_config @@ -515,6 +553,14 @@ def _build_megatron_runtime( ) +def _dtype_for_precision(precision: str) -> torch.dtype: + if precision == "bf16": + return torch.bfloat16 + if precision == "fp32": + return torch.float32 + raise ValueError(f"Unsupported HF parity precision: {precision}") + + def _megatron_task_tensor( task: Any, *, @@ -636,7 +682,7 @@ def _run_megatron_sft_step( request, moe_routing_replay_bundle=moe_routing_replay_bundle, ) - _assert_runtime_configuration(runtime.model, request.case_config) + _assert_runtime_configuration(runtime.model, request.case_config, ORACLE_TOPOLOGY) assert runtime.optimizer is not None if moe_routing_replay_bundle is not None: controller = runtime.moe_routing_replay_controller @@ -674,32 +720,40 @@ def _run_megatron_sft_step( sample_indices[micro_order], micro_order, ) - input_ids, position_ids, shifted_labels, mask, seq_len = ( - megatron_train._prepare_sft_micro_inputs(micro, device) + prepared_micro = megatron_train._prepare_dense_sft_micro( + micro, + device=device, + provider=runtime.provider, + model_support_handler=runtime.model_support_handler, + ) + prepare_replay_local_input_token_uids( + runtime.moe_routing_replay_controller, + prepared_micro.local_token_uids, + prepared_micro.attention_state, ) attention_mask = megatron_train._placeholder_attention_mask(device) forward_kwargs = runtime.model_support_handler.get_forward_kwargs( runtime.model[0], - attention_bias=megatron_train._causal_attention_state(seq_len, device), + attention_bias=prepared_micro.attention_state, ) per_token_loss = runtime.model[0]( - input_ids=input_ids, - position_ids=position_ids, + input_ids=prepared_micro.input_ids, + position_ids=prepared_micro.position_ids, attention_mask=attention_mask, - labels=shifted_labels, + labels=prepared_micro.labels, **forward_kwargs, ) - masked_losses = per_token_loss[mask] + masked_losses = per_token_loss[prepared_micro.loss_mask] trainable_losses.append(masked_losses.detach().cpu()) loss_sum = loss_sum + masked_losses.sum() - token_count += int(mask.sum().item()) + token_count += int(prepared_micro.loss_mask.sum().item()) masked_losses.sum().backward() _debug("finished Megatron forward/backward") num_tokens = megatron_train._local_trainable_sft_token_count_tensor( micro_inputs, device=device, ) - megatron_train._flush_param_grads_to_main_grads(runtime.model) + megatron_train.flush_param_grads_to_main_grads(runtime.model) megatron_train.finalize_model_grads_extended( megatron_train.as_megatron_api_chunks(runtime.model), num_tokens=num_tokens, @@ -730,10 +784,9 @@ def _normalize_hf_grads_for_bridge( hf_grads: dict[str, torch.Tensor], *, expected_grad_keys: set[str], - model_support_handler: Any, ) -> dict[str, torch.Tensor]: hf_grads = _filter_language_only_tensor_map(hf_grads) - hf_grads = model_support_handler.hf_tensor_map_to_art_canonical( + hf_grads = hf_tensor_map_to_art_canonical( hf_grads, expected_keys=expected_grad_keys, ) @@ -779,12 +832,24 @@ def _worker_run(request: HfParityRunRequest) -> None: ) ) device = torch.device("cuda", 0) + flex_patch_stack = ExitStack() + flex_patch_stack.enter_context( + _apply_requested_flex_backend_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + dtype = _dtype_for_precision(request.case_config.precision) + if dtype == torch.float32: + flex_patch_stack.enter_context( + _apply_test_flex_inner_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + flex_patch_stack.enter_context( + _apply_test_attention_full_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + install_megatron_qwen35_gdn_fp32_reference( + flex_patch_stack, + base_model=request.case_config.base_model, + ) try: _debug("starting HF parity worker") - model_support_handler = get_model_support_handler( - request.case_config.base_model, - allow_unvalidated_arch=request.case_config.allow_unvalidated_arch, - ) hf_outputs, hf_loss, hf_grads, moe_routing_replay_bundle = _run_hf_sft_step( base_model=request.case_config.base_model, num_layers=request.case_config.num_layers, @@ -792,6 +857,7 @@ def _worker_run(request: HfParityRunRequest) -> None: sample_indices=sample_indices, topology=replay_topology, device=device, + dtype=dtype, ) megatron_outputs, megatron_loss, megatron_grads = _run_megatron_sft_step( request=request, @@ -804,7 +870,6 @@ def _worker_run(request: HfParityRunRequest) -> None: normalized_hf_grads = _normalize_hf_grads_for_bridge( hf_grads, expected_grad_keys=set(megatron_grads.keys()), - model_support_handler=model_support_handler, ) active_embedding_rows = _active_embedding_token_rows(micro_inputs) active_router_rows = _active_router_rows_by_layer(moe_routing_replay_bundle) @@ -835,6 +900,7 @@ def _worker_run(request: HfParityRunRequest) -> None: phase="grads", reference=normalized_hf_grads, candidate=megatron_grads, + phase_pass_fns=_hf_parity_phase_pass_fns(), ) report = build_hf_parity_report( request=request, @@ -848,6 +914,7 @@ def _worker_run(request: HfParityRunRequest) -> None: ) _debug("wrote HF parity report") finally: + flex_patch_stack.close() if torch.distributed.is_initialized(): # ty: ignore[possibly-missing-attribute] torch.distributed.destroy_process_group() # ty: ignore[possibly-missing-attribute] diff --git a/tests/integration/megatron/model_support/lora_coverage.py b/tests/integration/megatron/model_support/lora_coverage.py index 1c30c1918..07097c4d4 100644 --- a/tests/integration/megatron/model_support/lora_coverage.py +++ b/tests/integration/megatron/model_support/lora_coverage.py @@ -132,6 +132,9 @@ def _covered_exported_target_modules( if ".mlp.experts.linear_fc2" in base_name: covered.update({"experts", "down_proj"}) continue + if ".mlp.experts.linear_fc" in base_name: + covered.add("experts") + continue if ".linear_fc1.weight" in base_name: covered.update({"gate_proj", "up_proj"}) continue diff --git a/tests/integration/megatron/model_support/oracle_harness.py b/tests/integration/megatron/model_support/oracle_harness.py index 6cf93caaa..189d75d7b 100644 --- a/tests/integration/megatron/model_support/oracle_harness.py +++ b/tests/integration/megatron/model_support/oracle_harness.py @@ -17,23 +17,42 @@ import torch from art.megatron.routing_replay import ROUTER_KEY_FORMAT_VERSION +from art.megatron.training.streaming_weight_offload import StreamingWeightOffloadConfig +from ..artifacts import GitRepoState, pinned_git_state +from ..metrics import DEFAULT_MEAN_ABS_PCT_THRESHOLD, mean_abs_pct_from_sums from .forward_trace import ForwardTraceCapture REPO_ROOT = Path(__file__).resolve().parents[4] ARTIFACT_ROOT = Path(REPO_ROOT / ".local/megatron_lora_correctness") +LIVE_TRAINING_LOG_PATH = REPO_ROOT / ".local" / "live_training.log" ORACLE_MOE_ROUTING_BUNDLE_DIRNAME = "oracle_moe_routing_replay" REGENERATE_ENV = "ART_REGENERATE_ORACLE" SENSITIVITY_MUTATION_ENV = "ART_SENSITIVITY_MUTATIONS" ORACLE_OBJECTIVE_ENV = "ART_ORACLE_OBJECTIVE" +ORACLE_BASE_MODEL_ENV = "ART_ORACLE_BASE_MODEL" KEEP_TOPOLOGY_ARTIFACTS_ENV = "ART_ORACLE_KEEP_TOPOLOGY_ARTIFACTS" +ORACLE_ARTIFACT_SUITE_NAME = "Megatron oracle artifacts" OracleObjective = Literal["rl", "sft"] SUPPORTED_ORACLE_OBJECTIVES: tuple[OracleObjective, ...] = ("rl", "sft") SensitivityMutation = str +FlexBackend = Literal[ + "FLASH", + "TRITON", + "TRITON_LEGACY", + "TRITON_LEGACY_INNER_FP32", + "TRITON_LEGACY_FULL_FP32", +] +TEST_DEFAULT_FLEX_BACKEND: FlexBackend = "TRITON" DEFAULT_SENSITIVITY_MUTATION = "skip_finalize" +CP_ATTENTION_SENSITIVITY_MUTATIONS = ( + "attn_kv_fetch_pack_on_comm_stream", + "attn_skip_nested_grad_sanitize", + "attn_skip_flash_lse_normalize", +) SHARED_SENSITIVITY_MUTATIONS = ( DEFAULT_SENSITIVITY_MUTATION, "fwd_skip_o_proj_tp_reduce", @@ -44,6 +63,7 @@ "save_drop_nonzero_ranked_tp_shards", "save_duplicate_replicated_entries", "dp_grad_accumulation_seqs", + *CP_ATTENTION_SENSITIVITY_MUTATIONS, ) RL_ONLY_SENSITIVITY_MUTATIONS = ("dp_local_token_normalization",) SFT_ONLY_SENSITIVITY_MUTATIONS = ("sft_local_token_normalization",) @@ -68,10 +88,14 @@ "weights.pt", ) NON_FINITE_METRIC_VALUE = 1e30 +ORACLE_DEFAULT_MEAN_ABS_PCT_LIMIT = DEFAULT_MEAN_ABS_PCT_THRESHOLD +ROUTER_SCORE_MEAN_ABS_PCT_LIMIT = 2e-4 +FORWARD_EXPERT_LORA_TRACE_NOISE_RELATIVE_L2_LIMIT = 3e-4 +FORWARD_EXPERT_LORA_TRACE_NOISE_REASON = "forward_expert_lora_trace_noise" EXPERT_TABLE_ROW_LIMIT = 8 EXPERT_TRIPLET_PARAM_RE = re.compile( r"layers\.(?P\d+|__layer_avg__)\.mlp\.experts\.(?P\d+)\." - r"(?Pgate_proj|up_proj|down_proj)\." + r"(?Pgate_proj|up_proj|gate_up_proj|down_proj)\." ) LAYER_INDEX_RE = re.compile(r"layers\.(\d+)\.") PHASE_PRINT_ORDER = { @@ -85,6 +109,10 @@ } +def _format_elapsed(seconds: float) -> str: + return f"{seconds:.1f}s" + + def oracle_output_slug( objective: OracleObjective, topology: "Topology", @@ -112,8 +140,7 @@ def objective_supports_sensitivity_mutation( is_moe: bool = True, ) -> bool: return mutation in supported_sensitivity_mutations_for_objective( - objective, - is_moe=is_moe, + objective, is_moe=is_moe ) @@ -133,6 +160,17 @@ def selected_oracle_objectives() -> list[OracleObjective]: ) +def _resolve_test_flex_backend( + case_config: "OracleCaseConfig", + flex_backend: FlexBackend | None, +) -> FlexBackend | None: + if flex_backend is not None: + return flex_backend + if case_config.precision == "fp32": + return TEST_DEFAULT_FLEX_BACKEND + return None + + class Topology(BaseModel): """Defines distributed topology settings for one Megatron run variant.""" @@ -178,24 +216,35 @@ def world_size(self) -> int: TOPOLOGIES = [ Topology(tp=1, ep=1, etp=1, dp=1, sp=False), - Topology(tp=2, ep=2, etp=1, dp=1, sp=True), - Topology(tp=2, ep=1, etp=2, dp=1, sp=True), - Topology(tp=1, ep=2, etp=1, dp=2, sp=False), + Topology(tp=1, ep=2, etp=1, dp=1, cp=2, sp=False), + Topology(tp=2, ep=2, etp=1, dp=1, cp=2, sp=True), + Topology(tp=2, ep=4, etp=2, dp=2, cp=2, sp=True), ] DENSE_TOPOLOGIES = [ Topology(tp=1, ep=1, etp=1, dp=1, sp=False), Topology(tp=2, ep=1, etp=1, dp=1, sp=True), Topology(tp=1, ep=1, etp=1, dp=2, sp=False), Topology(tp=2, ep=1, etp=1, dp=2, sp=True), + Topology(tp=1, ep=1, etp=1, dp=1, cp=2, sp=False), + Topology(tp=2, ep=1, etp=1, dp=1, cp=2, sp=True), + Topology(tp=2, ep=1, etp=1, dp=2, cp=2, sp=True), ] ORACLE_TOPOLOGY = TOPOLOGIES[0] DENSE_ORACLE_TOPOLOGY = DENSE_TOPOLOGIES[0] SENSITIVITY_TOPOLOGY = Topology(tp=2, ep=2, etp=1, dp=1, sp=True) +CP_ATTENTION_SENSITIVITY_TOPOLOGY = Topology(tp=1, ep=2, etp=1, dp=1, cp=2, sp=False) DENSE_SENSITIVITY_TOPOLOGY = Topology(tp=2, ep=1, etp=1, dp=1, sp=True) DENSE_DP_SENSITIVITY_TOPOLOGY = Topology(tp=1, ep=1, etp=1, dp=2, sp=False) +DENSE_CP_ATTENTION_SENSITIVITY_TOPOLOGY = Topology( + tp=1, ep=1, etp=1, dp=1, cp=2, sp=False +) SENSITIVITY_TOPOLOGY_BY_MUTATION: dict[SensitivityMutation, Topology] = { mutation: SENSITIVITY_TOPOLOGY for mutation in SUPPORTED_SENSITIVITY_MUTATIONS } +SENSITIVITY_TOPOLOGY_BY_MUTATION |= { + mutation: CP_ATTENTION_SENSITIVITY_TOPOLOGY + for mutation in CP_ATTENTION_SENSITIVITY_MUTATIONS +} SENSITIVITY_TOPOLOGY_BY_MUTATION["bwd_skip_sync_fc1_a"] = Topology( tp=2, ep=1, etp=2, dp=1, sp=True ) @@ -209,23 +258,15 @@ def world_size(self) -> int: } -def oracle_topology(*, is_moe: bool = True) -> Topology: - return ORACLE_TOPOLOGY if is_moe else DENSE_ORACLE_TOPOLOGY - - -def selected_suite_topologies(*, is_moe: bool = True) -> list[Topology]: - return list(TOPOLOGIES if is_moe else DENSE_TOPOLOGIES) - - class PackedTensorConfig(BaseModel): """Controls synthetic packed tensor generation used by oracle harness runs.""" num_sequences: int = 4 - sequence_length: int = 256 - prefill_tokens: int = 64 + sequence_length: int = 1024 + prefill_tokens: int = 256 completion_branches_per_prefix: int = Field(default=2, ge=1) decode_tokens_jitter: int = Field(default=32, ge=0) - decode_tokens: int = 64 + decode_tokens: int = 128 packing_mode: Literal["stop_early", "truncate"] = "stop_early" vocab_high: int = 8192 @@ -286,7 +327,6 @@ class OracleCaseConfig(BaseModel): """Contains all deterministic run parameters for one oracle case.""" base_model: str - is_moe: bool = True precision: Literal["bf16", "fp32"] = "fp32" num_layers: int = 4 seed: int = 20260304 @@ -299,6 +339,15 @@ class OracleCaseConfig(BaseModel): lora: LoraConfig = Field(default_factory=LoraConfig) allow_unvalidated_arch: bool = False + @property + def is_moe(self) -> bool: + from art.megatron.model_support import model_uses_expert_parallel + + return model_uses_expert_parallel( + self.base_model, + allow_unvalidated_arch=self.allow_unvalidated_arch, + ) + class DiskPackedTensorsSpec(BaseModel): """Describes packed tensor artifacts persisted on disk for reuse.""" @@ -322,6 +371,7 @@ class CaseArtifacts(BaseModel): class WorkerRunRequest(BaseModel): """Defines one distributed worker invocation for generating variant artifacts.""" + git: GitRepoState case_id: str objective: OracleObjective case_config: OracleCaseConfig @@ -333,6 +383,12 @@ class WorkerRunRequest(BaseModel): moe_routing_replay_path: str | None = None moe_routing_replay_strict: bool = True capture_moe_routing_bundle_path: str | None = None + flex_backend: FlexBackend | None = None + offload_between_jobs: bool = True + streaming_weight_offload: StreamingWeightOffloadConfig = Field( + default_factory=StreamingWeightOffloadConfig + ) + use_fp32_lora_reference: bool = True class StepTrace(BaseModel): @@ -341,6 +397,9 @@ class StepTrace(BaseModel): step_index: int loss: float probs_corr: float + micro_sample_indices: list[int | None] = Field(default_factory=list) + micro_losses: list[float] = Field(default_factory=list) + debug_files: dict[str, str] = Field(default_factory=dict) output_file: str grads_file: str deltas_file: str @@ -350,6 +409,7 @@ class StepTrace(BaseModel): class RunManifest(BaseModel): """Records run metadata and per-step trace references for one topology output.""" + git: GitRepoState case_id: str objective: OracleObjective base_model: str @@ -359,6 +419,11 @@ class RunManifest(BaseModel): seed: int num_steps: int packed_tensors: DiskPackedTensorsSpec + offload_between_jobs: bool = True + streaming_weight_offload: StreamingWeightOffloadConfig = Field( + default_factory=StreamingWeightOffloadConfig + ) + use_fp32_lora_reference: bool = True steps: list[StepTrace] @@ -387,7 +452,7 @@ class VariantSpec(BaseModel): """Declares how to execute and evaluate one candidate variant against the oracle.""" name: str - objective: OracleObjective + objective: OracleObjective = "rl" topology: Topology pass_fn_by_phase: dict[str, PhasePassFn] = Field( default_factory=dict, @@ -399,6 +464,11 @@ class VariantSpec(BaseModel): mutation: SensitivityMutation | None = None expected_signal: Literal["pass", "fail"] = "pass" force_regenerate: bool = True + flex_backend: FlexBackend | None = None + offload_between_jobs: bool = True + streaming_weight_offload: StreamingWeightOffloadConfig = Field( + default_factory=StreamingWeightOffloadConfig + ) def resolved_output_slug(self) -> str: """Resolves the artifact slug for this run, including mutation suffix when present.""" @@ -416,6 +486,7 @@ def resolved_reference_slug(self) -> str: class VariantReport(BaseModel): """Captures full comparison output for one variant run.""" + git: GitRepoState case_id: str variant: str topology: str @@ -463,7 +534,7 @@ def layer_averaged_summary(reference_stack, candidate_stack) -> dict[str, float] ref = reference_stack.detach().float() cand = candidate_stack.detach().float() layer_count = int(ref.shape[0]) - metrics = { + averaged_metrics = { k: 0.0 for k in [ "numel", @@ -478,8 +549,14 @@ def layer_averaged_summary(reference_stack, candidate_stack) -> dict[str, float] layer_accumulator = DiffAccumulator() layer_accumulator.update(ref[layer_index], cand[layer_index]) layer_summary = layer_accumulator.as_summary() - metrics = {k: metrics[k] + layer_summary[k] for k in metrics.keys()} - return {k: _finite_metric(metrics[k] / layer_count) for k in metrics.keys()} + averaged_metrics = { + k: averaged_metrics[k] + layer_summary[k] + for k in averaged_metrics.keys() + } + return { + k: _finite_metric(averaged_metrics[k] / layer_count) + for k in averaged_metrics.keys() + } def update_router_ids(self, reference_ids, candidate_ids) -> None: # type: ignore[no-untyped-def] """Adds router top-k id mismatch counts into the accumulator.""" @@ -521,7 +598,6 @@ def as_summary(self) -> dict[str, float]: mean_abs = self.abs_sum / self.numel typical_abs = self.ref_abs_sum / self.numel candidate_abs = self.candidate_abs_sum / self.numel - mean_abs_pct = (mean_abs / (typical_abs + 1e-12)) * 100.0 return { "numel": _finite_metric(float(self.numel), default=0.0), "mean_abs_diff": _finite_metric(mean_abs), @@ -530,7 +606,9 @@ def as_summary(self) -> dict[str, float]: ), "typical_abs_scale": _finite_metric(typical_abs, default=0.0), "candidate_abs_scale": _finite_metric(candidate_abs, default=0.0), - "mean_abs_pct": _finite_metric(mean_abs_pct), + "mean_abs_pct": _finite_metric( + mean_abs_pct_from_sums(self.abs_sum, self.ref_abs_sum, self.numel) + ), "topk_mismatch_fraction": _finite_metric(topk_fraction, default=1.0), "top1_mismatch_fraction": _finite_metric(top1_fraction, default=1.0), } @@ -588,24 +666,12 @@ def selected_sensitivity_mutations_for_objective( mutations: list[SensitivityMutation], *, is_moe: bool = True, - max_world_size: int | None = None, ) -> list[SensitivityMutation]: return [ mutation for mutation in mutations - if objective_supports_sensitivity_mutation( - objective, - mutation, - is_moe=is_moe, - ) - and ( - max_world_size is None - or sensitivity_topology_for_mutation( - mutation, - is_moe=is_moe, - ).world_size() - <= max_world_size - ) + if mutation + in supported_sensitivity_mutations_for_objective(objective, is_moe=is_moe) ] @@ -622,6 +688,8 @@ def sensitivity_topology_for_mutation( "sft_local_token_normalization", }: return DENSE_DP_SENSITIVITY_TOPOLOGY + if mutation in CP_ATTENTION_SENSITIVITY_MUTATIONS: + return DENSE_CP_ATTENTION_SENSITIVITY_TOPOLOGY return DENSE_SENSITIVITY_TOPOLOGY return SENSITIVITY_TOPOLOGY_BY_MUTATION[mutation] @@ -632,8 +700,6 @@ def sensitivity_required_world_size( is_moe: bool = True, ) -> int: """Returns the max world-size required by a selected mutation set.""" - if not mutations: - return 0 return max( sensitivity_topology_for_mutation(mutation, is_moe=is_moe).world_size() for mutation in mutations @@ -650,11 +716,15 @@ def keep_topology_artifacts() -> bool: return _truthy(os.environ.get(KEEP_TOPOLOGY_ARTIFACTS_ENV)) -def case_config( - base_model: str = "Qwen/Qwen3-30B-A3B-Instruct-2507", -) -> OracleCaseConfig: +DEFAULT_ORACLE_BASE_MODEL = "Qwen/Qwen3.5-35B-A3B" + + +def case_config(base_model: str | None = None) -> OracleCaseConfig: """Builds the deterministic default oracle case config.""" - return OracleCaseConfig(base_model=base_model) + return OracleCaseConfig( + base_model=base_model + or os.environ.get(ORACLE_BASE_MODEL_ENV, DEFAULT_ORACLE_BASE_MODEL) + ) def available_gpu_count() -> int: @@ -664,6 +734,16 @@ def available_gpu_count() -> int: return int(torch.cuda.device_count()) +def oracle_topology(*, is_moe: bool = True) -> Topology: + """Returns the canonical single-rank oracle topology for a model family.""" + return ORACLE_TOPOLOGY if is_moe else DENSE_ORACLE_TOPOLOGY + + +def selected_suite_topologies(*, is_moe: bool = True) -> list[Topology]: + """Returns the correctness topology list for a model family.""" + return list(TOPOLOGIES if is_moe else DENSE_TOPOLOGIES) + + def stable_case_id(case_config: OracleCaseConfig) -> str: """Builds a deterministic case id from case config contents.""" payload = case_config.model_dump(mode="json") @@ -691,6 +771,24 @@ def _read_json(path: Path) -> dict[str, Any]: return json.load(handle) +def _current_git_state() -> GitRepoState: + return pinned_git_state(ORACLE_ARTIFACT_SUITE_NAME) + + +def _manifest_matches_current_commit(path: Path) -> bool: + if not path.exists(): + return False + try: + payload = _read_json(path) + except Exception: + return False + git_payload = payload.get("git") + return ( + isinstance(git_payload, dict) + and git_payload.get("commit") == _current_git_state().commit + ) + + def _build_packed_tensors( config: PackedTensorConfig, seed: int, @@ -867,7 +965,10 @@ def _sample_advantage_value() -> float: ) weights[sequence_index, cursor:completion_end] = 1.0 cursor = completion_end + if completion_take < completion_length: + break + # Ensure paired cross-DP rows are never token-identical across valid tokens. half = config.num_sequences // 2 if half > 0 and config.num_sequences % 2 == 0: valid_lengths = (group_ids != -1).sum(dim=1) @@ -954,7 +1055,12 @@ def _prune_topology_artifacts(path: Path) -> None: if keep_topology_artifacts() or not path.exists(): return for child in path.iterdir(): - if child.name in {"variant_report.json", "run_request.json", "worker.log"}: + if child.name in { + "manifest.json", + "variant_report.json", + "run_request.json", + "worker.log", + }: continue if child.is_dir(): shutil.rmtree(child) @@ -962,6 +1068,20 @@ def _prune_topology_artifacts(path: Path) -> None: child.unlink() +def _prune_case_artifacts(case_dir: Path) -> None: + """Drops reusable generated inputs after tests have written reports.""" + if keep_topology_artifacts() or not case_dir.exists(): + return + for name in ("packed_tensors", "packed_tensors.json", "shared_init"): + path = case_dir / name + if not path.exists(): + continue + if path.is_dir(): + shutil.rmtree(path) + else: + path.unlink() + + def _load_manifest(topology_dir: Path) -> RunManifest: """Loads one run manifest for a topology output directory.""" manifest_path = topology_dir / "manifest.json" @@ -1038,6 +1158,13 @@ def _expert_agnostic_param_key(param: str) -> str: return f"{param[:start]}__expert_avg__{param[end:]}" +def _is_forward_expert_lora_trace(param: str) -> bool: + """Returns whether one forward-trace row is an expert LoRA internal.""" + return ".mlp.experts." in param and ( + ".lora." in param or ".gate_lora." in param or ".up_lora." in param + ) + + def _stacked_layers( pairs: list[tuple[str, Any, Any]], ) -> list[tuple[str, Any, Any]]: @@ -1083,25 +1210,195 @@ class VariantRunner: def __init__( self, *, - objective: OracleObjective, + objective: OracleObjective = "rl", case_config: OracleCaseConfig, + oracle_flex_backend: FlexBackend | None = None, + variant_flex_backend: FlexBackend | None = None, + oracle_topology_override: Topology | None = None, + oracle_slug_override: str | None = None, + oracle_offload_between_jobs: bool = True, + oracle_streaming_weight_offload: StreamingWeightOffloadConfig | None = None, + use_fp32_lora_reference: bool = True, console: Console | None = None, ) -> None: self.objective = objective self.case_config = case_config + self.git = _current_git_state() self.case_artifacts = ensure_case_artifacts(case_config) self.case_id = self.case_artifacts.case_id self.case_dir = Path(self.case_artifacts.case_dir) - self.oracle_topology = oracle_topology(is_moe=case_config.is_moe) - self.oracle_slug = oracle_output_slug(objective, self.oracle_topology) + self.oracle_topology = oracle_topology_override or oracle_topology( + is_moe=case_config.is_moe + ) + self.oracle_slug = oracle_slug_override or oracle_output_slug( + objective, self.oracle_topology + ) self.oracle_dir = self.case_dir / self.oracle_slug self.oracle_routing_bundle_dir = ( self.case_dir / f"{objective}__{ORACLE_MOE_ROUTING_BUNDLE_DIRNAME}" ) + self.oracle_offload_between_jobs = oracle_offload_between_jobs + self.oracle_streaming_weight_offload = ( + oracle_streaming_weight_offload or StreamingWeightOffloadConfig() + ) + self.use_fp32_lora_reference = use_fp32_lora_reference self.shared_init_path = Path(self.case_artifacts.shared_init_adapter_path) + self.oracle_flex_backend = _resolve_test_flex_backend( + case_config, oracle_flex_backend + ) + self.variant_flex_backend = _resolve_test_flex_backend( + case_config, variant_flex_backend + ) self.console = console or Console(width=140) self._oracle_initialized = False self._oracle_regenerated = False + self._sample_valid_lengths_cache: tuple[int, ...] | None = None + + def _sample_valid_lengths(self) -> tuple[int, ...]: + if self._sample_valid_lengths_cache is not None: + return self._sample_valid_lengths_cache + from art.megatron.context_parallel.builder import ( + build_shared_prefix_attention_spec, + ) + from art.preprocessing.pack import packed_tensors_from_dir + + packed_tensors = packed_tensors_from_dir( + **self.case_artifacts.packed_tensors.model_dump(exclude_none=True) + ) + group_ids = packed_tensors["group_ids"] + parent_ids = packed_tensors["parent_ids"] + self._sample_valid_lengths_cache = tuple( + int( + build_shared_prefix_attention_spec( + group_ids=group_ids[row_index : row_index + 1], + parent_ids=parent_ids[row_index : row_index + 1], + ) + .rows[0] + .valid_tokens + ) + for row_index in range(int(group_ids.shape[0])) + ) + return self._sample_valid_lengths_cache + + def _step_micro_sample_indices(self, step: StepTrace) -> list[int | None]: + base_sample_index = ( + step.step_index * self.case_config.grad_accumulation_sequences + ) + expected = [ + sample_index + if sample_index < self.case_artifacts.packed_tensors.num_sequences + else None + for sample_index in range( + base_sample_index, + base_sample_index + self.case_config.grad_accumulation_sequences, + ) + ] + if step.micro_sample_indices and len(step.micro_sample_indices) == len( + expected + ): + return list(step.micro_sample_indices) + return expected + + def _load_output_tensor_map( + self, + topology_dir: Path, + step: StepTrace, + ) -> dict[str, torch.Tensor]: + tensor = _load_output_tensor(topology_dir, step) + if isinstance(tensor, list): + outputs = tensor + elif isinstance(tensor, torch.Tensor) and tensor.ndim >= 1: + outputs = [tensor[index] for index in range(int(tensor.shape[0]))] + else: + return {"logprobs": tensor} + + sample_indices = self._step_micro_sample_indices(step) + valid_lengths = self._sample_valid_lengths() + output_map: dict[str, torch.Tensor] = {} + for output_index, output in enumerate(outputs): + key = f"logprobs.micro_{output_index:03d}" + if not isinstance(output, torch.Tensor): + output_map[key] = output + continue + if output_index < len(sample_indices): + sample_index = sample_indices[output_index] + if isinstance(sample_index, int): + valid_length = int(valid_lengths[sample_index]) + target_length = max(valid_length - 1, 0) + if output.ndim > 0 and int(output.shape[-1]) > target_length: + output = output[..., :target_length].contiguous() + output_map[key] = output + return output_map + + @staticmethod + def _load_loss_tensor_map(step: StepTrace) -> dict[str, torch.Tensor]: + return {"loss": torch.tensor([step.loss], dtype=torch.float32)} + + def _trim_trace_padding( + self, + trace: dict[str, list[dict[str, Any]]], + ) -> dict[str, list[dict[str, Any]]]: + valid_lengths = self._sample_valid_lengths() + sequence_length = int(self.case_config.packed_tensors.sequence_length) + if sequence_length <= 0: + return trace + + for calls in trace.values(): + for call in calls: + sample_index = call.get("micro_sample_index") + if not isinstance(sample_index, int): + continue + valid_length = int(valid_lengths[sample_index]) + if valid_length >= sequence_length: + continue + row_token_uids = call.get("row_token_uids") + if ( + isinstance(row_token_uids, torch.Tensor) + and row_token_uids.ndim == 1 + ): + local_token_uids = torch.remainder(row_token_uids, sequence_length) + keep_rows = torch.nonzero( + (row_token_uids >= 0) & (local_token_uids < valid_length), + as_tuple=False, + ).reshape(-1) + if int(keep_rows.numel()) > 0 and int(keep_rows.numel()) < int( + row_token_uids.numel() + ): + call["row_token_uids"] = row_token_uids.index_select( + 0, keep_rows + ).contiguous() + for key in ( + "primary_output", + "router_topk_scores", + "router_topk_ids", + ): + tensor = call.get(key) + if ( + isinstance(tensor, torch.Tensor) + and tensor.ndim > 0 + and int(tensor.shape[0]) == int(row_token_uids.numel()) + ): + call[key] = tensor.index_select( + 0, keep_rows + ).contiguous() + continue + for key in ("primary_output", "router_topk_scores", "router_topk_ids"): + tensor = call.get(key) + if not isinstance(tensor, torch.Tensor) or tensor.ndim == 0: + continue + leading_dim = int(tensor.shape[0]) + if leading_dim <= valid_length: + continue + if leading_dim % sequence_length == 0: + row_multiplier = leading_dim // sequence_length + target_rows = valid_length * row_multiplier + elif leading_dim <= sequence_length: + target_rows = valid_length + else: + continue + if 0 < target_rows < leading_dim: + call[key] = tensor[:target_rows].contiguous() + return trace def _run_topology( self, @@ -1112,15 +1409,23 @@ def _run_topology( replay_bundle_dir: Path | None, capture_bundle_dir: Path | None, regenerate: bool, + flex_backend: FlexBackend | None = None, + offload_between_jobs: bool = True, + streaming_weight_offload: StreamingWeightOffloadConfig | None = None, ) -> Path: """Executes one topology worker run and returns its output directory.""" topology_dir = self.case_dir / output_slug manifest_path = topology_dir / "manifest.json" - if manifest_path.exists() and not regenerate: + if ( + manifest_path.exists() + and not regenerate + and _manifest_matches_current_commit(manifest_path) + ): return topology_dir _replace_topology_dir(topology_dir) run_case_config = self.case_config request = WorkerRunRequest( + git=self.git, case_id=self.case_id, objective=self.objective, case_config=run_case_config, @@ -1136,6 +1441,12 @@ def _run_topology( capture_moe_routing_bundle_path=( None if capture_bundle_dir is None else str(capture_bundle_dir) ), + flex_backend=flex_backend, + offload_between_jobs=offload_between_jobs, + streaming_weight_offload=( + streaming_weight_offload or StreamingWeightOffloadConfig() + ), + use_fp32_lora_reference=self.use_fp32_lora_reference, ) from .oracle_worker import run_worker_subprocess @@ -1151,6 +1462,9 @@ def ensure_oracle(self) -> Path: self.shared_init_path.unlink() bundle_manifest = self.oracle_routing_bundle_dir / "manifest.json" oracle_manifest = self.oracle_dir / "manifest.json" + capture_manifest = ( + self.case_dir / f"{self.oracle_slug}__oracle_capture" / "manifest.json" + ) bundle_format_current = False if bundle_manifest.exists(): try: @@ -1165,11 +1479,15 @@ def ensure_oracle(self) -> Path: or not bundle_manifest.exists() or not bundle_format_current or not self.shared_init_path.exists() + or not _manifest_matches_current_commit(capture_manifest) ) run_oracle_topology = partial( self._run_topology, topology=self.oracle_topology, mutation=None, + flex_backend=self.oracle_flex_backend, + offload_between_jobs=self.oracle_offload_between_jobs, + streaming_weight_offload=self.oracle_streaming_weight_offload, regenerate=True, ) if self.case_config.is_moe and need_capture: @@ -1182,6 +1500,7 @@ def ensure_oracle(self) -> Path: regenerate or not oracle_manifest.exists() or not self.shared_init_path.exists() + or not _manifest_matches_current_commit(oracle_manifest) ): run_oracle_topology( output_slug=self.oracle_slug, @@ -1207,6 +1526,9 @@ def ensure_variant_artifacts( topology=variant.topology, output_slug=output_slug, mutation=variant.mutation, + flex_backend=variant.flex_backend or self.variant_flex_backend, + offload_between_jobs=variant.offload_between_jobs, + streaming_weight_offload=variant.streaming_weight_offload, replay_bundle_dir=( self.oracle_routing_bundle_dir if self.case_config.is_moe else None ), @@ -1333,7 +1655,8 @@ def _build_metric_rows_from_tensor_pairs( summary = accumulator.as_summary() elif layer_averaged: summary = DiffAccumulator.layer_averaged_summary( - reference_aligned, aligned_candidate + reference_aligned, + aligned_candidate, ) else: accumulator = DiffAccumulator() @@ -1439,6 +1762,57 @@ def _build_step_summaries(rows: list[MetricRow]) -> dict[int, dict[str, Any]]: phase_entry[row.param] = row.model_dump(mode="json") return step_summaries + @staticmethod + def _step_phase_rows( + rows: list[MetricRow], step_index: int, phase: str + ) -> list[MetricRow]: + return [ + row for row in rows if row.step_index == step_index and row.phase == phase + ] + + @classmethod + def _phase_rows_pass( + cls, rows: list[MetricRow], step_index: int, phase: str + ) -> bool: + phase_rows = cls._step_phase_rows(rows, step_index, phase) + return bool(phase_rows) and all(row.pass_signal for row in phase_rows) + + @classmethod + def _router_topk_exact(cls, rows: list[MetricRow], step_index: int) -> bool: + topk_rows = cls._step_phase_rows(rows, step_index, "router_topk_ids") + return bool(topk_rows) and all( + row.pass_signal + and row.topk_mismatch_fraction == 0.0 + and row.top1_mismatch_fraction == 0.0 + for row in topk_rows + ) + + @classmethod + def _apply_forward_expert_lora_trace_noise_passes( + cls, rows: list[MetricRow] + ) -> None: + """Reclassifies proven near-null expert LoRA forward trace noise only.""" + steps = {row.step_index for row in rows} + gate_by_step = { + step: ( + cls._phase_rows_pass(rows, step, "outputs") + and cls._phase_rows_pass(rows, step, "router_scores") + and cls._router_topk_exact(rows, step) + ) + for step in steps + } + for row in rows: + if row.pass_signal: + continue + if row.phase != "forward" or not _is_forward_expert_lora_trace(row.param): + continue + if not gate_by_step.get(row.step_index, False): + continue + if row.relative_l2 > FORWARD_EXPERT_LORA_TRACE_NOISE_RELATIVE_L2_LIMIT: + continue + row.pass_signal = True + row.failure_reasons = [FORWARD_EXPERT_LORA_TRACE_NOISE_REASON] + def compare_variant(self, variant: VariantSpec) -> VariantReport: """Compares one candidate variant against its reference topology.""" reference_slug = variant.resolved_reference_slug() @@ -1497,19 +1871,23 @@ def compare_variant(self, variant: VariantSpec) -> VariantReport: reference_manifest.steps, topology_manifest.steps ): step_index = reference_step.step_index - reference_trace = _load_forward_trace(reference_dir, step_index) - topology_trace = _load_forward_trace(topology_dir, step_index) + reference_trace = self._trim_trace_padding( + _load_forward_trace(reference_dir, step_index) + ) + topology_trace = self._trim_trace_padding( + _load_forward_trace(topology_dir, step_index) + ) map_phase_inputs = [ ( "outputs", - {"logprobs": _load_output_tensor(reference_dir, reference_step)}, - {"logprobs": _load_output_tensor(topology_dir, topology_step)}, + self._load_output_tensor_map(reference_dir, reference_step), + self._load_output_tensor_map(topology_dir, topology_step), False, ), ( "losses", - {"loss": torch.tensor([reference_step.loss], dtype=torch.float32)}, - {"loss": torch.tensor([topology_step.loss], dtype=torch.float32)}, + self._load_loss_tensor_map(reference_step), + self._load_loss_tensor_map(topology_step), False, ), ( @@ -1555,10 +1933,12 @@ def compare_variant(self, variant: VariantSpec) -> VariantReport: router_ids=router_ids, ) ) + self._apply_forward_expert_lora_trace_noise_passes(rows) pass_count = sum(1 for row in rows if row.pass_signal) fail_count = len(rows) - pass_count signal: Literal["pass", "fail"] = "pass" if fail_count == 0 else "fail" return VariantReport( + git=self.git, case_id=self.case_id, variant=variant.name, topology=topology_slug, @@ -1687,6 +2067,7 @@ def run_suite( ) finally: self._prune_reference_artifacts() + _prune_case_artifacts(self.case_dir) return reports @@ -1696,18 +2077,22 @@ def _default_phase_pass_fns() -> dict[str, PhasePassFn]: # we also average across experts to reduce noise # we don't expect particular layers to see errors as opposed to the others so this is helpful non_zero_scales = {"typical_abs_scale": 0.0, "candidate_abs_scale": 0.0} - fwd_out = MetricThresholdRule( - limits={"relative_l2": 1e-2, "mean_abs_pct": 1.0}, - minimums=non_zero_scales, + fwd_out_loss = MetricThresholdRule( + limits={"mean_abs_pct": ORACLE_DEFAULT_MEAN_ABS_PCT_LIMIT} ) - loss = MetricThresholdRule( - limits={"relative_l2": 2e-2, "mean_abs_pct": 2.0}, + fwd_out = MetricThresholdRule( + limits={"mean_abs_pct": ORACLE_DEFAULT_MEAN_ABS_PCT_LIMIT}, minimums=non_zero_scales, ) grads_deltas = MetricThresholdRule( - limits={"mean_abs_pct": 3.0}, + limits={"mean_abs_pct": ORACLE_DEFAULT_MEAN_ABS_PCT_LIMIT}, minimums=non_zero_scales, ) + router_scores_rule = MetricThresholdRule( + # Production RouterReplay replays top-k ids and gathers probabilities from + # live candidate scores, so scores are close but not bit-exact. + limits={"mean_abs_pct": ROUTER_SCORE_MEAN_ABS_PCT_LIMIT} + ) router_topk_rule = ( MetricThresholdRule( # should be no mismatch due to router replay limits={ @@ -1716,13 +2101,10 @@ def _default_phase_pass_fns() -> dict[str, PhasePassFn]: } ) ) - return { - "forward": fwd_out, - "outputs": fwd_out, - "losses": loss, - } | { + return {"forward": fwd_out, "outputs": fwd_out, "losses": fwd_out_loss} | { "grads": grads_deltas, "deltas": grads_deltas, + "router_scores": router_scores_rule, "router_topk_ids": router_topk_rule, } @@ -1730,8 +2112,9 @@ def _default_phase_pass_fns() -> dict[str, PhasePassFn]: def _suite_variants( objective: OracleObjective, *, - is_moe: bool, + is_moe: bool = True, max_world_size: int | None = None, + variant_flex_backend: FlexBackend | None = None, ) -> list[VariantSpec]: """Builds the standard oracle suite variant ordering.""" phase_pass = _default_phase_pass_fns() @@ -1745,6 +2128,7 @@ def _suite_variants( objective=objective, topology=topology, pass_fn_by_phase=phase_pass, + flex_backend=variant_flex_backend, ) ) return variants @@ -1754,17 +2138,25 @@ def run_suite( *, case_config: OracleCaseConfig, max_world_size: int | None = None, + oracle_flex_backend: FlexBackend | None = None, + variant_flex_backend: FlexBackend | None = None, ) -> list[VariantReport]: """Runs non-oracle topologies against the canonical replay-backed oracle.""" reports: list[VariantReport] = [] for objective in selected_oracle_objectives(): - runner = VariantRunner(objective=objective, case_config=case_config) + runner = VariantRunner( + objective=objective, + case_config=case_config, + oracle_flex_backend=oracle_flex_backend, + variant_flex_backend=variant_flex_backend, + ) reports.extend( runner.run_suite( _suite_variants( objective, is_moe=case_config.is_moe, max_world_size=max_world_size, + variant_flex_backend=variant_flex_backend, ) ) ) @@ -1776,57 +2168,58 @@ def run_sensitivity_suite( case_config: OracleCaseConfig, mutations: list[SensitivityMutation], max_world_size: int | None = None, + oracle_flex_backend: FlexBackend | None = None, + variant_flex_backend: FlexBackend | None = None, ) -> list[VariantReport]: """Runs a list of sensitivity mutations and expects each to fail.""" phase_pass = _default_phase_pass_fns() reports: list[VariantReport] = [] ran_any_variants = False - matched_any_objective = False for objective in selected_oracle_objectives(): - runner = VariantRunner(objective=objective, case_config=case_config) - objective_supported_mutations = selected_sensitivity_mutations_for_objective( - objective, - mutations, - is_moe=case_config.is_moe, - ) - matched_any_objective = matched_any_objective or bool( - objective_supported_mutations + runner = VariantRunner( + objective=objective, + case_config=case_config, + oracle_flex_backend=oracle_flex_backend, + variant_flex_backend=variant_flex_backend, ) objective_mutations = selected_sensitivity_mutations_for_objective( objective, mutations, is_moe=case_config.is_moe, - max_world_size=max_world_size, ) if not objective_mutations: continue - variants = [ - VariantSpec( - name=f"{objective}_sensitivity_{mutation}", - objective=objective, - topology=sensitivity_topology_for_mutation( - mutation, - is_moe=case_config.is_moe, - ), - mutation=mutation, - expected_signal="fail", - pass_fn_by_phase=phase_pass, + variants = [] + for mutation in objective_mutations: + topology = sensitivity_topology_for_mutation( + mutation, + is_moe=case_config.is_moe, ) - for mutation in objective_mutations - ] + if max_world_size is not None and topology.world_size() > max_world_size: + continue + variants.append( + VariantSpec( + name=f"{objective}_sensitivity_{mutation}", + objective=objective, + topology=topology, + mutation=mutation, + expected_signal="fail", + pass_fn_by_phase=phase_pass, + flex_backend=variant_flex_backend, + ) + ) + if not variants: + continue ran_any_variants = True reports.extend(runner.run_suite(variants)) - if ran_any_variants or (max_world_size is not None and matched_any_objective): + if ran_any_variants: return reports requested = ", ".join(mutations) - supported_by_objective = [] - for objective in selected_oracle_objectives(): - objective_supported = supported_sensitivity_mutations_for_objective( - objective, - is_moe=case_config.is_moe, - ) - supported_by_objective.append(f"{objective}: {', '.join(objective_supported)}") - supported = ", ".join(supported_by_objective) + supported = ", ".join( + f"{objective}: " + f"{', '.join(supported_sensitivity_mutations_for_objective(objective, is_moe=case_config.is_moe))}" + for objective in selected_oracle_objectives() + ) raise ValueError( "No sensitivity variants matched the selected objectives. " f"Requested mutations: {requested}. Supported by objective: {supported}." diff --git a/tests/integration/megatron/model_support/oracle_worker.py b/tests/integration/megatron/model_support/oracle_worker.py index ee4a1471f..86ad2fb0e 100644 --- a/tests/integration/megatron/model_support/oracle_worker.py +++ b/tests/integration/megatron/model_support/oracle_worker.py @@ -11,7 +11,7 @@ import sys import time from types import MethodType -from typing import Any, Callable +from typing import Any, Callable, cast import numpy as np import torch @@ -24,6 +24,8 @@ from ..routing_replay.bundle import build_bundle_from_forward_trace_dir from ..routing_replay.trace import install_moe_routing_trace_hooks from .forward_trace import ForwardTraceCapture +from .gdn_fp32_reference import install_megatron_qwen35_gdn_fp32_reference +from .gdn_trace_uids import install_gdn_trace_token_uid_hooks from .oracle_harness import ( SUPPORTED_SENSITIVITY_MUTATIONS, OracleCaseConfig, @@ -40,10 +42,12 @@ _TOPOLOGY_ENV_VARS = { "tp": "ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE", + "cp": "ART_MEGATRON_CONTEXT_PARALLEL_SIZE", "ep": "ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE", "etp": "ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE", } _ORACLE_DEBUG_ENV = "ART_ORACLE_DEBUG" +_ATTACH_TOKEN_UIDS_ENV = "ART_MEGATRON_ATTACH_TOKEN_UIDS" _ORACLE_DEBUG_START_TIME = time.perf_counter() @@ -110,8 +114,13 @@ def run_worker_subprocess( f"\n=== {request.objective} {request.topology.slug()} ===\n" ) live_log.flush() - env = {**os.environ, "PYTHONUNBUFFERED": "1"} - env["ART_DISABLE_MEGATRON_COMPILE"] = "1" + env = { + **os.environ, + "ART_MEGATRON_ATTACH_TOKEN_UIDS": "1", + "PYTHONUNBUFFERED": "1", + } + if request.case_config.precision == "fp32": + env["NVIDIA_TF32_OVERRIDE"] = "0" run = subprocess.Popen( command, cwd=str(worker_cwd), @@ -156,6 +165,7 @@ def _set_deterministic_seed(seed: int) -> None: def provider_topology_env_vars(topology: Topology) -> dict[str, str]: return { _TOPOLOGY_ENV_VARS["tp"]: str(topology.tp), + _TOPOLOGY_ENV_VARS["cp"]: str(topology.cp), _TOPOLOGY_ENV_VARS["ep"]: str(topology.ep), _TOPOLOGY_ENV_VARS["etp"]: str(topology.etp), } @@ -177,7 +187,7 @@ def provider_topology_env(topology: Topology): def _merge_sharded_dicts(shards_by_rank: list[dict[str, Any]]) -> dict[str, Any]: """Merges rank-sharded LoRA tensors into a full state dict on rank 0.""" - from art.megatron.weights.merge import merge_sharded_adapter_entries + from art.megatron.weights.lora_publish import merge_sharded_adapter_entries entries_by_key: dict[str, list[tuple[dict[str, Any], torch.Tensor]]] = {} for rank_entry in shards_by_rank: @@ -238,7 +248,7 @@ def _collect_lora_state( raise RuntimeError( f"Duplicate LoRA key while collecting state: {key}" ) - local_state[key] = value.detach().cpu() + local_state[key] = value.detach().cpu().contiguous() return _gather_full_state(local_state, local_manifest) @@ -266,7 +276,7 @@ def _collect_lora_grads( raise RuntimeError( f"Duplicate LoRA grad key while collecting grads: {key}" ) - local_grads[key] = value.detach().cpu() + local_grads[key] = value.detach().cpu().contiguous() return _gather_full_state(local_grads, local_manifest) @@ -349,6 +359,31 @@ def _build_deterministic_shared_init( return initialized +def _stack_output_tensors(outputs: list[torch.Tensor]) -> torch.Tensor: + """Stacks micro outputs, padding the trailing sequence axis when lengths differ.""" + if not outputs: + raise RuntimeError("Expected at least one output tensor to stack") + first = outputs[0] + if all(tensor.shape == first.shape for tensor in outputs[1:]): + return torch.stack(outputs, dim=0) + if any(tensor.ndim != first.ndim for tensor in outputs[1:]) or any( + tensor.shape[:-1] != first.shape[:-1] for tensor in outputs[1:] + ): + raise RuntimeError("Unable to stack output tensors with incompatible shapes") + + max_last_dim = max(int(tensor.shape[-1]) for tensor in outputs) + padded_outputs: list[torch.Tensor] = [] + for tensor in outputs: + if int(tensor.shape[-1]) == max_last_dim: + padded_outputs.append(tensor) + continue + pad_value = float("nan") if tensor.dtype.is_floating_point else 0 + padded = tensor.new_full((*tensor.shape[:-1], max_last_dim), pad_value) + padded[..., : tensor.shape[-1]] = tensor + padded_outputs.append(padded) + return torch.stack(padded_outputs, dim=0) + + def _configure_provider( provider: Any, topology: Topology, @@ -381,17 +416,18 @@ def _patch_finalize_provider_bundle_for_oracle( def _oracle_finalize_provider_bundle(provider_bundle: Any) -> Any: provider = provider_bundle.provider + from art.megatron.provider import _finalize_provider_with_art_overrides + if case_config.precision == "fp32": - if case_config.is_moe: - provider.moe_token_dispatcher_type = "alltoall" - provider.moe_flex_dispatcher_backend = None - provider.moe_shared_expert_overlap = True - provider.overlap_moe_expert_parallel_comm = False + provider.moe_token_dispatcher_type = "alltoall" + provider.moe_flex_dispatcher_backend = None + provider.moe_enable_deepep = False + provider.moe_shared_expert_overlap = True + provider.overlap_moe_expert_parallel_comm = False provider.delay_wgrad_compute = False provider.ep_overlap_early_attn_memory_release = False - provider.finalize() - return provider_bundle - return original_finalize_provider_bundle(provider_bundle) + _finalize_provider_with_art_overrides(provider) + return provider_bundle megatron_train_module.finalize_provider_bundle = _oracle_finalize_provider_bundle try: @@ -422,7 +458,6 @@ def _build_optimizer_config(case_config: OracleCaseConfig): weight_decay=0.0, adam_eps=1e-13, ) - return OptimizerConfig( bf16=True, fp16=False, @@ -443,12 +478,210 @@ def _configure_cuda_precision(case_config: OracleCaseConfig) -> None: torch.set_float32_matmul_precision("highest") +@contextmanager +def _apply_requested_flex_backend_patch(flex_backend: str | None): + if flex_backend is None: + yield + return + + import art.megatron.flex_attn.compiled as compiled_flex_attention + + original_dense = compiled_flex_attention.dense_compiled_flex_attention + original_sparse = compiled_flex_attention.sparse_compiled_flex_attention + original_backend = compiled_flex_attention._FORCED_FLEX_BACKEND + original_kernel_options = compiled_flex_attention._FORCED_FLEX_KERNEL_OPTIONS + if flex_backend == "FLASH": + patched_backend = "FLASH" + patched_kernel_options = cast(Any, {"BACKEND": "FLASH"}) + elif flex_backend == "TRITON": + patched_backend = "TRITON" + patched_kernel_options = cast(Any, {"BACKEND": "TRITON"}) + elif flex_backend in { + "TRITON_LEGACY", + "TRITON_LEGACY_INNER_FP32", + "TRITON_LEGACY_FULL_FP32", + }: + patched_backend = "TRITON" + patched_kernel_options = cast(Any, {"FORCE_USE_FLEX_ATTENTION": True}) + else: + raise RuntimeError(f"Unsupported flex backend request: {flex_backend}") + + compiled_flex_attention._FORCED_FLEX_BACKEND = patched_backend # type: ignore[invalid-assignment] + compiled_flex_attention._FORCED_FLEX_KERNEL_OPTIONS = patched_kernel_options + compiled_flex_attention.dense_compiled_flex_attention = torch.compile( + compiled_flex_attention._forced_flex_attention_dense + ) + compiled_flex_attention.sparse_compiled_flex_attention = torch.compile( + compiled_flex_attention._forced_flex_attention_sparse + ) + try: + yield + finally: + compiled_flex_attention._FORCED_FLEX_BACKEND = original_backend + compiled_flex_attention._FORCED_FLEX_KERNEL_OPTIONS = original_kernel_options + compiled_flex_attention.dense_compiled_flex_attention = original_dense + compiled_flex_attention.sparse_compiled_flex_attention = original_sparse + + +@contextmanager +def _apply_test_flex_inner_fp32_patch(flex_backend: str | None): + if flex_backend != "TRITON_LEGACY_INNER_FP32": + yield + return + + from torch.nn.attention.flex_attention import AuxRequest, flex_attention + + import art.megatron.flex_attn.compiled as compiled_flex_attention + + original_dense = compiled_flex_attention.dense_compiled_flex_attention + original_sparse = compiled_flex_attention.sparse_compiled_flex_attention + legacy_kernel_options = cast(Any, {"FORCE_USE_FLEX_ATTENTION": True}) + + def _fp32_inner_call( + q, + k, + v, + *, + block_mask, + scale, + enable_gqa, + return_aux: AuxRequest | None = None, + ): + out = flex_attention( + q.float(), + k.float(), + v.float(), + block_mask=block_mask, + scale=scale, + enable_gqa=enable_gqa, + kernel_options=legacy_kernel_options, + return_aux=return_aux, + ) + if return_aux is None: + assert torch.is_tensor(out) + return out.to(dtype=q.dtype) + attn_out, aux = out + return attn_out.to(dtype=q.dtype), aux + + compiled_flex_attention.dense_compiled_flex_attention = torch.compile( + _fp32_inner_call + ) + compiled_flex_attention.sparse_compiled_flex_attention = torch.compile( + _fp32_inner_call + ) + try: + yield + finally: + compiled_flex_attention.dense_compiled_flex_attention = original_dense + compiled_flex_attention.sparse_compiled_flex_attention = original_sparse + + +@contextmanager +def _apply_test_attention_full_fp32_patch(flex_backend: str | None): + if flex_backend != "TRITON_LEGACY_FULL_FP32": + yield + return + + from megatron.core.tensor_parallel.layers import ( + ColumnParallelLinear, + RowParallelLinear, + ) + from megatron.core.transformer.attention import Attention + from torch.nn.attention.flex_attention import AuxRequest, flex_attention + + import art.megatron.flex_attn.compiled as compiled_flex_attention + + original_dense = compiled_flex_attention.dense_compiled_flex_attention + original_sparse = compiled_flex_attention.sparse_compiled_flex_attention + original_column_forward_impl = ColumnParallelLinear._forward_impl + original_row_forward_impl = RowParallelLinear._forward_impl + original_attention_forward = Attention.forward + legacy_kernel_options = cast(Any, {"FORCE_USE_FLEX_ATTENTION": True}) + + def _fp32_inner_call( + q, + k, + v, + *, + block_mask, + scale, + enable_gqa, + return_aux: AuxRequest | None = None, + ): + out = flex_attention( + q.float(), + k.float(), + v.float(), + block_mask=block_mask, + scale=scale, + enable_gqa=enable_gqa, + kernel_options=legacy_kernel_options, + return_aux=return_aux, + ) + if return_aux is None: + return out + return out + + def _column_forward_impl_fp32(self, input, weight, *args, **kwargs): + fp32_kwargs = dict(kwargs) + if fp32_kwargs.get("bias") is not None: + fp32_kwargs["bias"] = fp32_kwargs["bias"].float() + return original_column_forward_impl( + self, input.float(), weight.float(), *args, **fp32_kwargs + ) + + def _row_forward_impl_fp32(self, input, weight, *args, **kwargs): + fp32_kwargs = dict(kwargs) + if fp32_kwargs.get("bias") is not None: + fp32_kwargs["bias"] = fp32_kwargs["bias"].float() + return original_row_forward_impl( + self, input.float(), weight.float(), *args, **fp32_kwargs + ) + + def _attention_forward_fp32(self, hidden_states, *args, **kwargs): + output, bias = original_attention_forward(self, hidden_states, *args, **kwargs) + target_dtype = hidden_states.dtype + if torch.is_tensor(output): + output = output.to(dtype=target_dtype) + if torch.is_tensor(bias): + bias = bias.to(dtype=target_dtype) + return output, bias + + compiled_flex_attention.dense_compiled_flex_attention = torch.compile( + _fp32_inner_call + ) + compiled_flex_attention.sparse_compiled_flex_attention = torch.compile( + _fp32_inner_call + ) + setattr(ColumnParallelLinear, "_forward_impl", _column_forward_impl_fp32) + setattr(RowParallelLinear, "_forward_impl", _row_forward_impl_fp32) + setattr(Attention, "forward", _attention_forward_fp32) + try: + yield + finally: + compiled_flex_attention.dense_compiled_flex_attention = original_dense + compiled_flex_attention.sparse_compiled_flex_attention = original_sparse + setattr(ColumnParallelLinear, "_forward_impl", original_column_forward_impl) + setattr(RowParallelLinear, "_forward_impl", original_row_forward_impl) + setattr(Attention, "forward", original_attention_forward) + + def _assert_runtime_configuration( model_chunks: list[Any], case_config: OracleCaseConfig, + topology: Topology, ) -> None: - """Validates runtime model depth equals requested oracle case config.""" + """Validates runtime model depth/topology equals requested oracle config.""" observed_num_layers: set[int] = set() + observed_context_parallel_sizes: set[int] = set() + gdn_layers = 0 + standard_attention_layers = 0 + + try: + from megatron.core.ssm.gated_delta_net import GatedDeltaNet + except ImportError: # pragma: no cover - optional dependency guard. + GatedDeltaNet = () # type: ignore[assignment] + from megatron.core.transformer.attention import SelfAttention for chunk in model_chunks: module: Any = chunk @@ -457,12 +690,33 @@ def _assert_runtime_configuration( config = getattr(module, "config", None) if config is not None and hasattr(config, "num_layers"): observed_num_layers.add(int(config.num_layers)) + if config is not None and hasattr(config, "context_parallel_size"): + observed_context_parallel_sizes.add(int(config.context_parallel_size)) + for child in module.modules(): + if GatedDeltaNet and isinstance(child, GatedDeltaNet): + gdn_layers += 1 + if isinstance(child, SelfAttention): + standard_attention_layers += 1 if observed_num_layers != {case_config.num_layers}: raise RuntimeError( "Runtime num_layers mismatch: " f"requested={case_config.num_layers}, observed={sorted(observed_num_layers)}" ) + if observed_context_parallel_sizes != {topology.cp}: + raise RuntimeError( + "Runtime context_parallel_size mismatch: " + f"requested={topology.cp}, observed={sorted(observed_context_parallel_sizes)}" + ) + if "qwen3.5" not in case_config.base_model.lower(): + return + if gdn_layers <= 0: + raise RuntimeError("Expected Qwen3.5 runtime to include GatedDeltaNet layers.") + if topology.cp > 1 and case_config.num_layers == 1 and standard_attention_layers: + raise RuntimeError( + "Expected one-layer Qwen3.5 CP oracle to skip standard self-attention, " + f"found {standard_attention_layers} SelfAttention layer(s)." + ) def _delta_state( @@ -634,6 +888,173 @@ def _mutated_forward(self: Any, x: Any): module.forward = original_forward +@contextmanager +def _apply_attention_async_comm_mutation(mutation: SensitivityMutation | None): + if mutation != "attn_kv_fetch_pack_on_comm_stream": + yield + return + + from art.megatron.context_parallel import comm + + original = comm.A2AVCommunicator.launch_kv_fetch + comm_delay_cycles = 80_000_000 + + def _mutated_launch_kv_fetch( + self: Any, + *, + k_local: torch.Tensor, + v_local: torch.Tensor, + plan: Any, + group: Any, + async_op: bool, + range_meta_cache: dict[Any, Any] | None = None, + label: str = "kv_fetch", + input_layout: str = "token_major", + output_layout: str = "head_major", + ): + if group is None or comm._DIST.get_world_size(group) == 1: + return original( + self, + k_local=k_local, + v_local=v_local, + plan=plan, + group=group, + async_op=async_op, + range_meta_cache=range_meta_cache, + label=label, + input_layout=input_layout, + output_layout=output_layout, + ) + + total_send_rows = int(sum(plan.send_splits)) + total_recv_rows = int(sum(plan.recv_splits)) + recv_packed = k_local.new_empty( + comm._packed_peer_tensor_shape( + tensor=k_local, + total_rows=total_recv_rows, + input_layout=input_layout, + ) + ) + input_split_sizes = [split * 2 for split in plan.send_splits] + output_split_sizes = [split * 2 for split in plan.recv_splits] + stream = self._get_stream(k_local) if async_op else None + if stream is None: + return original( + self, + k_local=k_local, + v_local=v_local, + plan=plan, + group=group, + async_op=async_op, + range_meta_cache=range_meta_cache, + label=label, + input_layout=input_layout, + output_layout=output_layout, + ) + current_stream = torch.cuda.current_stream(k_local.device) + if total_send_rows > 0: + stream.wait_stream(current_stream) + with torch.cuda.stream(stream): + if total_send_rows <= 0: + send_buffer = k_local.new_empty( + comm._packed_peer_tensor_shape( + tensor=k_local, + total_rows=0, + input_layout=input_layout, + ) + ) + else: + send_buffer = comm._pack_gathered_tensors_per_peer( + left_tensor=k_local, + right_tensor=v_local, + ranges_by_peer=plan.send_ranges_by_peer, + range_meta_cache=range_meta_cache, + input_layout=input_layout, + ) + if total_send_rows > 0: + torch.cuda._sleep(comm_delay_cycles) + handle = comm._launch_peer_exchange( + recv_buffer=recv_packed, + send_buffer=send_buffer, + output_split_sizes=output_split_sizes, + input_split_sizes=input_split_sizes, + group=group, + async_op=True, + ) + if total_send_rows > 0 and send_buffer.numel() > 0: + send_buffer.zero_() + return comm.KvFetchWork( + packed_buffer=recv_packed, + recv_splits=plan.recv_splits, + handle=handle, + send_buffer=send_buffer, + stream=stream, + label=label, + output_layout=output_layout, + ) + + comm.A2AVCommunicator.launch_kv_fetch = _mutated_launch_kv_fetch # type: ignore[invalid-assignment] + try: + yield + finally: + comm.A2AVCommunicator.launch_kv_fetch = original + + +@contextmanager +def _apply_attention_nested_grad_mutation(mutation: SensitivityMutation | None): + if mutation != "attn_skip_nested_grad_sanitize": + yield + return + + from art.megatron.context_parallel import executor + + original = executor._sanitize_nested_stage_input_grad + shared_scratch: dict[tuple[int | None, torch.dtype], torch.Tensor] = {} + + def _mutated_sanitize(grad: torch.Tensor | None) -> torch.Tensor | None: + if grad is None: + return None + key = (grad.device.index, grad.dtype) + flat = shared_scratch.get(key) + needed = int(grad.numel()) + if flat is None or flat.numel() < needed: + flat = torch.empty(needed, device=grad.device, dtype=grad.dtype) + shared_scratch[key] = flat + view = flat[:needed].view_as(grad) + view.copy_(grad) + return view + + executor._sanitize_nested_stage_input_grad = _mutated_sanitize # type: ignore[invalid-assignment] + try: + yield + finally: + executor._sanitize_nested_stage_input_grad = original + + +@contextmanager +def _apply_attention_lse_normalize_mutation(mutation: SensitivityMutation | None): + if mutation != "attn_skip_flash_lse_normalize": + yield + return + + from art.megatron.context_parallel import executor + import art.megatron.flex_attn.compiled as compiled_flex_attention + + original_compiled = compiled_flex_attention.normalize_flex_lse + original_executor = executor.normalize_flex_lse + + def _identity(lse: torch.Tensor) -> torch.Tensor: + return lse + + compiled_flex_attention.normalize_flex_lse = _identity # type: ignore[invalid-assignment] + executor.normalize_flex_lse = _identity # type: ignore[invalid-assignment] + try: + yield + finally: + compiled_flex_attention.normalize_flex_lse = original_compiled + executor.normalize_flex_lse = original_executor + + @contextmanager def _patch_lora_for_fp32( model_chunks: list[Any], @@ -738,6 +1159,11 @@ def _mutation_hook( ) known_mutations = {None, *SUPPORTED_SENSITIVITY_MUTATIONS} + known_mutations |= { + "attn_kv_fetch_pack_on_comm_stream", + "attn_skip_nested_grad_sanitize", + "attn_skip_flash_lse_normalize", + } if mutation not in known_mutations: raise ValueError(f"Unsupported mutation: {mutation}") @@ -853,6 +1279,9 @@ def _scaled_loss_fn(*args: Any, **kwargs: Any): with ExitStack() as stack: stack.enter_context(_apply_o_proj_forward_mutation(model_chunks, mutation)) stack.enter_context(_apply_grad_sync_skip_mutation(model_chunks, mutation)) + stack.enter_context(_apply_attention_async_comm_mutation(mutation)) + stack.enter_context(_apply_attention_nested_grad_mutation(mutation)) + stack.enter_context(_apply_attention_lse_normalize_mutation(mutation)) try: yield finally: @@ -872,11 +1301,13 @@ def _scaled_loss_fn(*args: Any, **kwargs: Any): def _worker_run(request: WorkerRunRequest) -> None: """Executes one full distributed training trace generation worker run.""" + os.environ.setdefault(_ATTACH_TOKEN_UIDS_ENV, "1") from safetensors.torch import load_file, save_file # ty: ignore[unresolved-import] import torch from art import dev, types from art.megatron import train as megatron_train + from art.megatron.training.weight_offload import WeightOffloadManager from art.preprocessing.pack import packed_tensors_from_dir local_rank = int(os.environ["LOCAL_RANK"]) @@ -885,6 +1316,21 @@ def _worker_run(request: WorkerRunRequest) -> None: _enable_debug_traceback_dump() _set_deterministic_seed(request.case_config.seed) _configure_cuda_precision(request.case_config) + flex_patch_stack = ExitStack() + flex_patch_stack.enter_context( + _apply_requested_flex_backend_patch(request.flex_backend) + ) + flex_patch_stack.enter_context( + _apply_test_flex_inner_fp32_patch(request.flex_backend) + ) + flex_patch_stack.enter_context( + _apply_test_attention_full_fp32_patch(request.flex_backend) + ) + if request.case_config.precision == "fp32": + install_megatron_qwen35_gdn_fp32_reference( + flex_patch_stack, + base_model=request.case_config.base_model, + ) with provider_topology_env(request.topology): _debug( @@ -894,13 +1340,14 @@ def _worker_run(request: WorkerRunRequest) -> None: with _patch_finalize_provider_bundle_for_oracle( megatron_train, request.case_config ): + provider_torch_dtype = ( + torch.float32 + if request.case_config.precision == "fp32" + else torch.bfloat16 + ) runtime = megatron_train.build_training_runtime( model_identifier=request.case_config.base_model, - provider_torch_dtype=( - torch.float32 - if request.case_config.precision == "fp32" - else torch.bfloat16 - ), + provider_torch_dtype=provider_torch_dtype, provider_configure=lambda provider: _configure_provider( provider, request.topology, request.case_config ), @@ -913,8 +1360,17 @@ def _worker_run(request: WorkerRunRequest) -> None: _debug("finished build_training_runtime") model_chunks = runtime.model optimizer = runtime.optimizer - assert optimizer is not None - _assert_runtime_configuration(model_chunks, request.case_config) + _assert_runtime_configuration(model_chunks, request.case_config, request.topology) + weight_offload = WeightOffloadManager.from_config( + model=model_chunks, + rank=torch.distributed.get_rank(), # ty: ignore[possibly-missing-attribute] + compile_enabled=runtime.transformer_layers_compiled, + offload_between_jobs=request.offload_between_jobs, + streaming_config=request.streaming_weight_offload, + ) + weight_offload.install() + weight_offload.after_job() + weight_offload.before_job() topology_dir = Path(request.topology_dir) traces_dir = topology_dir / "traces" @@ -923,27 +1379,35 @@ def _worker_run(request: WorkerRunRequest) -> None: # setup the shared initial lora shared_init_path = Path(request.shared_init_adapter_path) if not shared_init_path.exists(): + _debug("collecting initial lora state") initial_state = _collect_lora_state(model_chunks) if torch.distributed.get_rank() == 0: # ty: ignore[possibly-missing-attribute] + _debug("building deterministic initial lora state") shared_init_path.parent.mkdir(parents=True, exist_ok=True) deterministic_init = _build_deterministic_shared_init( _require_not_none(initial_state, "initial_state"), seed=request.case_config.seed, ) + _debug("saving deterministic initial lora state") save_file( deterministic_init, str(shared_init_path), ) + _debug("waiting for shared initial lora state") torch.distributed.barrier() # ty: ignore[possibly-missing-attribute] # load the shared initial lora into the model and validate we can collect it from the model + _debug("loading shared initial lora state") adapter_model = load_file(str(shared_init_path)) megatron_train.load_adapter_into_model(model_chunks, adapter_model, optimizer) + _debug("collecting loaded lora state") loaded_state = _collect_lora_state(model_chunks) if torch.distributed.get_rank() == 0: # ty: ignore[possibly-missing-attribute] + _debug("validating loaded lora state") _validate_loaded_state_matches_adapter( _require_not_none(loaded_state, "loaded_state"), adapter_model ) + _debug("waiting after loaded lora validation") torch.distributed.barrier() # ty: ignore[possibly-missing-attribute] # load the inputs @@ -974,29 +1438,31 @@ def _worker_run(request: WorkerRunRequest) -> None: experimental_config: dev.TrainConfig = {} step_traces: list[StepTrace] = [] captured_grads: dict[str, Any] | None = None - routing_replay_controller = runtime.moe_routing_replay_controller - install_moe_routing_trace_hooks(lambda: runtime.moe_routing_replay_controller) forward_trace_capture = ForwardTraceCapture( model_chunks, enabled=True, - strict_output_match=request.mutation is None, ) + install_moe_routing_trace_hooks(lambda: runtime.moe_routing_replay_controller) def _capture_lora_grads() -> None: nonlocal captured_grads captured_grads = _collect_lora_grads(model_chunks) - with ( - _mutation_hook( - megatron_train, - model_chunks, - request.mutation, - request.topology, - pre_optimizer_step_hook=_capture_lora_grads, - loss_scale=request.case_config.loss_scale, - ), - _patch_lora_for_fp32(model_chunks, optimizer), - ): + with ExitStack() as training_stack: + training_stack.enter_context(install_gdn_trace_token_uid_hooks()) + training_stack.enter_context( + _mutation_hook( + megatron_train, + model_chunks, + request.mutation, + request.topology, + pre_optimizer_step_hook=_capture_lora_grads, + loss_scale=request.case_config.loss_scale, + ) + ) + if request.use_fp32_lora_reference: + training_stack.enter_context(_patch_lora_for_fp32(model_chunks, optimizer)) + _debug("starting training loop") for step_index in range(request.case_config.num_steps): micro_sample_indices = megatron_train.build_micro_sample_indices( @@ -1015,6 +1481,7 @@ def _capture_lora_grads() -> None: ) step_result = megatron_train.run_training_step( model_chunks=model_chunks, + provider=runtime.provider, model_support_handler=runtime.model_support_handler, optimizer=optimizer, learning_rate=train_config.learning_rate, @@ -1034,6 +1501,7 @@ def _capture_lora_grads() -> None: ) step_result = megatron_train.run_megatron_sft_step( model_chunks=model_chunks, + provider=runtime.provider, model_support_handler=runtime.model_support_handler, optimizer=optimizer, learning_rate=train_config.learning_rate, @@ -1044,7 +1512,17 @@ def _capture_lora_grads() -> None: moe_routing_replay_controller=runtime.moe_routing_replay_controller, ) _debug(f"finished step_index={step_index}") - ordered_micro_outputs = forward_trace_capture.ordered_step_outputs() + print(f"finished step_index={step_index}", flush=True) + ordered_step_outputs = ( + forward_trace_capture.ordered_step_outputs_with_sample_indices() + ) + if ordered_step_outputs is None: + ordered_micro_sample_indices = None + ordered_micro_outputs = None + else: + ordered_micro_sample_indices, ordered_micro_outputs = ( + ordered_step_outputs + ) forward_trace_capture.save_current_step(traces_dir) torch.distributed.barrier() # ty: ignore[possibly-missing-attribute] current_lora_state = _collect_lora_state(model_chunks) @@ -1080,7 +1558,9 @@ def _capture_lora_grads() -> None: raise RuntimeError("Expected at least one captured micro output") torch.save( - torch.stack(ordered_outputs, dim=0), + _stack_output_tensors( + [(-output).contiguous() for output in ordered_outputs] + ), topology_dir / output_rel, ) save_file(grads, str(topology_dir / grads_rel)) @@ -1095,6 +1575,11 @@ def _capture_lora_grads() -> None: / request.case_config.loss_scale ), probs_corr=step_result.probs_corr, + micro_sample_indices=list( + ordered_micro_sample_indices + if ordered_micro_sample_indices is not None + else micro_sample_indices + ), output_file=str(output_rel), grads_file=str(grads_rel), deltas_file=str(deltas_rel), @@ -1122,6 +1607,7 @@ def _capture_lora_grads() -> None: # build and save the run manifest manifest = RunManifest( + git=request.git, case_id=request.case_id, objective=request.objective, base_model=request.case_config.base_model, @@ -1131,17 +1617,26 @@ def _capture_lora_grads() -> None: seed=request.case_config.seed, num_steps=request.case_config.num_steps, packed_tensors=request.packed_tensors, + offload_between_jobs=request.offload_between_jobs, + streaming_weight_offload=request.streaming_weight_offload, + use_fp32_lora_reference=request.use_fp32_lora_reference, steps=step_traces, ) _write_json(topology_dir / "manifest.json", manifest.model_dump(mode="json")) + weight_offload.after_job() torch.distributed.barrier() # ty: ignore[possibly-missing-attribute] + flex_patch_stack.close() torch.distributed.destroy_process_group() # ty: ignore[possibly-missing-attribute] def run_worker_cli(run_request_path: Path) -> None: """Loads a worker request and dispatches worker execution.""" request = WorkerRunRequest.model_validate(_read_json(run_request_path)) - _worker_run(request) + try: + _worker_run(request) + finally: + if _oracle_debug_enabled(): + faulthandler.cancel_dump_traceback_later() def _parse_args(argv: list[str]) -> argparse.Namespace: diff --git a/tests/integration/megatron/model_support/packed_position_ids.py b/tests/integration/megatron/model_support/packed_position_ids.py index e29a0fbf4..44efe9047 100644 --- a/tests/integration/megatron/model_support/packed_position_ids.py +++ b/tests/integration/megatron/model_support/packed_position_ids.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +from contextlib import ExitStack import os from pathlib import Path import subprocess @@ -14,17 +15,25 @@ import torch from art.megatron import train as megatron_train -from art.megatron.flex_attention import create_shared_prefix_attention_state from art.megatron.model_support.discovery import inspect_architecture +from art.megatron.shared_prefix_state import create_shared_prefix_state +from ..artifacts import GitRepoState, pinned_git_state from .oracle_harness import ( ORACLE_TOPOLOGY, + TEST_DEFAULT_FLEX_BACKEND, OracleCaseConfig, PackedTensorConfig, _read_json, _write_json, ) -from .oracle_worker import _configure_provider, provider_topology_env +from .oracle_worker import ( + _apply_requested_flex_backend_patch, + _apply_test_attention_full_fp32_patch, + _apply_test_flex_inner_fp32_patch, + _configure_provider, + provider_topology_env, +) # Qwen3.5/3.6 hybrid MoE runs show small shape-dependent logit drift between # the single packed forward and many shorter reference forwards, even when the @@ -33,6 +42,7 @@ _LOGITS_MEAN_ABS_PCT_LIMIT = 0.2 _DEBUG_ENV = "ART_PACKED_POSITION_IDS_DEBUG" PACKED_POSITION_IDS_REPORT_FILENAME = "report.json" +PACKED_POSITION_IDS_ARTIFACT_SUITE_NAME = "Megatron packed-position-id artifacts" REPO_ROOT = Path(__file__).resolve().parents[4] @@ -136,6 +146,7 @@ class PackedPositionIdScenario(BaseModel): class PackedPositionIdsReport(BaseModel): + git: GitRepoState base_model: str output_dir: str num_layers: int @@ -143,6 +154,7 @@ class PackedPositionIdsReport(BaseModel): class PackedPositionIdsRunRequest(BaseModel): + git: GitRepoState base_model: str num_layers: int output_dir: str @@ -555,9 +567,12 @@ def _logits_equivalence_check( continue row_input_ids = input_ids[row_index : row_index + 1] row_position_ids = position_ids[row_index : row_index + 1] - packed_bias = create_shared_prefix_attention_state( + packed_bias = create_shared_prefix_state( group_ids=row_group_ids, parent_ids=row_parent_ids, + build_gdn_execution_spec=bool( + getattr(handler, "build_gdn_execution_spec", False) + ), ) _debug_log(f"logits_check row={row_index} families={len(families)}") packed_logits = _time_block( @@ -598,9 +613,12 @@ def _logits_equivalence_check( ) reference_group_ids = torch.zeros_like(reference_input_ids) reference_parent_ids = torch.zeros_like(reference_input_ids) - reference_bias = create_shared_prefix_attention_state( + reference_bias = create_shared_prefix_state( group_ids=reference_group_ids, parent_ids=reference_parent_ids, + build_gdn_execution_spec=bool( + getattr(handler, "build_gdn_execution_spec", False) + ), ) _debug_log( "logits_check row=" @@ -710,6 +728,7 @@ def _run_packed_position_ids_subprocess( def _run_packed_position_ids_worker( *, + git: GitRepoState, base_model: str, num_layers: int, output_dir: Path, @@ -760,6 +779,7 @@ def _run_packed_position_ids_worker( ), ] report = PackedPositionIdsReport( + git=git, base_model=base_model, output_dir=str(output_dir), num_layers=num_layers, @@ -775,6 +795,16 @@ def _run_packed_position_ids_worker( allow_unvalidated_arch=allow_unvalidated_arch, ) runtime: megatron_train.TrainingRuntime | None = None + flex_patch_stack = ExitStack() + flex_patch_stack.enter_context( + _apply_requested_flex_backend_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + flex_patch_stack.enter_context( + _apply_test_flex_inner_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) + flex_patch_stack.enter_context( + _apply_test_attention_full_fp32_patch(TEST_DEFAULT_FLEX_BACKEND) + ) try: with provider_topology_env(ORACLE_TOPOLOGY): runtime = _time_block( @@ -897,6 +927,7 @@ def _run_packed_position_ids_worker( torch.cuda.empty_cache() _debug_log("run complete; model deleted and cuda cache emptied") finally: + flex_patch_stack.close() del runtime torch.cuda.empty_cache() _cleanup_distributed_state() @@ -933,6 +964,7 @@ def run_packed_position_ids( if report_path.exists(): report_path.unlink() request = PackedPositionIdsRunRequest( + git=pinned_git_state(PACKED_POSITION_IDS_ARTIFACT_SUITE_NAME), base_model=base_model, num_layers=resolved_num_layers, output_dir=str(output_dir), @@ -946,6 +978,7 @@ def run_packed_position_ids( def run_worker_cli(run_request_path: Path) -> None: request = PackedPositionIdsRunRequest.model_validate(_read_json(run_request_path)) _run_packed_position_ids_worker( + git=request.git, base_model=request.base_model, num_layers=request.num_layers, output_dir=Path(request.output_dir), diff --git a/tests/integration/megatron/model_support/routing_replay_bundle.py b/tests/integration/megatron/model_support/routing_replay_bundle.py new file mode 100644 index 000000000..379ca36ba --- /dev/null +++ b/tests/integration/megatron/model_support/routing_replay_bundle.py @@ -0,0 +1,300 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import torch + +from art.megatron.routing_replay import ( + ROUTER_KEY_FORMAT_VERSION, + ROUTER_NAME_TOKEN, + MoeRoutingReplayBundle, + ParallelTopology, + RouterCallRoute, + StepRouterRoutes, + StepRoutes, + build_router_key_from_module_name, +) + + +def _flatten_router_tensor(tensor: torch.Tensor) -> torch.Tensor: + if tensor.ndim < 2: + raise RuntimeError( + f"Router tensor must have rank >=2, got shape={tuple(tensor.shape)}" + ) + num_experts = int(tensor.shape[-1]) + return tensor.reshape(-1, num_experts).contiguous() + + +def _extract_router_output_tensors( + call_entry: Any, +) -> tuple[torch.Tensor, torch.Tensor]: + probs = None + routing_map = None + if isinstance(call_entry, dict): + output = call_entry.get("output") + if isinstance(output, (list, tuple)) and len(output) >= 2: + probs, routing_map = output[0], output[1] + elif isinstance(output, dict): + probs = output.get("probs") + routing_map = output.get("routing_map") + elif isinstance(call_entry, (list, tuple)) and len(call_entry) >= 2: + probs, routing_map = call_entry[0], call_entry[1] + else: + raise RuntimeError(f"Unsupported router output type: {type(call_entry)}") + + if not isinstance(probs, torch.Tensor): + raise RuntimeError(f"Expected probs tensor, got {type(probs)}") + if not isinstance(routing_map, torch.Tensor): + raise RuntimeError(f"Expected routing_map tensor, got {type(routing_map)}") + + probs_2d = _flatten_router_tensor(probs.to(torch.float32)) + routing_map_2d = _flatten_router_tensor(routing_map.bool()) + if probs_2d.shape != routing_map_2d.shape: + raise RuntimeError( + "Router output shape mismatch: " + f"probs={tuple(probs_2d.shape)} routing_map={tuple(routing_map_2d.shape)}" + ) + return probs_2d, routing_map_2d + + +def _extract_dp_slot_from_rank_meta(rank_meta: Any) -> tuple[int, int] | None: + if isinstance(rank_meta, dict): + rank_meta = [rank_meta] + if not isinstance(rank_meta, list) or not rank_meta: + return None + dp_ranks = { + int(item["dp_rank"]) + for item in rank_meta + if isinstance(item, dict) and "dp_rank" in item + } + dp_world_sizes = { + int(item["dp_world_size"]) + for item in rank_meta + if isinstance(item, dict) and "dp_world_size" in item + } + if len(dp_ranks) != 1 or len(dp_world_sizes) != 1: + return None + return next(iter(dp_ranks)), next(iter(dp_world_sizes)) + + +def _trace_call_route_metadata( + call_entry: dict[str, Any], +) -> tuple[int | None, int | None]: + sample_index = call_entry.get("micro_sample_index") + if isinstance(sample_index, int): + return int(sample_index), None + dp_slot = _extract_dp_slot_from_rank_meta(call_entry.get("rank_meta")) + micro_order = int(call_entry.get("micro_order", 0)) + if dp_slot is None: + return None, micro_order + dp_rank, dp_world_size = dp_slot + return None, micro_order * dp_world_size + dp_rank + + +def _compact_route_from_dense( + probs_2d: torch.Tensor, + routing_map_2d: torch.Tensor, +) -> RouterCallRoute: + num_tokens, num_experts = probs_2d.shape + if num_tokens == 0: + return RouterCallRoute( + expert_indices=torch.zeros((0, 0), dtype=torch.int32), + expert_probs=torch.zeros((0, 0), dtype=torch.float32), + expert_mask=torch.zeros((0, 0), dtype=torch.bool), + num_experts=num_experts, + ) + + max_topk = int(routing_map_2d.sum(dim=1).max().item()) + expert_indices = torch.zeros((num_tokens, max_topk), dtype=torch.int32) + expert_probs = torch.zeros((num_tokens, max_topk), dtype=torch.float32) + expert_mask = torch.zeros((num_tokens, max_topk), dtype=torch.bool) + for token_index in range(num_tokens): + expert_ids = torch.nonzero( + routing_map_2d[token_index], as_tuple=False + ).flatten() + slot_count = int(expert_ids.numel()) + if slot_count == 0: + continue + expert_indices[token_index, :slot_count] = expert_ids.to(torch.int32) + expert_probs[token_index, :slot_count] = probs_2d[token_index, expert_ids].to( + torch.float32 + ) + expert_mask[token_index, :slot_count] = True + + return RouterCallRoute( + expert_indices=expert_indices, + expert_probs=expert_probs, + expert_mask=expert_mask, + num_experts=num_experts, + ) + + +def _rank_token_counts( + call_entry: dict[str, Any], token_count: int +) -> tuple[int, ...] | None: + row_splits = call_entry.get("primary_output__row_splits") + if not isinstance(row_splits, list): + return None + counts = tuple(int(count) for count in row_splits) + if sum(counts) != token_count: + return None + return counts + + +def _dedupe_checkpoint_router_calls( + call_entries: list[dict[str, Any]], +) -> list[dict[str, Any]]: + deduped: list[dict[str, Any]] = [] + previous_call_key: tuple[int | None, int | None, int] | None = None + previous_route: RouterCallRoute | None = None + for call_entry in call_entries: + probs_2d, routing_map_2d = _extract_router_output_tensors(call_entry) + compact_route = _compact_route_from_dense(probs_2d, routing_map_2d) + sample_index, micro_slot = _trace_call_route_metadata(call_entry) + call_key = ( + sample_index, + micro_slot, + int(call_entry.get("micro_order", 0)), + ) + is_checkpoint_duplicate = ( + previous_call_key == call_key + and previous_route is not None + and torch.equal(compact_route.expert_indices, previous_route.expert_indices) + and torch.equal(compact_route.expert_probs, previous_route.expert_probs) + and torch.equal(compact_route.expert_mask, previous_route.expert_mask) + ) + if is_checkpoint_duplicate: + continue + deduped.append(call_entry) + previous_call_key = call_key + previous_route = compact_route + return deduped + + +def _build_router_key_from_trace_name(trace_module_name: str) -> str: + if not trace_module_name.startswith("chunk"): + raise RuntimeError( + "Forward trace router module name must start with 'chunk.'; " + f"got '{trace_module_name}'" + ) + chunk_prefix, separator, module_name = trace_module_name.partition(".") + if not separator or not chunk_prefix.removeprefix("chunk").isdigit(): + raise RuntimeError( + "Forward trace router module name must start with 'chunk.'; " + f"got '{trace_module_name}'" + ) + return build_router_key_from_module_name( + chunk_index=int(chunk_prefix.removeprefix("chunk")), + module_name=module_name, + ) + + +def build_bundle_from_forward_trace_dir( + *, + traces_dir: str | Path, + num_steps: int, + topology: ParallelTopology, +) -> MoeRoutingReplayBundle: + trace_dir = Path(traces_dir) + steps: dict[int, StepRoutes] = {} + router_keys_union: set[str] = set() + max_topk = 0 + + for step_index in range(num_steps): + trace_path = trace_dir / f"forward_trace_step_{step_index:03d}.pt" + if not trace_path.exists(): + raise FileNotFoundError( + f"Missing forward trace for step={step_index}: {trace_path}" + ) + step_trace: dict[str, list[dict[str, Any]]] = torch.load( + trace_path, map_location="cpu", weights_only=False + ) + + step_routers: dict[str, StepRouterRoutes] = {} + step_global_tokens: int | None = None + token_count_by_call_key: dict[tuple[str, int], int] = {} + for module_name in sorted(step_trace.keys()): + if ROUTER_NAME_TOKEN not in module_name: + continue + router_key = _build_router_key_from_trace_name(module_name) + router_calls: dict[int, RouterCallRoute] = {} + deduped_router_calls = _dedupe_checkpoint_router_calls( + step_trace[module_name] + ) + for call_index, call_entry in enumerate(deduped_router_calls): + probs_2d, routing_map_2d = _extract_router_output_tensors(call_entry) + compact_route = _compact_route_from_dense(probs_2d, routing_map_2d) + sample_index, micro_slot = _trace_call_route_metadata(call_entry) + compact_route.sample_index = sample_index + compact_route.micro_slot = micro_slot + compact_route.rank_token_counts = _rank_token_counts( + call_entry, compact_route.num_global_tokens + ) + router_calls[call_index] = compact_route + max_topk = max(max_topk, compact_route.max_topk) + token_count = compact_route.num_global_tokens + call_key = ( + ("sample", int(sample_index)) + if sample_index is not None + else ( + ("dummy_micro_slot", int(micro_slot)) + if micro_slot is not None + else ("call_index", int(call_index)) + ) + ) + previous_token_count = token_count_by_call_key.get(call_key) + if ( + previous_token_count is not None + and previous_token_count != token_count + ): + raise RuntimeError( + "Inconsistent token count across routers for the same micro: " + f"step={step_index}, call_key={call_key}, " + f"expected={previous_token_count}, got={token_count}, " + f"router='{router_key}', call={call_index}" + ) + token_count_by_call_key[call_key] = token_count + step_global_tokens = ( + token_count + if step_global_tokens is None + else max(step_global_tokens, token_count) + ) + + if not router_calls: + raise RuntimeError( + f"Router trace has no calls for module '{module_name}' at step={step_index}" + ) + step_routers[router_key] = StepRouterRoutes(calls=router_calls) + router_keys_union.add(router_key) + + if not step_routers: + raise RuntimeError( + f"No router traces found for step={step_index} in {trace_path}" + ) + if step_global_tokens is None: + raise RuntimeError( + f"Could not infer token count for step={step_index} from router traces" + ) + global_token_uids = torch.arange(step_global_tokens, dtype=torch.int64) + steps[step_index] = StepRoutes( + routers=step_routers, + global_token_uids=global_token_uids, + ) + + router_keys = sorted(router_keys_union) + for step_index, step_routes in steps.items(): + if set(step_routes.routers.keys()) != set(router_keys): + raise RuntimeError( + f"Step {step_index} router keys differ from global set: " + f"step_keys={sorted(step_routes.routers.keys())}, router_keys={router_keys}" + ) + + return MoeRoutingReplayBundle( + format_version=ROUTER_KEY_FORMAT_VERSION, + topology=topology, + num_steps=num_steps, + max_topk=max_topk, + router_keys=router_keys, + steps=steps, + ) diff --git a/tests/integration/megatron/model_support/test_compile_flags.py b/tests/integration/megatron/model_support/test_compile_flags.py index 0edac9a94..15654fc09 100644 --- a/tests/integration/megatron/model_support/test_compile_flags.py +++ b/tests/integration/megatron/model_support/test_compile_flags.py @@ -1,21 +1,33 @@ from art.megatron.model_support.handlers.qwen3_5 import QWEN3_5_MOE_HANDLER from art.megatron.model_support.handlers.qwen3_moe import QWEN3_MOE_HANDLER +_QWEN3_MOE_COMPILE_FLAGS = ( + "alltoall_dtoh", + "alltoall_dispatch_preprocess", + "deepep_dispatch_combine", + "deepep_permute_restore", + "te_triton_permute_with_mask_map", +) +_QWEN35_MOE_COMPILE_FLAGS = ( + "alltoall_dtoh", + "alltoall_dispatch_preprocess", + "deepep_dispatch_combine", + "deepep_permute_restore", + "flex_token_dispatch_combine", + "te_triton_permute_with_mask_map", + "weighted_bias_swiglu_no_inner_forward_cast", +) + def test_qwen3_moe_compile_workarounds_cover_deepep_permute_restore() -> None: - config = QWEN3_MOE_HANDLER.compile_workaround_config(object()) - assert config.flags == ( - "alltoall_dtoh", - "alltoall_dispatch_preprocess", - "deepep_permute_restore", - ) + provider = type("Provider", (), {"context_parallel_size": 1})() + config = QWEN3_MOE_HANDLER.compile_workaround_config(provider) + assert config.flags == _QWEN3_MOE_COMPILE_FLAGS + assert config.unconditional_flags == () def test_qwen35_moe_compile_workarounds_cover_deepep_permute_restore() -> None: provider = type("Provider", (), {"moe_shared_expert_overlap": False})() config = QWEN3_5_MOE_HANDLER.compile_workaround_config(provider) - assert config.flags == ( - "alltoall_dtoh", - "alltoall_dispatch_preprocess", - "deepep_permute_restore", - ) + assert config.flags == _QWEN35_MOE_COMPILE_FLAGS + assert config.unconditional_flags == () diff --git a/tests/integration/megatron/model_support/test_hf_parity_invariants.py b/tests/integration/megatron/model_support/test_hf_parity_invariants.py index 24345136c..be07ec6f6 100644 --- a/tests/integration/megatron/model_support/test_hf_parity_invariants.py +++ b/tests/integration/megatron/model_support/test_hf_parity_invariants.py @@ -4,13 +4,12 @@ import pytest import torch -from art.megatron.model_support.handlers import QWEN3_5_MOE_HANDLER -from art.megatron.model_support.spec import MinimalLayerCoverageReport - +from ..artifacts import GitRepoState from . import hf_parity as hf_parity_module from . import hf_parity_worker as hf_parity_worker_module from .hf_parity import ( HF_PARITY_OUTPUT_DIRNAME, + HF_PARITY_PACKED_TENSORS, HF_PARITY_REPORT_FILENAME, HfParityReport, HfParityRunRequest, @@ -28,6 +27,11 @@ _normalize_hf_tensor_map_for_bridge, ) from .oracle_harness import DiskPackedTensorsSpec, OracleCaseConfig +from .validation_spec import MinimalLayerCoverageReport + + +def _git_state() -> GitRepoState: + return GitRepoState(path="/repo", commit="a" * 40, dirty=False) def test_build_parity_sample_indices_pads_with_none() -> None: @@ -37,6 +41,23 @@ def test_build_parity_sample_indices_pads_with_none() -> None: ) == [0, 1, None, None] +def test_hf_parity_uses_train_inf_mismatch_settings() -> None: + assert HF_PARITY_PACKED_TENSORS.sequence_length == 256 + assert HF_PARITY_PACKED_TENSORS.prefill_tokens == 64 + assert HF_PARITY_PACKED_TENSORS.decode_tokens == 64 + + phase_pass = hf_parity_module._hf_parity_phase_pass_fns() + assert cast(Any, phase_pass["outputs"]).limits == { + "relative_l2": 1e-2, + "mean_abs_pct": 1.0, + } + assert cast(Any, phase_pass["losses"]).limits == { + "relative_l2": 2e-2, + "mean_abs_pct": 2.0, + } + assert cast(Any, phase_pass["grads"]).limits == {"mean_abs_pct": 3.0} + + def test_set_hf_config_num_layers_updates_supported_field() -> None: config = SimpleNamespace(num_hidden_layers=28) @@ -105,6 +126,7 @@ def test_run_hf_parity_always_reruns_existing_report( output_dir = case_dir / HF_PARITY_OUTPUT_DIRNAME output_dir.mkdir(parents=True) stale_report = HfParityReport( + git=_git_state(), case_id="stale", base_model="Qwen/Qwen3.5-35B-A3B", model_key="qwen3_5_moe", @@ -142,6 +164,7 @@ def test_run_hf_parity_always_reruns_existing_report( def _fake_subprocess(request, run_output_dir): calls.append(request.case_id) fresh_report = HfParityReport( + git=request.git, case_id=request.case_id, base_model=request.case_config.base_model, model_key=request.coverage.model_key, @@ -171,6 +194,7 @@ def test_run_hf_parity_subprocess_does_not_override_recompute( monkeypatch, tmp_path ) -> None: request = HfParityRunRequest( + git=_git_state(), case_id="case-id", case_config=OracleCaseConfig(base_model="Qwen/Qwen3.5-35B-A3B"), packed_tensors=DiskPackedTensorsSpec( @@ -275,7 +299,6 @@ def test_normalize_hf_grads_for_bridge_keeps_expected_key_set() -> None: "model.language_model.layers.0.input_layernorm.weight", "lm_head.weight", }, - model_support_handler=QWEN3_5_MOE_HANDLER, ) assert set(normalized) == { @@ -297,6 +320,7 @@ def test_build_megatron_runtime_uses_training_provider_bundle( ) request = HfParityRunRequest( + git=_git_state(), case_id="case", case_config=OracleCaseConfig(base_model="Qwen/Qwen3.5-35B-A3B"), packed_tensors=DiskPackedTensorsSpec( diff --git a/tests/integration/megatron/model_support/test_lora_oracle_correctness.py b/tests/integration/megatron/model_support/test_lora_oracle_correctness.py index c66e87482..6b72a91a7 100644 --- a/tests/integration/megatron/model_support/test_lora_oracle_correctness.py +++ b/tests/integration/megatron/model_support/test_lora_oracle_correctness.py @@ -5,19 +5,23 @@ import pytest from .oracle_harness import ( - ORACLE_TOPOLOGY, + LIVE_TRAINING_LOG_PATH, SENSITIVITY_MUTATION_ENV, + TEST_DEFAULT_FLEX_BACKEND, available_gpu_count, case_config, + oracle_topology, run_sensitivity_suite, run_suite, sensitivity_enabled, sensitivity_mutations, + sensitivity_required_world_size, ) REPO_ROOT = Path(__file__).resolve().parents[4] CORRECTNESS_LOG_PATH = REPO_ROOT / ".local" / "correctness.log" SENSITIVITY_LOG_PATH = REPO_ROOT / ".local" / "sensitivity.log" +TEST_FLEX_BACKEND = TEST_DEFAULT_FLEX_BACKEND def _run_suite_with_log( @@ -26,6 +30,8 @@ def _run_suite_with_log( run: Callable[[], object], ) -> None: log_path.parent.mkdir(parents=True, exist_ok=True) + LIVE_TRAINING_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + LIVE_TRAINING_LOG_PATH.write_text("", encoding="utf-8") with log_path.open("w", encoding="utf-8") as log_file: with redirect_stdout(log_file), redirect_stderr(log_file): run() @@ -38,13 +44,9 @@ def _announce_report_log( ) -> None: with capsys.disabled(): print(f"\nMegatron LoRA oracle report log: {log_path}", flush=True) - - -def _require_gpus_for(topology_world_size: int) -> None: - gpu_count = available_gpu_count() - if gpu_count < topology_world_size: - pytest.skip( - f"Need {topology_world_size} GPUs for topology run, only found {gpu_count}" + print( + f"Megatron LoRA live training log: {LIVE_TRAINING_LOG_PATH}", + flush=True, ) @@ -53,22 +55,28 @@ def test_megatron_lora_topology_suite(capsys: pytest.CaptureFixture[str]) -> Non Runs the suite of topologies and expects each to pass (numerical differences within our thresholds) """ _announce_report_log(log_path=CORRECTNESS_LOG_PATH, capsys=capsys) + config = case_config() + topology = oracle_topology(is_moe=config.is_moe) gpu_count = available_gpu_count() - if gpu_count < ORACLE_TOPOLOGY.world_size(): + if gpu_count < topology.world_size(): CORRECTNESS_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) CORRECTNESS_LOG_PATH.write_text( ( "Topology suite skipped. " - f"Need {ORACLE_TOPOLOGY.world_size()} GPUs, found {gpu_count}.\n" + f"Need {topology.world_size()} GPUs, found {gpu_count}.\n" ), encoding="utf-8", ) - _require_gpus_for(ORACLE_TOPOLOGY.world_size()) + pytest.skip( + f"Need {topology.world_size()} GPUs for topology run, only found {gpu_count}" + ) _run_suite_with_log( log_path=CORRECTNESS_LOG_PATH, run=lambda: run_suite( - case_config=case_config(), + case_config=config, max_world_size=gpu_count, + oracle_flex_backend=TEST_FLEX_BACKEND, + variant_flex_backend=TEST_FLEX_BACKEND, ), ) @@ -95,22 +103,30 @@ def test_megatron_lora_diff_sensitivity(capsys: pytest.CaptureFixture[str]) -> N ) mutations = sensitivity_mutations() assert mutations + config = case_config() + sensitivity_world_size = sensitivity_required_world_size( + mutations, + is_moe=config.is_moe, + ) gpu_count = available_gpu_count() - if gpu_count < ORACLE_TOPOLOGY.world_size(): + if gpu_count < sensitivity_world_size: SENSITIVITY_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) SENSITIVITY_LOG_PATH.write_text( ( "Sensitivity suite skipped. " - f"Need {ORACLE_TOPOLOGY.world_size()} GPUs, found {gpu_count}.\n" + f"Need {sensitivity_world_size} GPUs, found {gpu_count}.\n" ), encoding="utf-8", ) - _require_gpus_for(ORACLE_TOPOLOGY.world_size()) + pytest.skip( + f"Need {sensitivity_world_size} GPUs for topology run, only found {gpu_count}" + ) _run_suite_with_log( log_path=SENSITIVITY_LOG_PATH, run=lambda: run_sensitivity_suite( - case_config=case_config(), + case_config=config, mutations=mutations, - max_world_size=gpu_count, + oracle_flex_backend=TEST_FLEX_BACKEND, + variant_flex_backend=TEST_FLEX_BACKEND, ), ) diff --git a/tests/integration/megatron/model_support/test_oracle_harness_invariants.py b/tests/integration/megatron/model_support/test_oracle_harness_invariants.py index 7f17de88d..5a45bc03a 100644 --- a/tests/integration/megatron/model_support/test_oracle_harness_invariants.py +++ b/tests/integration/megatron/model_support/test_oracle_harness_invariants.py @@ -1,19 +1,103 @@ +from typing import Any + +import pytest import torch -from .forward_trace import ForwardTraceCapture +from .forward_trace import ForwardTraceCapture, _extract_router_topk from .oracle_harness import ( + CP_ATTENTION_SENSITIVITY_MUTATIONS, + DENSE_CP_ATTENTION_SENSITIVITY_TOPOLOGY, + DENSE_DP_SENSITIVITY_TOPOLOGY, DENSE_ORACLE_TOPOLOGY, + DENSE_TOPOLOGIES, + FORWARD_EXPERT_LORA_TRACE_NOISE_REASON, + FORWARD_EXPERT_LORA_TRACE_NOISE_RELATIVE_L2_LIMIT, + ORACLE_DEFAULT_MEAN_ABS_PCT_LIMIT, ORACLE_TOPOLOGY, + ROUTER_SCORE_MEAN_ABS_PCT_LIMIT, + TEST_DEFAULT_FLEX_BACKEND, TOPOLOGIES, DiffAccumulator, + MetricRow, MetricThresholdRule, + PackedTensorConfig, Topology, + VariantRunner, _default_phase_pass_fns, + _resolve_test_flex_backend, _suite_variants, + case_config, selected_sensitivity_mutations_for_objective, + sensitivity_topology_for_mutation, ) +def _metric_row( + *, + phase: str, + param: str, + pass_signal: bool, + step_index: int = 0, + mean_abs_pct: float = 0.0, + relative_l2: float = 0.0, + topk_mismatch_fraction: float | None = None, + top1_mismatch_fraction: float | None = None, +) -> MetricRow: + return MetricRow( + case_id="case", + variant="variant", + topology="candidate", + oracle_topology="oracle", + step_index=step_index, + phase=phase, + param=param, + numel=1.0, + mean_abs_diff=0.0, + relative_l2=relative_l2, + typical_abs_scale=1.0, + mean_abs_pct=mean_abs_pct, + topk_mismatch_fraction=topk_mismatch_fraction, + top1_mismatch_fraction=top1_mismatch_fraction, + pass_signal=pass_signal, + failure_reasons=[] if pass_signal else ["mean_abs_pct=2>1"], + ) + + +def _expert_trace_call( + *, + ep_rank: int, + etp_rank: int, + values: torch.Tensor, + uids: torch.Tensor, + hint: dict[str, object], +) -> dict[str, object]: + return { + "micro_call_index": 0, + "micro_order": 0, + "micro_sample_index": 0, + "module_type": "SyntheticExpert", + "primary_output": values, + "row_token_uids": uids, + "merge_hints": {"primary_output": hint}, + "rank_meta": { + "global_rank": ep_rank * 2 + etp_rank, + "world_size": 4, + "tp_rank": etp_rank, + "tp_world_size": 2, + "cp_rank": 0, + "cp_world_size": 1, + "ep_rank": ep_rank, + "ep_world_size": 2, + "etp_rank": etp_rank, + "etp_world_size": 2, + "dp_rank": 0, + "dp_world_size": 1, + "expert_dp_rank": 0, + "expert_dp_world_size": 1, + }, + } + + def test_metric_threshold_rule_can_require_strictly_positive_values() -> None: rule = MetricThresholdRule(minimums={"candidate_abs_scale": 0.0}) @@ -23,7 +107,7 @@ def test_metric_threshold_rule_can_require_strictly_positive_values() -> None: assert rule.failure_reasons(summary) == ["candidate_abs_scale=0<=0"] -def test_diff_accumulator_summary_tracks_candidate_abs_scale() -> None: +def test_diff_accumulator_summary_uses_aggregate_mean_abs_pct() -> None: accumulator = DiffAccumulator() accumulator.update( @@ -35,9 +119,472 @@ def test_diff_accumulator_summary_tracks_candidate_abs_scale() -> None: assert summary["typical_abs_scale"] == 1.5 assert summary["candidate_abs_scale"] == 0.25 + assert summary["mean_abs_diff"] == 1.25 + assert summary["mean_abs_pct"] == pytest.approx((1.25 / 1.5) * 100.0) + + +def test_context_parallel_accumulator_dtype_matches_dense_fp32_oracle() -> None: + from art.megatron.context_parallel.executor import _accum_output_dtype + + assert _accum_output_dtype(torch.float32) is torch.float32 + assert _accum_output_dtype(torch.bfloat16) is torch.float32 + assert _accum_output_dtype(torch.float16) is torch.float32 + + +def test_context_parallel_seeded_accumulator_can_own_stage_storage() -> None: + from art.megatron.context_parallel.executor import _seed_stage_accumulators + + stage_out = torch.tensor([[1.0, 2.0]], dtype=torch.float32) + stage_lse = torch.tensor([3.0], dtype=torch.float32) + + accum_out, accum_lse = _seed_stage_accumulators( + stage_out=stage_out, + stage_lse=stage_lse, + target_dtype=torch.float32, + needs_owned_storage=True, + ) + accum_out.add_(1.0) + accum_lse.add_(1.0) + + assert stage_out.tolist() == [[1.0, 2.0]] + assert stage_lse.tolist() == [3.0] + + +def test_fp32_oracle_defaults_to_test_triton_backend() -> None: + config = case_config().model_copy(update={"precision": "fp32"}) + + assert _resolve_test_flex_backend(config, None) == TEST_DEFAULT_FLEX_BACKEND + assert _resolve_test_flex_backend(config, "FLASH") == "FLASH" + + +def test_bf16_oracle_preserves_production_flex_default() -> None: + config = case_config().model_copy(update={"precision": "bf16"}) + + assert _resolve_test_flex_backend(config, None) is None + + +def test_production_compiled_flex_default_stays_flash() -> None: + from art.megatron.flex_attn import compiled as compiled_flex_attention + + assert compiled_flex_attention._FORCED_FLEX_BACKEND == "FLASH" + assert compiled_flex_attention._FORCED_FLEX_KERNEL_OPTIONS == {"BACKEND": "FLASH"} + + +def test_forward_trace_reads_row_uids_from_output_tensor() -> None: + output = torch.zeros((2, 1), dtype=torch.float32) + setattr(output, "_art_trace_row_token_uids", torch.tensor([4, 7])) + + row_uids, uid_span = ForwardTraceCapture._row_token_uids_for_trace( + inputs=(), + output=output, + module=object(), + ) + + assert uid_span is None + assert row_uids is not None + assert torch.equal(row_uids, torch.tensor([4, 7])) + + +def test_forward_trace_prefers_local_tensor_uids_over_module_fallback() -> None: + module = type("ModuleWithGenericTraceUids", (), {})() + inputs = torch.zeros((2, 1), dtype=torch.float32) + setattr(module, "_art_trace_row_token_uids", torch.tensor([10, 11])) + setattr(inputs, "_art_trace_row_token_uids", torch.tensor([4, 7])) + + row_uids, _uid_span = ForwardTraceCapture._row_token_uids_for_trace( + inputs=(inputs,), + module=module, + ) + + assert row_uids is not None + assert torch.equal(row_uids, torch.tensor([4, 7])) + + +def test_forward_trace_extracts_empty_router_topk_with_config_hint() -> None: + topk = _extract_router_topk( + ( + torch.empty((0, 8), dtype=torch.float32), + torch.empty((0, 8), dtype=torch.bool), + ), + topk_hint=2, + ) + assert topk is not None + ids, scores = topk + + assert ids.shape == (0, 2) + assert scores.shape == (0, 2) + + +def test_megatron_empty_swiglu_patch_preserves_known_output_width() -> None: + from art.megatron.runtime.bridge_runtime import install_art_bridge_runtime_patches + + install_art_bridge_runtime_patches() + from megatron.core.fusions.fused_bias_swiglu import bias_swiglu_impl + + x = torch.empty((0, 1, 8), dtype=torch.float32, requires_grad=True) + bias = torch.randn((8,), dtype=torch.float32, requires_grad=True) + y = bias_swiglu_impl(x, bias) + + assert y.shape == (0, 1, 4) + y.add_(torch.empty_like(y)) + y.sum().backward() + assert x.grad is not None + assert bias.grad is not None + assert torch.equal(bias.grad, torch.zeros_like(bias.grad)) + + +def test_megatron_empty_unpermute_patch_allows_view_then_inplace_add() -> None: + from art.megatron.runtime.bridge_runtime import install_art_bridge_runtime_patches + + install_art_bridge_runtime_patches() + from megatron.core.transformer.moe.token_dispatcher import unpermute + + tokens = torch.empty((0, 8), dtype=torch.float32, requires_grad=True) + sorted_indices = torch.empty((0,), dtype=torch.long) + output = unpermute( + tokens, + sorted_indices, + torch.Size((0, 8)), + fused=True, + ) + output = output.view(0, 1, 8) + output.add_(torch.empty_like(output)) + output.sum().backward() + + assert tokens.grad is not None + + +def test_forward_trace_splits_expert_rows_with_input_uid_span() -> None: + module_name = "chunk0.module.decoder.layers.0.mlp.experts.linear_fc1" + module = type("SyntheticExpertModule", (), {})() + inputs = torch.zeros((4, 1), dtype=torch.float32) + setattr(inputs, "_art_trace_row_token_uids", torch.tensor([0, 1, 10, 11])) + setattr(inputs, "_art_trace_uid_span", 10) + trace_item = { + "micro_sample_index": None, + "primary_output": torch.tensor([[0.0], [1.0], [2.0], [3.0]]), + } + + split_items = ForwardTraceCapture._split_expert_trace_items( + module_name=module_name, + module=module, + inputs=(inputs,), + trace_item=trace_item, + ) + + assert [item["micro_sample_index"] for item in split_items] == [0, 1] + assert torch.equal(split_items[0]["row_token_uids"], torch.tensor([0, 1])) + assert torch.equal(split_items[1]["row_token_uids"], torch.tensor([10, 11])) + assert torch.equal(split_items[0]["primary_output"], torch.tensor([[0.0], [1.0]])) + assert torch.equal(split_items[1]["primary_output"], torch.tensor([[2.0], [3.0]])) + assert split_items[0]["row_uid_span"] == 10 + assert split_items[1]["row_uid_span"] == 10 + + +def test_forward_trace_canonicalizes_row_outputs_by_token_uid() -> None: + trace: dict[str, list[dict[str, Any]]] = { + "chunk0.module.decoder.layers.0": [ + { + "primary_output": torch.tensor([[30.0], [10.0], [20.0]]), + "router_topk_scores": torch.tensor([[0.3], [0.1], [0.2]]), + "router_topk_ids": torch.tensor([[3], [1], [2]]), + "output": { + "probs": torch.tensor([[3.0], [1.0], [2.0]]), + "routing_map": torch.tensor([[True], [False], [True]]), + }, + "row_token_uids": torch.tensor([3, 1, 2]), + } + ] + } + + ForwardTraceCapture.canonicalize_trace(trace) + + call = trace["chunk0.module.decoder.layers.0"][0] + assert torch.equal(call["row_token_uids"], torch.tensor([1, 2, 3])) + assert torch.equal( + call["primary_output"], + torch.tensor([[10.0], [20.0], [30.0]]), + ) + assert torch.equal(call["router_topk_scores"], torch.tensor([[0.1], [0.2], [0.3]])) + assert torch.equal(call["router_topk_ids"], torch.tensor([[1], [2], [3]])) + assert torch.equal(call["output"]["probs"], torch.tensor([[1.0], [2.0], [3.0]])) + assert torch.equal( + call["output"]["routing_map"], + torch.tensor([[False], [True], [True]]), + ) + + +def test_forward_trace_expands_attention_output_uids_for_out_norm_heads() -> None: + trace: dict[str, list[dict[str, Any]]] = { + "chunk0.module.decoder.layers.0.self_attention": [ + { + "micro_order": 0, + "micro_sample_index": 0, + "primary_output": torch.zeros((3, 1, 8)), + "row_token_uids": torch.tensor([0, -1, 2]), + } + ], + "chunk0.module.decoder.layers.0.self_attention.out_norm": [ + { + "micro_order": 0, + "micro_sample_index": 0, + "primary_output": torch.arange(24, dtype=torch.float32).reshape(6, 4), + "merge_hints": { + "primary_output": { + "op": "concat", + "dim": 0, + "layout": "rank_blocked_token_heads", + "local_heads": 2, + "world_size_key": "tp_world_size", + } + }, + "rank_meta": {"tp_world_size": 1}, + } + ], + } + + ForwardTraceCapture.canonicalize_trace(trace) + + call = trace["chunk0.module.decoder.layers.0.self_attention.out_norm"][0] + assert torch.equal(call["row_token_uids"], torch.tensor([-1, -1, 0, 0, 2, 2])) + assert torch.equal( + call["primary_output"], + torch.tensor( + [ + [8.0, 9.0, 10.0, 11.0], + [12.0, 13.0, 14.0, 15.0], + [0.0, 1.0, 2.0, 3.0], + [4.0, 5.0, 6.0, 7.0], + [16.0, 17.0, 18.0, 19.0], + [20.0, 21.0, 22.0, 23.0], + ] + ), + ) + + +def test_forward_trace_merges_expert_tp_feature_shards_inside_ep_groups() -> None: + module_name = "chunk0.module.decoder.layers.0.mlp.experts.linear_fc1.gate_lora" + rank_traces = [ + { + module_name: [ + _expert_trace_call( + ep_rank=0, + etp_rank=0, + values=torch.tensor([[1.0, 2.0], [3.0, 4.0]]), + uids=torch.tensor([10, 20]), + hint={"op": "concat", "dim": -1}, + ) + ] + }, + { + module_name: [ + _expert_trace_call( + ep_rank=0, + etp_rank=1, + values=torch.tensor([[5.0, 6.0], [7.0, 8.0]]), + uids=torch.tensor([10, 20]), + hint={"op": "concat", "dim": -1}, + ) + ] + }, + { + module_name: [ + _expert_trace_call( + ep_rank=1, + etp_rank=0, + values=torch.tensor([[9.0, 10.0]]), + uids=torch.tensor([30]), + hint={"op": "concat", "dim": -1}, + ) + ] + }, + { + module_name: [ + _expert_trace_call( + ep_rank=1, + etp_rank=1, + values=torch.tensor([[11.0, 12.0]]), + uids=torch.tensor([30]), + hint={"op": "concat", "dim": -1}, + ) + ] + }, + ] + + merged = ForwardTraceCapture.canonicalize_trace( + ForwardTraceCapture._merge_rank_traces(rank_traces) + ) + call = merged[module_name][0] + + assert torch.equal(call["row_token_uids"], torch.tensor([10, 20, 30])) + assert torch.equal( + call["primary_output"], + torch.tensor( + [ + [1.0, 2.0, 5.0, 6.0], + [3.0, 4.0, 7.0, 8.0], + [9.0, 10.0, 11.0, 12.0], + ] + ), + ) + + +def test_forward_trace_sums_expert_tp_row_shards_inside_ep_groups() -> None: + module_name = "chunk0.module.decoder.layers.0.mlp.experts.linear_fc2" + rank_traces = [ + { + module_name: [ + _expert_trace_call( + ep_rank=0, + etp_rank=0, + values=torch.tensor([[1.0, 2.0]]), + uids=torch.tensor([10]), + hint={"op": "sum"}, + ) + ] + }, + { + module_name: [ + _expert_trace_call( + ep_rank=0, + etp_rank=1, + values=torch.tensor([[10.0, 20.0]]), + uids=torch.tensor([10]), + hint={"op": "sum"}, + ) + ] + }, + { + module_name: [ + _expert_trace_call( + ep_rank=1, + etp_rank=0, + values=torch.tensor([[3.0, 4.0]]), + uids=torch.tensor([20]), + hint={"op": "sum"}, + ) + ] + }, + { + module_name: [ + _expert_trace_call( + ep_rank=1, + etp_rank=1, + values=torch.tensor([[30.0, 40.0]]), + uids=torch.tensor([20]), + hint={"op": "sum"}, + ) + ] + }, + ] + + merged = ForwardTraceCapture.canonicalize_trace( + ForwardTraceCapture._merge_rank_traces(rank_traces) + ) + call = merged[module_name][0] + + assert torch.equal(call["row_token_uids"], torch.tensor([10, 20])) + assert torch.equal( + call["primary_output"], + torch.tensor([[11.0, 22.0], [33.0, 44.0]]), + ) + + +def test_gate_up_rank_interleaved_trace_layout_canonicalizes_dense_tp() -> None: + canonical = torch.arange(16, dtype=torch.float32).reshape(2, 1, 8) + gate0, gate1, up0, up1 = canonical.chunk(4, dim=-1) + rank_concat = torch.cat((gate0, up0, gate1, up1), dim=-1) + + actual = ForwardTraceCapture._canonicalize_primary_output_tensor( + module_name="chunk0.module.decoder.layers.0.mlp.linear_fc1", + tensor=rank_concat, + call={ + "merge_hints": { + "primary_output": { + "layout": "gate_up_rank_interleaved", + "world_size_key": "tp_world_size", + } + }, + "rank_meta": [{"tp_world_size": 2}, {"tp_world_size": 2}], + }, + ) + + assert torch.equal(actual, canonical) + + +def test_forward_trace_canonicalizes_cp_tp_rank_blocked_heads_with_row_uids() -> None: + module_name = "chunk0.module.decoder.layers.0.self_attention.out_norm" + rank_traces = [] + rank_specs = [ + (0, 0, [10, 10, 20, 20], [[100.0], [101.0], [200.0], [201.0]]), + (0, 1, [10, 10, 20, 20], [[110.0], [111.0], [210.0], [211.0]]), + (1, 0, [5, 5], [[50.0], [51.0]]), + (1, 1, [5, 5], [[60.0], [61.0]]), + ] + for global_rank, (cp_rank, tp_rank, uids, values) in enumerate(rank_specs): + rank_traces.append( + { + module_name: [ + { + "micro_call_index": 0, + "micro_order": 0, + "micro_sample_index": 0, + "module_type": "RMSNorm", + "primary_output": torch.tensor(values), + "row_token_uids": torch.tensor(uids), + "merge_hints": { + "primary_output": { + "op": "concat", + "dim": 0, + "layout": "rank_blocked_token_heads", + "local_heads": 2, + "world_size_key": "tp_world_size", + } + }, + "rank_meta": { + "global_rank": global_rank, + "world_size": 4, + "tp_rank": tp_rank, + "tp_world_size": 2, + "cp_rank": cp_rank, + "cp_world_size": 2, + }, + } + ] + } + ) + + merged = ForwardTraceCapture.canonicalize_trace( + ForwardTraceCapture._merge_rank_traces(rank_traces) + ) + call = merged[module_name][0] + + assert torch.equal( + call["row_token_uids"], + torch.tensor([5, 5, 5, 5, 10, 10, 10, 10, 20, 20, 20, 20]), + ) + assert torch.equal( + call["primary_output"].flatten(), + torch.tensor( + [ + 50.0, + 51.0, + 60.0, + 61.0, + 100.0, + 101.0, + 110.0, + 111.0, + 200.0, + 201.0, + 210.0, + 211.0, + ] + ), + ) -def test_default_phase_rules_require_non_zero_forward_outputs_losses_grads_and_deltas() -> ( +def test_default_phase_rules_require_non_zero_forward_outputs_grads_and_deltas() -> ( None ): phase_pass = _default_phase_pass_fns() @@ -50,41 +597,179 @@ def test_default_phase_rules_require_non_zero_forward_outputs_losses_grads_and_d assert not phase_pass["forward"](zero_signal_summary) assert not phase_pass["outputs"](zero_signal_summary) - assert not phase_pass["losses"](zero_signal_summary) assert not phase_pass["grads"](zero_signal_summary) assert not phase_pass["deltas"](zero_signal_summary) + assert phase_pass["losses"](zero_signal_summary) + + +def test_default_phase_rules_use_default_mean_abs_pct_limit() -> None: + phase_pass = _default_phase_pass_fns() + passing_summary = { + "relative_l2": 0.0, + "mean_abs_pct": ORACLE_DEFAULT_MEAN_ABS_PCT_LIMIT, + "typical_abs_scale": 1.0, + "candidate_abs_scale": 1.0, + } + failing_summary = { + **passing_summary, + "mean_abs_pct": ORACLE_DEFAULT_MEAN_ABS_PCT_LIMIT + 1e-6, + } + + assert phase_pass["forward"](passing_summary) + assert phase_pass["outputs"](passing_summary) + assert phase_pass["grads"](passing_summary) + assert phase_pass["deltas"](passing_summary) + assert phase_pass["losses"](passing_summary) + assert not phase_pass["forward"](failing_summary) + assert not phase_pass["outputs"](failing_summary) + assert not phase_pass["grads"](failing_summary) + assert not phase_pass["deltas"](failing_summary) + assert not phase_pass["losses"](failing_summary) + + +def test_router_score_rule_uses_tight_dedicated_limit() -> None: + phase_pass = _default_phase_pass_fns() + assert phase_pass["router_scores"]( + {"relative_l2": 1.0, "mean_abs_pct": ROUTER_SCORE_MEAN_ABS_PCT_LIMIT} + ) + assert not phase_pass["router_scores"]( + {"relative_l2": 0.0, "mean_abs_pct": ROUTER_SCORE_MEAN_ABS_PCT_LIMIT + 1e-8} + ) + + +def test_forward_expert_lora_noise_pass_requires_clean_step_gates() -> None: + noisy_row = _metric_row( + phase="forward", + param="chunk0.module.decoder.layers.__layer_avg__.mlp.experts.linear_fc2.lora.call_3", + pass_signal=False, + mean_abs_pct=2.0, + relative_l2=FORWARD_EXPERT_LORA_TRACE_NOISE_RELATIVE_L2_LIMIT, + ) + rows = [ + noisy_row, + _metric_row(phase="outputs", param="logprobs.micro_000", pass_signal=True), + _metric_row( + phase="router_scores", + param="chunk0.module.decoder.layers.__layer_avg__.mlp.router.call_3", + pass_signal=True, + mean_abs_pct=0.0, + relative_l2=0.0, + ), + _metric_row( + phase="router_topk_ids", + param="chunk0.module.decoder.layers.__layer_avg__.mlp.router.call_3", + pass_signal=True, + topk_mismatch_fraction=0.0, + top1_mismatch_fraction=0.0, + ), + ] + + VariantRunner._apply_forward_expert_lora_trace_noise_passes(rows) + + assert noisy_row.pass_signal + assert noisy_row.failure_reasons == [FORWARD_EXPERT_LORA_TRACE_NOISE_REASON] + + +def test_forward_expert_lora_noise_pass_rejects_broad_escape_hatches() -> None: + def _candidate(param: str, *, relative_l2: float = 1e-4) -> MetricRow: + return _metric_row( + phase="forward", + param=param, + pass_signal=False, + mean_abs_pct=2.0, + relative_l2=relative_l2, + ) + + def _gates( + *, output_pass: bool = True, router_exact: bool = True + ) -> list[MetricRow]: + return [ + _metric_row( + phase="outputs", param="logprobs.micro_000", pass_signal=output_pass + ), + _metric_row( + phase="router_scores", + param="chunk0.module.decoder.layers.__layer_avg__.mlp.router.call_3", + pass_signal=router_exact, + mean_abs_pct=0.0 if router_exact else 1e-9, + relative_l2=0.0 if router_exact else 1e-9, + ), + _metric_row( + phase="router_topk_ids", + param="chunk0.module.decoder.layers.__layer_avg__.mlp.router.call_3", + pass_signal=True, + topk_mismatch_fraction=0.0, + top1_mismatch_fraction=0.0, + ), + ] + + non_expert = _candidate( + "chunk0.module.decoder.layers.__layer_avg__.self_attention.out_proj.lora.call_3" + ) + VariantRunner._apply_forward_expert_lora_trace_noise_passes([non_expert, *_gates()]) + assert not non_expert.pass_signal + + too_large = _candidate( + "chunk0.module.decoder.layers.__layer_avg__.mlp.experts.linear_fc2.lora.call_3", + relative_l2=FORWARD_EXPERT_LORA_TRACE_NOISE_RELATIVE_L2_LIMIT + 1e-9, + ) + VariantRunner._apply_forward_expert_lora_trace_noise_passes([too_large, *_gates()]) + assert not too_large.pass_signal + + fc1_gate = _candidate( + "chunk0.module.decoder.layers.__layer_avg__.mlp.experts.linear_fc1.gate_lora.call_3" + ) + VariantRunner._apply_forward_expert_lora_trace_noise_passes([fc1_gate, *_gates()]) + assert fc1_gate.pass_signal + + fc1_up = _candidate( + "chunk0.module.decoder.layers.__layer_avg__.mlp.experts.linear_fc1.up_lora.call_3" + ) + VariantRunner._apply_forward_expert_lora_trace_noise_passes([fc1_up, *_gates()]) + assert fc1_up.pass_signal + + output_failed = _candidate( + "chunk0.module.decoder.layers.__layer_avg__.mlp.experts.linear_fc2.lora.call_3" + ) + VariantRunner._apply_forward_expert_lora_trace_noise_passes( + [output_failed, *_gates(output_pass=False)] + ) + assert not output_failed.pass_signal + + router_not_exact = _candidate( + "chunk0.module.decoder.layers.__layer_avg__.mlp.experts.linear_fc2.lora.call_3" + ) + VariantRunner._apply_forward_expert_lora_trace_noise_passes( + [router_not_exact, *_gates(router_exact=False)] + ) + assert not router_not_exact.pass_signal def test_suite_variants_skip_duplicate_oracle_replay_variant() -> None: - variants = _suite_variants("rl", is_moe=True) + variants = _suite_variants("rl") assert variants assert all(variant.topology != ORACLE_TOPOLOGY for variant in variants) assert all("oracle_replay" not in variant.name for variant in variants) -def test_dense_suite_variants_include_tp2_dp2_without_oracle_duplicate() -> None: +def test_dense_suite_variants_preserve_dense_and_cp_topologies() -> None: variants = _suite_variants("rl", is_moe=False) assert variants assert all(variant.topology != DENSE_ORACLE_TOPOLOGY for variant in variants) assert any( - variant.topology.tp == 2 and variant.topology.dp == 2 for variant in variants + variant.topology.tp == 2 + and variant.topology.dp == 2 + and variant.topology.cp == 1 + for variant in variants + ) + assert any( + variant.topology.tp == 2 + and variant.topology.dp == 2 + and variant.topology.cp == 2 + for variant in variants ) - - -def test_moe_suite_variants_use_minimal_non_cp_topology_matrix() -> None: - assert TOPOLOGIES == [ - Topology(tp=1, ep=1, etp=1, dp=1, sp=False), - Topology(tp=2, ep=2, etp=1, dp=1, sp=True), - Topology(tp=2, ep=1, etp=2, dp=1, sp=True), - Topology(tp=1, ep=2, etp=1, dp=2, sp=False), - ] - assert [topology.world_size() for topology in TOPOLOGIES] == [1, 2, 2, 2] - - variants = _suite_variants("rl", is_moe=True) - - assert [variant.topology for variant in variants] == TOPOLOGIES[1:] def test_max_world_size_arg_filters_dense_variants() -> None: @@ -97,34 +782,89 @@ def test_max_world_size_arg_filters_dense_variants() -> None: ) -def test_max_world_size_arg_filters_sensitivity_mutations() -> None: +def test_oracle_topologies_are_the_compact_cp_validation_matrix() -> None: + assert TOPOLOGIES == [ + Topology(tp=1, ep=1, etp=1, dp=1, sp=False), + Topology(tp=1, ep=2, etp=1, dp=1, cp=2, sp=False), + Topology(tp=2, ep=2, etp=1, dp=1, cp=2, sp=True), + Topology(tp=2, ep=4, etp=2, dp=2, cp=2, sp=True), + ] + assert [topology.world_size() for topology in TOPOLOGIES] == [1, 2, 4, 8] + + +def test_dense_topologies_include_vllm_separation_and_cp_coverage() -> None: + assert DENSE_TOPOLOGIES == [ + Topology(tp=1, ep=1, etp=1, dp=1, sp=False), + Topology(tp=2, ep=1, etp=1, dp=1, sp=True), + Topology(tp=1, ep=1, etp=1, dp=2, sp=False), + Topology(tp=2, ep=1, etp=1, dp=2, sp=True), + Topology(tp=1, ep=1, etp=1, dp=1, cp=2, sp=False), + Topology(tp=2, ep=1, etp=1, dp=1, cp=2, sp=True), + Topology(tp=2, ep=1, etp=1, dp=2, cp=2, sp=True), + ] + assert [topology.world_size() for topology in DENSE_TOPOLOGIES] == [ + 1, + 2, + 2, + 4, + 2, + 4, + 8, + ] + + +def test_dense_sensitivity_keeps_dp_and_cp_attention_cases() -> None: mutations = selected_sensitivity_mutations_for_objective( "rl", - ["skip_finalize", "dp_local_token_normalization"], - is_moe=True, - max_world_size=1, + [ + "skip_finalize", + "dp_local_token_normalization", + *CP_ATTENTION_SENSITIVITY_MUTATIONS, + ], + is_moe=False, ) - assert mutations == [] + assert mutations == [ + "skip_finalize", + "dp_local_token_normalization", + *CP_ATTENTION_SENSITIVITY_MUTATIONS, + ] + assert sensitivity_topology_for_mutation("skip_finalize", is_moe=False) == Topology( + tp=2, ep=1, etp=1, dp=1, sp=True + ) + assert ( + sensitivity_topology_for_mutation( + "dp_local_token_normalization", + is_moe=False, + ) + == DENSE_DP_SENSITIVITY_TOPOLOGY + ) + assert ( + sensitivity_topology_for_mutation( + CP_ATTENTION_SENSITIVITY_MUTATIONS[0], + is_moe=False, + ) + == DENSE_CP_ATTENTION_SENSITIVITY_TOPOLOGY + ) -def test_gate_up_rank_interleaved_trace_layout_canonicalizes_dense_tp() -> None: - canonical = torch.arange(16, dtype=torch.float32).reshape(2, 1, 8) - gate0, gate1, up0, up1 = canonical.chunk(4, dim=-1) - rank_concat = torch.cat((gate0, up0, gate1, up1), dim=-1) +def test_case_config_base_model_can_be_overridden_by_env( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("ART_ORACLE_BASE_MODEL", "Qwen/Qwen3.5-35B-A3B") - actual = ForwardTraceCapture._canonicalize_primary_output_tensor( - module_name="chunk0.module.decoder.layers.0.mlp.linear_fc1", - tensor=rank_concat, - call={ - "merge_hints": { - "primary_output": { - "layout": "gate_up_rank_interleaved", - "world_size_key": "tp_world_size", - } - }, - "rank_meta": [{"tp_world_size": 2}, {"tp_world_size": 2}], - }, - ) + assert case_config().base_model == "Qwen/Qwen3.5-35B-A3B" + assert case_config(base_model="custom/model").base_model == "custom/model" - assert torch.equal(actual, canonical) + +def test_packed_tensor_defaults_match_main_rebase_oracle_tokens() -> None: + config = PackedTensorConfig() + + assert config.num_sequences == 4 + assert config.sequence_length == 1024 + assert config.prefill_tokens == 256 + assert config.completion_branches_per_prefix == 2 + assert config.decode_tokens == 128 + assert config.decode_tokens_jitter == 32 + assert config.packing_mode == "stop_early" + assert config.vocab_high == 8192 diff --git a/tests/integration/megatron/model_support/test_provider_support.py b/tests/integration/megatron/model_support/test_provider_support.py index f4fb8be57..91620896f 100644 --- a/tests/integration/megatron/model_support/test_provider_support.py +++ b/tests/integration/megatron/model_support/test_provider_support.py @@ -9,12 +9,14 @@ from megatron.core.transformer.enums import AttnBackend -from art.megatron.flex_attention import FlexDotProductAttention +from art.megatron.context_parallel.core_attention import ArtContextParallelCoreAttention +from art.megatron.flex_attn.attention import FlexDotProductAttention from art.megatron.lora import default_lora_rank_for_handler from art.megatron.model_support.registry import ( UnsupportedModelArchitectureError, get_model_support_handler, get_model_support_spec, + model_uses_expert_parallel, ) import art.megatron.provider as provider_module @@ -24,7 +26,13 @@ def __init__(self) -> None: self.transformer_layer_spec = self._base_layer_spec self.finalized = False self.overlap_moe_expert_parallel_comm = False + self.moe_shared_expert_overlap = False self.num_moe_experts = 0 + self.recompute_granularity: str | None = None + self.recompute_method: str | None = None + self.recompute_num_layers: int | None = None + self.expert_model_parallel_size = 1 + self.expert_tensor_parallel_size = 1 def _base_layer_spec( self, config: object, vp_stage: int | None = None @@ -62,6 +70,25 @@ def _base_layer_spec( return SimpleNamespace(layer_specs=[gdn_layer, attention_layer]) +class _FakeGdnCpProvider(_FakeProvider): + def __init__(self) -> None: + super().__init__() + self.experimental_attention_variant = "gated_delta_net" + self.context_parallel_size = 2 + self.linear_attention_freq = 4 + self.linear_conv_kernel_dim = 2 + self.linear_key_head_dim = 8 + self.linear_value_head_dim = 16 + self.linear_num_key_heads = 2 + self.linear_num_value_heads = 4 + self.tensor_model_parallel_size = 1 + self.variant_seen_by_finalize: str | None = None + + def finalize(self) -> None: + self.variant_seen_by_finalize = self.experimental_attention_variant + self.finalized = True + + class _FakeBridge: def __init__(self, *, model_bridge: object, provider: _FakeProvider) -> None: self._model_bridge = model_bridge @@ -77,10 +104,17 @@ def test_openpipe_qwen3_14b_instruct_uses_qwen3_dense_support() -> None: handler = get_model_support_handler("OpenPipe/Qwen3-14B-Instruct") assert spec.key == "qwen3_dense" + assert spec.is_moe is False assert spec.native_vllm_lora_status == "validated" assert handler.key == "qwen3_dense" +def test_model_support_specs_own_moe_metadata() -> None: + assert model_uses_expert_parallel("OpenPipe/Qwen3-14B-Instruct") is False + assert model_uses_expert_parallel("Qwen/Qwen3-30B-A3B-Instruct-2507") is True + assert model_uses_expert_parallel("Qwen/Qwen3.5-35B-A3B") is True + + def test_megatron_lora_rank_defaults_by_architecture() -> None: dense_handler = get_model_support_handler("OpenPipe/Qwen3-14B-Instruct") moe_handler = get_model_support_handler("Qwen/Qwen3-30B-A3B-Instruct-2507") @@ -113,12 +147,12 @@ def test_get_provider_accepts_registry_supported_models( assert resolved.recompute_granularity == "full" assert resolved.recompute_method == "uniform" assert resolved.recompute_num_layers == 1 - assert resolved.tensor_model_parallel_size == 2 - assert resolved.context_parallel_size == 1 + assert resolved.tensor_model_parallel_size == 1 + assert resolved.context_parallel_size == 2 assert resolved.pipeline_model_parallel_size == 1 assert resolved.expert_model_parallel_size == 2 assert resolved.expert_tensor_parallel_size == 1 - assert resolved.sequence_parallel is True + assert resolved.sequence_parallel is False assert resolved.moe_shared_expert_overlap is False assert resolved.moe_router_dtype == "fp32" assert resolved.moe_aux_loss_coeff == 0.0 @@ -127,10 +161,20 @@ def test_get_provider_accepts_registry_supported_models( layer_spec = cast(Any, resolved.transformer_layer_spec)(resolved, vp_stage=7) assert ( layer_spec.submodules.self_attention.submodules.core_attention - is FlexDotProductAttention + is ArtContextParallelCoreAttention ) +def test_finalize_provider_bundle_allows_art_gdn_context_parallel() -> None: + provider = _FakeGdnCpProvider() + + provider_module._finalize_provider_with_art_overrides(cast(Any, provider)) + + assert provider.finalized is True + assert provider.variant_seen_by_finalize is None + assert provider.experimental_attention_variant == "gated_delta_net" + + def test_qwen35_provider_uses_handler_shared_expert_runtime_default( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -247,10 +291,12 @@ def test_finalize_provider_bundle_uses_post_prepare_topology( bundle = provider_module.prepare_provider_bundle("Qwen/Qwen3-30B-A3B-Instruct-2507") assert provider.finalized is False - assert getattr(provider, "tensor_model_parallel_size") == 2 + assert getattr(provider, "tensor_model_parallel_size") == 1 + assert getattr(provider, "context_parallel_size") == 2 assert getattr(provider, "expert_model_parallel_size") == 2 bundle.provider.tensor_model_parallel_size = 1 + bundle.provider.context_parallel_size = 1 bundle.provider.expert_model_parallel_size = 1 bundle.provider.sequence_parallel = False provider_module.finalize_provider_bundle(bundle) @@ -275,6 +321,7 @@ def test_get_provider_bundle_honors_single_gpu_env_topology( ) monkeypatch.setattr(provider_module.torch.cuda, "device_count", lambda: 2) monkeypatch.setenv("ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE", "1") + monkeypatch.setenv("ART_MEGATRON_CONTEXT_PARALLEL_SIZE", "1") monkeypatch.setenv("ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE", "1") monkeypatch.setenv("ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE", "1") @@ -299,6 +346,101 @@ def test_get_provider_bundle_honors_single_gpu_env_topology( ) +def test_get_provider_bundle_honors_context_parallel_env_topology( + monkeypatch: pytest.MonkeyPatch, +) -> None: + provider = _FakeProvider() + fake_bridge = _FakeBridge( + model_bridge=object(), + provider=provider, + ) + monkeypatch.setattr( + provider_module.AutoBridge, + "from_hf_pretrained", + lambda *args, **kwargs: fake_bridge, + ) + monkeypatch.setattr(provider_module.torch.cuda, "device_count", lambda: 4) + monkeypatch.setenv("ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE", "1") + monkeypatch.setenv("ART_MEGATRON_CONTEXT_PARALLEL_SIZE", "2") + monkeypatch.setenv("ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE", "1") + monkeypatch.setenv("ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE", "1") + + bundle = provider_module.get_provider_bundle("Qwen/Qwen3-30B-A3B-Instruct-2507") + resolved = bundle.provider + + assert resolved.tensor_model_parallel_size == 1 + assert resolved.context_parallel_size == 2 + assert resolved.expert_model_parallel_size == 1 + assert resolved.expert_tensor_parallel_size == 1 + layer_spec = resolved.transformer_layer_spec(resolved, vp_stage=0) + assert ( + layer_spec.submodules.self_attention.submodules.core_attention + is ArtContextParallelCoreAttention + ) + + +def test_qwen35_handler_keeps_standard_attention_on_flex_under_cp( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from art.megatron.model_support.handlers import qwen3_5 as qwen35_handler_module + + provider = _FakeHybridProvider() + fake_bridge = _FakeBridge( + model_bridge=object(), + provider=provider, + ) + monkeypatch.setattr( + provider_module.AutoBridge, + "from_hf_pretrained", + lambda *args, **kwargs: fake_bridge, + ) + monkeypatch.setattr(provider_module.torch.cuda, "device_count", lambda: 4) + monkeypatch.setenv("ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE", "1") + monkeypatch.setenv("ART_MEGATRON_CONTEXT_PARALLEL_SIZE", "2") + monkeypatch.setenv("ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE", "1") + monkeypatch.setenv("ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE", "1") + monkeypatch.setattr( + qwen35_handler_module, + "_qwen35_provider_types", + lambda: (_FakeHybridProvider,), + ) + monkeypatch.setattr( + qwen35_handler_module, + "_require_qwen35_provider_symbols", + lambda: ( + object(), + (_FakeHybridProvider,), + lambda block_spec, attention_module: None, + provider._base_layer_spec, + ), + ) + + resolved = provider_module.get_provider("Qwen/Qwen3.5-35B-A3B") + layer_spec = cast(Any, resolved).transformer_layer_spec(resolved, vp_stage=0) + + gdn_layer, attention_layer = layer_spec.layer_specs + assert not hasattr(gdn_layer.submodules.self_attention.submodules, "core_attention") + assert ( + attention_layer.submodules.self_attention.submodules.core_attention + is ArtContextParallelCoreAttention + ) + + +def test_art_flex_patch_uses_runtime_context_parallel_state( + monkeypatch: pytest.MonkeyPatch, +) -> None: + layer_spec = _FakeProvider()._base_layer_spec(SimpleNamespace()) + config = SimpleNamespace(context_parallel_size=1) + monkeypatch.setattr(provider_module, "_runtime_context_parallel_size", lambda: 2) + + provider_module.patch_art_flex_attention(layer_spec, config) + + assert ( + layer_spec.submodules.self_attention.submodules.core_attention + is ArtContextParallelCoreAttention + ) + + def test_get_provider_bundle_disables_recompute_from_env( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -323,7 +465,7 @@ def test_get_provider_bundle_disables_recompute_from_env( assert resolved.recompute_granularity is None assert resolved.recompute_method is None assert resolved.recompute_num_layers is None - assert resolved.recompute_modules is None + assert resolved.recompute_modules == [] def test_get_provider_bundle_honors_expert_parallel_env_overrides( @@ -350,3 +492,40 @@ def test_get_provider_bundle_honors_expert_parallel_env_overrides( assert resolved.expert_model_parallel_size == 1 assert resolved.expert_tensor_parallel_size == 2 assert resolved.sequence_parallel is True + + +def test_ep_overlap_recompute_contract_disables_full_recompute() -> None: + provider = _FakeProvider() + provider.overlap_moe_expert_parallel_comm = True + provider.moe_shared_expert_overlap = True + provider.recompute_granularity = "full" + provider.recompute_method = "uniform" + provider.recompute_num_layers = 1 + + provider_module._enforce_ep_overlap_recompute_contract(cast(Any, provider)) + + assert provider.moe_shared_expert_overlap is False + assert provider.recompute_granularity is None + assert provider.recompute_method is None + assert provider.recompute_num_layers is None + + +def test_finalize_provider_bundle_can_disable_flex_dispatcher_backend( + monkeypatch: pytest.MonkeyPatch, +) -> None: + provider = _FakeProvider() + provider.expert_model_parallel_size = 2 + provider.expert_tensor_parallel_size = 1 + dispatcher_calls: list[str] = [] + monkeypatch.setenv("ART_MEGATRON_MOE_FLEX_DISPATCHER_BACKEND", "disabled") + monkeypatch.setattr( + provider_module, + "apply_flex_dispatcher_backend", + lambda provider, moe_flex_dispatcher_backend: dispatcher_calls.append( + cast(str, moe_flex_dispatcher_backend) + ), + ) + + provider_module._apply_art_training_runtime_finalize_defaults(cast(Any, provider)) + + assert dispatcher_calls == [] diff --git a/tests/integration/megatron/model_support/test_workflow.py b/tests/integration/megatron/model_support/test_workflow.py index 50e408e0e..0a9fec8d5 100644 --- a/tests/integration/megatron/model_support/test_workflow.py +++ b/tests/integration/megatron/model_support/test_workflow.py @@ -1,16 +1,17 @@ +import os from types import SimpleNamespace from art.megatron.model_support.spec import ( ArchitectureReport, LayerFamilyInstance, - ValidationReport, - ValidationStageResult, ) +from .validation_spec import ValidationReport, ValidationStageResult from .workflow import ( MANDATORY_VALIDATION_STAGES, NATIVE_VLLM_LORA_STAGE, SKIP_SENSITIVITY_ENV, + _inspect_architecture_for_workflow, assess_minimal_layer_coverage, build_all_architectures_validation_report, build_validation_report, @@ -48,6 +49,40 @@ def test_validated_architecture_representative_models_are_fixed() -> None: ] +def test_inspect_architecture_for_workflow_uses_minimal_topology(monkeypatch) -> None: + seen_env: dict[str, str | None] = {} + + def _inspect_architecture(base_model: str, **kwargs) -> ArchitectureReport: + del kwargs + seen_env.update( + { + "tp": os.environ.get("ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE"), + "cp": os.environ.get("ART_MEGATRON_CONTEXT_PARALLEL_SIZE"), + "ep": os.environ.get("ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE"), + "etp": os.environ.get("ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE"), + } + ) + return ArchitectureReport( + base_model=base_model, + model_key="qwen3_dense", + handler_key="qwen3_dense", + layer_families=[LayerFamilyInstance(key="standard_attention", count=1)], + recommended_min_layers=1, + ) + + monkeypatch.setattr( + "tests.integration.megatron.model_support.workflow.inspect_architecture", + _inspect_architecture, + ) + + _inspect_architecture_for_workflow( + "Qwen/Qwen3-32B", + allow_unvalidated_arch=True, + ) + + assert seen_env == {"tp": "1", "cp": "1", "ep": "1", "etp": "1"} + + def test_build_all_architectures_validation_report_stops_on_failure( monkeypatch, tmp_path, @@ -70,6 +105,7 @@ def _build_validation_report( del allow_unvalidated_arch calls.append(base_model) return ValidationReport( + git={}, base_model=base_model, model_key="qwen3_dense", stages=[ diff --git a/tests/integration/megatron/model_support/trace_uids.py b/tests/integration/megatron/model_support/trace_uids.py new file mode 100644 index 000000000..9f8c5b398 --- /dev/null +++ b/tests/integration/megatron/model_support/trace_uids.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from typing import Any + +import torch + +TRACE_ROW_TOKEN_UIDS_ATTR = "_art_trace_row_token_uids" +TRACE_UID_SPAN_ATTR = "_art_trace_uid_span" + + +def extract_tensor_attr(value: Any, attr_name: str) -> Any: + if isinstance(value, torch.Tensor): + return getattr(value, attr_name, None) + if isinstance(value, dict): + for item in value.values(): + attr_value = extract_tensor_attr(item, attr_name) + if attr_value is not None: + return attr_value + if isinstance(value, (list, tuple)): + for item in value: + attr_value = extract_tensor_attr(item, attr_name) + if attr_value is not None: + return attr_value + return None + + +def normalize_row_token_uids(value: Any) -> torch.Tensor | None: + if not isinstance(value, torch.Tensor): + return None + return value.detach().to(device="cpu", dtype=torch.int64).reshape(-1) + + +def positive_uid_span(value: Any) -> int | None: + return int(value) if isinstance(value, int) and value > 0 else None + + +def row_token_uids_from_trace_sources( + *, + inputs: Any, + output: Any, + module: Any, + row_count: int | None = None, + prefer_uid_span: bool = False, +) -> tuple[torch.Tensor | None, int | None]: + candidates = ( + ( + extract_tensor_attr(output, TRACE_ROW_TOKEN_UIDS_ATTR), + extract_tensor_attr(output, TRACE_UID_SPAN_ATTR), + ), + ( + extract_tensor_attr(inputs, TRACE_ROW_TOKEN_UIDS_ATTR), + extract_tensor_attr(inputs, TRACE_UID_SPAN_ATTR), + ), + ( + getattr(module, TRACE_ROW_TOKEN_UIDS_ATTR, None), + getattr(module, TRACE_UID_SPAN_ATTR, None), + ), + ) + row_count_matches: list[tuple[torch.Tensor, int | None]] = [] + tensor_candidates: list[tuple[torch.Tensor, int | None]] = [] + for row_token_uids, uid_span in candidates: + row_token_uids = normalize_row_token_uids(row_token_uids) + if row_token_uids is None: + continue + candidate = (row_token_uids, positive_uid_span(uid_span)) + tensor_candidates.append(candidate) + if row_count is None or int(row_token_uids.numel()) == int(row_count): + row_count_matches.append(candidate) + if not tensor_candidates: + return None, None + + def _select( + options: list[tuple[torch.Tensor, int | None]], + ) -> tuple[torch.Tensor, int | None] | None: + if prefer_uid_span: + for row_token_uids, uid_span in options: + if uid_span is not None: + return row_token_uids, uid_span + return options[0] if options else None + + selected = _select(row_count_matches) or _select(tensor_candidates) + return selected if selected is not None else (None, None) + + +def expand_token_uids_for_heads( + token_uids: torch.Tensor, + *, + head_count: int, +) -> torch.Tensor: + if token_uids.ndim != 1: + raise RuntimeError( + f"Expected 1D token UID tensor, got shape={tuple(token_uids.shape)}" + ) + if head_count <= 0: + raise RuntimeError(f"Expected positive head_count, got {head_count}") + return token_uids.repeat_interleave(head_count).contiguous() diff --git a/tests/integration/megatron/model_support/validation_spec.py b/tests/integration/megatron/model_support/validation_spec.py new file mode 100644 index 000000000..6901f81a2 --- /dev/null +++ b/tests/integration/megatron/model_support/validation_spec.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + + +class MinimalLayerCoverageReport(BaseModel): + base_model: str + model_key: str + requested_num_layers: int + recommended_min_layers: int + covered: bool + missing_layer_families: list[str] = Field(default_factory=list) + unresolved_risks: list[str] = Field(default_factory=list) + + +class ValidationStageResult(BaseModel): + name: str + passed: bool = False + metrics: dict[str, Any] = Field(default_factory=dict) + artifact_dir: str | None = None + + +class ValidationReport(BaseModel): + git: dict[str, Any] + base_model: str + model_key: str + dependency_versions: dict[str, str] = Field(default_factory=dict) + stages: list[ValidationStageResult] = Field(default_factory=list) diff --git a/tests/integration/megatron/model_support/workflow.py b/tests/integration/megatron/model_support/workflow.py index c239457a8..88f389b21 100644 --- a/tests/integration/megatron/model_support/workflow.py +++ b/tests/integration/megatron/model_support/workflow.py @@ -19,8 +19,12 @@ ) from art.megatron.model_support.spec import ( ArchitectureReport, - MinimalLayerCoverageReport, NativeVllmLoraStatus, +) + +from ..artifacts import pinned_git_state +from .validation_spec import ( + MinimalLayerCoverageReport, ValidationReport, ValidationStageResult, ) @@ -33,6 +37,7 @@ LIVE_TRAINING_LOG_PATH = LOCAL_LOG_DIR / "live_training.log" ORACLE_LIVE_TRAINING_LOG_ENV = "ART_ORACLE_LIVE_TRAINING_LOG" SKIP_SENSITIVITY_ENV = "ART_MODEL_SUPPORT_SKIP_SENSITIVITY" +WORKFLOW_ARTIFACT_SUITE_NAME = "Megatron model-support validation workflow" MANDATORY_VALIDATION_STAGES = ( "dependency_resolution", @@ -106,6 +111,7 @@ def initialize_validation_report( ) handler = get_model_support_handler_for_spec(spec) return ValidationReport( + git=pinned_git_state(WORKFLOW_ARTIFACT_SUITE_NAME).model_dump(mode="json"), base_model=base_model, model_key=spec.key, dependency_versions=detect_dependency_versions(), @@ -151,6 +157,7 @@ def _inspect_architecture_for_workflow( # of inheriting visible GPU count and tripping model-specific TP limits. with _temporary_env( ART_MEGATRON_TENSOR_MODEL_PARALLEL_SIZE="1", + ART_MEGATRON_CONTEXT_PARALLEL_SIZE="1", ART_MEGATRON_EXPERT_MODEL_PARALLEL_SIZE="1", ART_MEGATRON_EXPERT_TENSOR_PARALLEL_SIZE="1", ): @@ -348,6 +355,7 @@ def run_hf_parity_stage( num_steps=1, allow_unvalidated_arch=allow_unvalidated_arch, ) + case_config = hf_parity.hf_parity_case_config(case_config) report = hf_parity.run_hf_parity(case_config=case_config) case_artifacts = oracle_harness.ensure_case_artifacts(case_config) artifact_dir = str( diff --git a/tests/integration/megatron/routing_replay/bundle.py b/tests/integration/megatron/routing_replay/bundle.py index f9cab1c40..e036397bd 100644 --- a/tests/integration/megatron/routing_replay/bundle.py +++ b/tests/integration/megatron/routing_replay/bundle.py @@ -1,234 +1,5 @@ from __future__ import annotations -from pathlib import Path -from typing import Any +from ..model_support.routing_replay_bundle import build_bundle_from_forward_trace_dir -import torch - -from art.megatron.routing_replay import ( - ROUTER_NAME_TOKEN, - MoeRoutingReplayBundle, - ParallelTopology, - RouterCallRoute, - StepRouterRoutes, - StepRoutes, - build_router_key_from_trace_name, -) - - -def _flatten_router_tensor(tensor: torch.Tensor) -> torch.Tensor: - if tensor.ndim < 2: - raise RuntimeError( - f"Router tensor must have rank >=2, got shape={tuple(tensor.shape)}" - ) - num_experts = int(tensor.shape[-1]) - return tensor.reshape(-1, num_experts).contiguous() - - -def _extract_router_output_tensors(output: Any) -> tuple[torch.Tensor, torch.Tensor]: - if isinstance(output, (list, tuple)) and len(output) >= 2: - probs, routing_map = output[0], output[1] - elif isinstance(output, dict): - probs = output.get("probs") - routing_map = output.get("routing_map") - else: - raise RuntimeError(f"Unsupported router output type: {type(output)}") - if not isinstance(probs, torch.Tensor): - raise RuntimeError(f"Expected probs tensor, got {type(probs)}") - if not isinstance(routing_map, torch.Tensor): - raise RuntimeError(f"Expected routing_map tensor, got {type(routing_map)}") - probs_2d = _flatten_router_tensor(probs.to(torch.float32)) - routing_map_2d = _flatten_router_tensor(routing_map.bool()) - if probs_2d.shape != routing_map_2d.shape: - raise RuntimeError( - "Router output shape mismatch: " - f"probs={tuple(probs_2d.shape)} routing_map={tuple(routing_map_2d.shape)}" - ) - return probs_2d, routing_map_2d - - -def _extract_dp_slot_from_rank_meta(rank_meta: Any) -> tuple[int, int] | None: - if isinstance(rank_meta, dict): - rank_meta = [rank_meta] - if not isinstance(rank_meta, list) or not rank_meta: - return None - dp_ranks = { - int(item["dp_rank"]) - for item in rank_meta - if isinstance(item, dict) and "dp_rank" in item - } - dp_world_sizes = { - int(item["dp_world_size"]) - for item in rank_meta - if isinstance(item, dict) and "dp_world_size" in item - } - if len(dp_ranks) != 1 or len(dp_world_sizes) != 1: - return None - return next(iter(dp_ranks)), next(iter(dp_world_sizes)) - - -def _trace_call_route_metadata( - call_entry: dict[str, Any], -) -> tuple[int | None, int | None]: - sample_index = call_entry.get("micro_sample_index") - if isinstance(sample_index, int): - return int(sample_index), None - dp_slot = _extract_dp_slot_from_rank_meta(call_entry.get("rank_meta")) - micro_order = int(call_entry.get("micro_order", 0)) - if dp_slot is None: - return None, micro_order - dp_rank, dp_world_size = dp_slot - return None, micro_order * dp_world_size + dp_rank - - -def _same_route(left: RouterCallRoute, right: RouterCallRoute) -> bool: - return bool( - left.num_experts == right.num_experts - and torch.equal(left.expert_indices, right.expert_indices) - and torch.equal(left.expert_mask, right.expert_mask) - ) - - -def _compact_route_from_dense( - _probs_2d: torch.Tensor, - routing_map_2d: torch.Tensor, -) -> RouterCallRoute: - num_tokens, num_experts = routing_map_2d.shape - if num_tokens == 0: - return RouterCallRoute( - expert_indices=torch.zeros((0, 0), dtype=torch.int32), - expert_mask=torch.zeros((0, 0), dtype=torch.bool), - num_experts=num_experts, - ) - topk_by_row = routing_map_2d.sum(dim=1) - if not bool((topk_by_row == topk_by_row[0]).all().item()): - raise RuntimeError( - "Megatron Core RouterReplay requires a fixed topk for every token row; " - f"observed row counts={torch.unique(topk_by_row).tolist()}" - ) - topk = int(topk_by_row[0].item()) - expert_indices = torch.zeros((num_tokens, topk), dtype=torch.int32) - for token_index in range(num_tokens): - expert_ids = torch.nonzero( - routing_map_2d[token_index], as_tuple=False - ).flatten() - if int(expert_ids.numel()) != topk: - raise RuntimeError( - f"Unexpected route topk for token={token_index}: " - f"expected={topk}, got={int(expert_ids.numel())}" - ) - expert_indices[token_index] = expert_ids.to(torch.int32) - return RouterCallRoute( - expert_indices=expert_indices, - expert_mask=torch.ones_like(expert_indices, dtype=torch.bool), - num_experts=num_experts, - ) - - -def build_bundle_from_forward_trace_dir( - *, - traces_dir: str | Path, - num_steps: int, - topology: ParallelTopology, -) -> MoeRoutingReplayBundle: - trace_dir = Path(traces_dir) - steps: dict[int, StepRoutes] = {} - router_keys_union: set[str] = set() - max_topk = 0 - - for step_index in range(num_steps): - trace_path = trace_dir / f"forward_trace_step_{step_index:03d}.pt" - if not trace_path.exists(): - raise FileNotFoundError( - f"Missing forward trace for step={step_index}: {trace_path}" - ) - step_trace: dict[str, list[dict[str, Any]]] = torch.load( - trace_path, map_location="cpu", weights_only=False - ) - - step_routers: dict[str, StepRouterRoutes] = {} - step_global_tokens: int | None = None - for module_name in sorted(step_trace.keys()): - if ROUTER_NAME_TOKEN not in module_name: - continue - router_key = build_router_key_from_trace_name(module_name) - router_calls: dict[int, RouterCallRoute] = {} - calls_by_micro_key: dict[tuple[str, int], int] = {} - for call_index, call_entry in enumerate(step_trace[module_name]): - probs_2d, routing_map_2d = _extract_router_output_tensors( - call_entry.get("output") - ) - compact_route = _compact_route_from_dense(probs_2d, routing_map_2d) - sample_index, micro_slot = _trace_call_route_metadata(call_entry) - compact_route.sample_index = sample_index - compact_route.micro_slot = micro_slot - micro_key = ( - ("sample", int(sample_index)) - if sample_index is not None - else ( - ("dummy_micro_slot", int(micro_slot)) - if micro_slot is not None - else None - ) - ) - if micro_key is not None and micro_key in calls_by_micro_key: - existing_call_index = calls_by_micro_key[micro_key] - existing_route = router_calls[existing_call_index] - if not _same_route(existing_route, compact_route): - raise RuntimeError( - "Router trace contains conflicting duplicate routes for " - f"router='{router_key}', step={step_index}, " - f"micro_key={micro_key}, existing_call={existing_call_index}, " - f"duplicate_call={call_index}" - ) - continue - stored_call_index = len(router_calls) - if micro_key is not None: - calls_by_micro_key[micro_key] = stored_call_index - router_calls[stored_call_index] = compact_route - max_topk = max(max_topk, compact_route.max_topk) - token_count = compact_route.num_global_tokens - if step_global_tokens is None: - step_global_tokens = token_count - elif step_global_tokens != token_count: - raise RuntimeError( - "Inconsistent token count across routers within step: " - f"step={step_index}, expected={step_global_tokens}, " - f"got={token_count}, router='{router_key}', call={call_index}" - ) - if not router_calls: - raise RuntimeError( - f"Router trace has no calls for module '{module_name}' " - f"at step={step_index}" - ) - step_routers[router_key] = StepRouterRoutes(calls=router_calls) - router_keys_union.add(router_key) - - if not step_routers: - raise RuntimeError( - f"No router traces found for step={step_index} in {trace_path}" - ) - if step_global_tokens is None: - raise RuntimeError( - f"Could not infer token count for step={step_index} from router traces" - ) - steps[step_index] = StepRoutes( - routers=step_routers, - global_token_uids=torch.arange(step_global_tokens, dtype=torch.int64), - ) - - router_keys = sorted(router_keys_union) - for step_index, step_routes in steps.items(): - if set(step_routes.routers) != set(router_keys): - raise RuntimeError( - f"Step {step_index} router keys differ from global set: " - f"step_keys={sorted(step_routes.routers)}, router_keys={router_keys}" - ) - - return MoeRoutingReplayBundle( - topology=topology, - num_steps=num_steps, - max_topk=max_topk, - router_keys=router_keys, - steps=steps, - ) +__all__ = ["build_bundle_from_forward_trace_dir"] diff --git a/tests/integration/megatron/routing_replay/trace.py b/tests/integration/megatron/routing_replay/trace.py index 6bb2402be..0f9628007 100644 --- a/tests/integration/megatron/routing_replay/trace.py +++ b/tests/integration/megatron/routing_replay/trace.py @@ -20,32 +20,99 @@ def _active_controller() -> Any | None: return _CONTROLLER_GETTER() +@torch._dynamo.disable def _dispatcher_local_token_uids( controller: Any, dispatcher: Any, *, num_local_tokens: int, ) -> torch.Tensor: + num_local_tokens = int(num_local_tokens) step_routes = controller._active_step_routes if step_routes is None: raise RuntimeError("Routing replay dispatcher used without an active step") - local_uids = controller.local_token_indexer.build_local_token_uids( - global_token_uids=step_routes.global_token_uids, - num_local_tokens=num_local_tokens, - sequence_parallel=bool( - getattr(getattr(dispatcher, "config", None), "sequence_parallel", False) - ), - context_parallel_size=int( - getattr(getattr(dispatcher, "config", None), "context_parallel_size", 1) - ), + sequence_parallel = bool( + getattr(getattr(dispatcher, "config", None), "sequence_parallel", False) ) + prepared_uids = getattr( + controller, + "local_token_uids_for_active_dispatch", + None, + ) + if callable(prepared_uids): + local_uids = prepared_uids( + num_local_tokens=num_local_tokens, + sequence_parallel=sequence_parallel, + ) + else: + local_uids = None + if local_uids is None: + route_uids, rank_sliced = _active_route_global_token_uids(controller) + if int(route_uids.numel()) == num_local_tokens: + local_uids = route_uids + elif rank_sliced: + local_uids = torch.arange(num_local_tokens, dtype=torch.int64) + else: + local_uids = controller.local_token_indexer.build_local_token_uids( + global_token_uids=route_uids, + num_local_tokens=num_local_tokens, + sequence_parallel=sequence_parallel, + context_parallel_size=int( + getattr( + getattr(dispatcher, "config", None), + "context_parallel_size", + 1, + ) + ), + ) sample_index = getattr(controller, "_active_sample_index", None) uid_span = int(step_routes.global_token_uids.numel()) if isinstance(sample_index, int) and sample_index >= 0 and uid_span > 0: - local_uids = local_uids + sample_index * uid_span + if bool((local_uids >= 0).all().item()): + local_uids = local_uids + sample_index * uid_span + else: + local_uids = local_uids.clone() + valid_mask = local_uids >= 0 + local_uids[valid_mask] += sample_index * uid_span return local_uids +def _active_route_global_token_uids(controller: Any) -> tuple[torch.Tensor, bool]: + step_routes = controller._active_step_routes + if step_routes is None: + raise RuntimeError("Routing replay dispatcher used without an active step") + active_call_key = controller._active_router_call_key() + if active_call_key is None: + return step_routes.global_token_uids, False + for router_key in sorted(controller._local_router_keys): + router_calls = step_routes.routers[router_key].calls + last_call_index = controller._router_last_call_indices.get(router_key) + if last_call_index in router_calls: + route = router_calls[last_call_index] + if controller._router_call_key(route) == active_call_key: + return _route_token_uids_for_rank(route) + for route in router_calls.values(): + if controller._router_call_key(route) == active_call_key: + return _route_token_uids_for_rank(route) + return step_routes.global_token_uids, False + + +def _route_token_uids_for_rank(route: Any) -> tuple[torch.Tensor, bool]: + token_uids = torch.arange(route.num_global_tokens, dtype=torch.int64) + rank_token_counts = getattr(route, "rank_token_counts", None) + if ( + rank_token_counts is None or not torch.distributed.is_initialized() # ty: ignore[possibly-missing-attribute] + ): + return token_uids, False + world_size = int(torch.distributed.get_world_size()) # ty: ignore[possibly-missing-attribute] + if len(rank_token_counts) != world_size: + return token_uids, False + rank = int(torch.distributed.get_rank()) # ty: ignore[possibly-missing-attribute] + start = sum(int(count) for count in rank_token_counts[:rank]) + end = start + int(rank_token_counts[rank]) + return token_uids[start:end].contiguous(), True + + def _trace_row_uids_from_source(source: Any) -> tuple[torch.Tensor | None, int | None]: row_token_uids = getattr(source, TRACE_ROW_TOKEN_UIDS_ATTR, None) if not isinstance(row_token_uids, torch.Tensor): diff --git a/tests/integration/megatron/runtime_isolation/artifacts.py b/tests/integration/megatron/runtime_isolation/artifacts.py index da754db97..feda950b5 100644 --- a/tests/integration/megatron/runtime_isolation/artifacts.py +++ b/tests/integration/megatron/runtime_isolation/artifacts.py @@ -1,88 +1,23 @@ from __future__ import annotations -from datetime import datetime, timezone -import os from pathlib import Path -import re -import subprocess -import sys -import uuid -from pydantic import BaseModel +from ..artifacts import REPO_ROOT +from ..artifacts import create_artifact_dir as _create_artifact_dir +from ..artifacts import require_clean_git_state as _require_clean_git_state TEST_ROOT = Path(__file__).resolve().parent ARTIFACTS_ROOT = TEST_ROOT / "artifacts" -REPO_ROOT = Path( - subprocess.run( - ["git", "rev-parse", "--show-toplevel"], - cwd=TEST_ROOT, - check=True, - capture_output=True, - text=True, - ).stdout.strip() -) - - -class ArtifactMetadata(BaseModel): - commit: str - branch: str - test_nodeid: str - created_at_utc: str - python_executable: str - artifact_dir: str - - -def _git(*args: str) -> str: - return subprocess.run( - ["git", *args], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=True, - ).stdout.strip() - - -def _dirty_lines() -> list[str]: - output = _git("status", "--porcelain=v1", "--untracked-files=all") - return [line for line in output.splitlines() if line] +SUITE_NAME = "Megatron runtime-isolation tests" def require_clean_git_state() -> str: - dirty = _dirty_lines() - if dirty: - rendered = "\n".join(dirty) - raise RuntimeError( - "Megatron runtime-isolation tests require a fully committed worktree.\n" - "Commit or remove these changes before running tests:\n" - f"{rendered}" - ) - return _git("rev-parse", "HEAD") - - -def _sanitize_nodeid(nodeid: str) -> str: - collapsed = re.sub(r"[^A-Za-z0-9_.-]+", "_", nodeid.strip()) - return collapsed.strip("._") or "unnamed_test" + return _require_clean_git_state(SUITE_NAME) def create_artifact_dir(test_nodeid: str) -> Path: - commit = require_clean_git_state() - branch = _git("branch", "--show-current") - test_name = _sanitize_nodeid(test_nodeid) - timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") - run_id = f"{timestamp}_{os.getpid()}_{uuid.uuid4().hex[:8]}" - artifact_dir = ARTIFACTS_ROOT / test_name / commit[:12] / run_id - artifact_dir.mkdir(parents=True, exist_ok=False) - - metadata = ArtifactMetadata( - commit=commit, - branch=branch, - test_nodeid=test_nodeid, - created_at_utc=datetime.now(timezone.utc).isoformat(), - python_executable=sys.executable, - artifact_dir=str(artifact_dir), - ) - (artifact_dir / "run_metadata.json").write_text( - metadata.model_dump_json(indent=2) + "\n", - encoding="utf-8", + return _create_artifact_dir( + test_nodeid, + artifacts_root=ARTIFACTS_ROOT, + suite_name=SUITE_NAME, ) - return artifact_dir diff --git a/tests/integration/megatron/runtime_isolation/test_live_megatron_backend_smoke.py b/tests/integration/megatron/runtime_isolation/test_live_megatron_backend_smoke.py index ad3ce4ffc..7cc102473 100644 --- a/tests/integration/megatron/runtime_isolation/test_live_megatron_backend_smoke.py +++ b/tests/integration/megatron/runtime_isolation/test_live_megatron_backend_smoke.py @@ -11,7 +11,7 @@ import art from art import dev -from art.megatron.runtime.backend import MegatronBackend +from art.megatron.backend import MegatronBackend from art.megatron.service import MegatronService from ..model_support.oracle_harness import ORACLE_TOPOLOGY, Topology @@ -35,7 +35,7 @@ DEDICATED_MULTIRANK_MERGED_ENV = "ART_RUN_LIVE_MEGATRON_MULTIRANK_MERGED_SMOKE" SHARED_LORA_ENV = "ART_RUN_LIVE_MEGATRON_SHARED_SMOKE" SHARED_LONG_LORA_ENV = "ART_RUN_LIVE_MEGATRON_SHARED_LONG_SMOKE" -SHARED_TOPOLOGY = Topology(tp=2, ep=2, etp=1, dp=1, sp=True) +SHARED_TOPOLOGY = Topology(tp=1, ep=2, etp=1, dp=1, cp=2, sp=False) def _base_model() -> str: diff --git a/tests/integration/megatron/runtime_isolation/test_runtime_launcher.py b/tests/integration/megatron/runtime_isolation/test_runtime_launcher.py index 0cb4bac95..b9bca13fe 100644 --- a/tests/integration/megatron/runtime_isolation/test_runtime_launcher.py +++ b/tests/integration/megatron/runtime_isolation/test_runtime_launcher.py @@ -1,16 +1,13 @@ -import importlib.util import os from pathlib import Path +from types import SimpleNamespace +from typing import Any, cast import pytest +from art import vllm_runtime as runtime + ROOT = Path(__file__).resolve().parents[4] -spec = importlib.util.spec_from_file_location( - "art_vllm_runtime_launcher", ROOT / "src" / "art" / "vllm_runtime.py" -) -assert spec is not None and spec.loader is not None -runtime = importlib.util.module_from_spec(spec) -spec.loader.exec_module(runtime) def test_get_vllm_runtime_project_root_defaults_to_repo_subdir(monkeypatch) -> None: @@ -71,6 +68,38 @@ def test_build_runtime_server_cmd_honors_runtime_bin_override(monkeypatch) -> No assert command[:2] == ["/opt/art/bin/runtime", "--wrapped"] +def test_get_vllm_runtime_nccl_so_path_queries_runtime_python( + monkeypatch, + tmp_path: Path, +) -> None: + monkeypatch.delenv("ART_VLLM_RUNTIME_BIN", raising=False) + runtime_root = tmp_path / "custom-runtime" + runtime_bin = runtime_root / ".venv" / "bin" / "art-vllm-runtime-server" + runtime_python = runtime_root / ".venv" / "bin" / "python" + runtime_bin.parent.mkdir(parents=True, exist_ok=True) + runtime_bin.write_text("#!/bin/sh\n", encoding="ascii") + runtime_python.write_text("#!/bin/sh\n", encoding="ascii") + nccl_so_path = tmp_path / "libnccl.so.2" + nccl_so_path.write_text("nccl\n", encoding="ascii") + seen: dict[str, object] = {} + + def fake_run(command, *, capture_output: bool, text: bool): + seen["command"] = command + seen["capture_output"] = capture_output + seen["text"] = text + return SimpleNamespace(returncode=0, stdout=f"{nccl_so_path}\n", stderr="") + + monkeypatch.setenv("ART_VLLM_RUNTIME_PROJECT_ROOT", str(runtime_root)) + monkeypatch.setattr(runtime.subprocess, "run", fake_run) + + assert runtime.get_vllm_runtime_nccl_so_path() == nccl_so_path.resolve() + command = seen["command"] + assert isinstance(command, list) + assert command[0] == str(runtime_python) + assert seen["capture_output"] is True + assert seen["text"] is True + + def test_cleanup_old_managed_runtimes_only_deletes_marked_venvs( monkeypatch, tmp_path: Path, @@ -259,7 +288,7 @@ async def get(self, url: str, timeout: float): monkeypatch.setattr(runtime.httpx, "AsyncClient", lambda: FakeClient()) await runtime.wait_for_vllm_runtime( - process=FakeProcess(), + process=cast(Any, FakeProcess()), host="127.0.0.1", port=8123, timeout=12.0, diff --git a/tests/integration/megatron/runtime_isolation/test_service_runtime_boundary.py b/tests/integration/megatron/runtime_isolation/test_service_runtime_boundary.py index 586f5673d..5d6bf40eb 100644 --- a/tests/integration/megatron/runtime_isolation/test_service_runtime_boundary.py +++ b/tests/integration/megatron/runtime_isolation/test_service_runtime_boundary.py @@ -9,6 +9,7 @@ import pytest from art.megatron.service import MegatronService +from art.types import MegatronTopologyConfig from art.unsloth.service import UnslothService @@ -178,7 +179,42 @@ async def test_megatron_dedicated_merged_start_syncs_initial_weights( assert location == ("127.0.0.1", 8000) start_vllm.assert_awaited_once() - sync_merged.assert_awaited_once_with(lora_path="/tmp/lora", step=0) + sync_merged.assert_awaited_once_with( + lora_path="/tmp/lora", + step=0, + megatron_topology=None, + ) + + +@pytest.mark.asyncio +async def test_megatron_dedicated_merged_start_uses_configured_topology( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + service = MegatronService( + model_name="test-model", + base_model="Qwen/Qwen3-0.6B", + config={ + "trainer_gpu_ids": [0], + "inference_gpu_ids": [1], + "rollout_weights_mode": "merged", + "megatron_topology": {"tp": 1, "cp": 2, "ep": 2, "etp": 1}, + }, + output_dir=str(tmp_path), + ) + start_vllm = AsyncMock(return_value=("127.0.0.1", 8000)) + sync_merged = AsyncMock() + monkeypatch.setattr(service, "_resolve_active_lora_path", lambda: "/tmp/lora") + monkeypatch.setattr(service, "_start_vllm_subprocess", start_vllm) + monkeypatch.setattr(service, "_sync_dedicated_merged_weights", sync_merged) + + await service.start_openai_server(None) + + sync_merged.assert_awaited_once_with( + lora_path="/tmp/lora", + step=0, + megatron_topology=MegatronTopologyConfig(tp=1, cp=2, ep=2, etp=1), + ) @pytest.mark.asyncio diff --git a/tests/integration/megatron/train_inf_mismatch/artifacts.py b/tests/integration/megatron/train_inf_mismatch/artifacts.py index 1ee3dee72..4ffdaf133 100644 --- a/tests/integration/megatron/train_inf_mismatch/artifacts.py +++ b/tests/integration/megatron/train_inf_mismatch/artifacts.py @@ -1,76 +1,23 @@ -from datetime import datetime, timezone -import os +from __future__ import annotations + from pathlib import Path -import re -import subprocess -import sys -import uuid -from pydantic import BaseModel +from ..artifacts import REPO_ROOT +from ..artifacts import create_artifact_dir as _create_artifact_dir +from ..artifacts import require_clean_git_state as _require_clean_git_state TEST_ROOT = Path(__file__).resolve().parent ARTIFACTS_ROOT = TEST_ROOT / "artifacts" -REPO_ROOT = Path( - subprocess.run( - ["git", "rev-parse", "--show-toplevel"], - cwd=TEST_ROOT, - check=True, - capture_output=True, - text=True, - ).stdout.strip() -) - - -class ArtifactMetadata(BaseModel): - commit: str - branch: str - test_nodeid: str - created_at_utc: str - python_executable: str - artifact_dir: str - - -def _git(*args: str) -> str: - return subprocess.run( - ["git", *args], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=True, - ).stdout.strip() +SUITE_NAME = "Megatron train/inf mismatch tests" def require_clean_git_state() -> str: - dirty = _git("status", "--porcelain=v1", "--untracked-files=all").splitlines() - if dirty: - rendered = "\n".join(dirty) - raise RuntimeError( - "Megatron train/inf mismatch tests require a committed worktree.\n" - "Commit or remove these changes before running tests:\n" - f"{rendered}" - ) - return _git("rev-parse", "HEAD") + return _require_clean_git_state(SUITE_NAME) def create_artifact_dir(test_nodeid: str) -> Path: - commit = require_clean_git_state() - test_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", test_nodeid).strip("._") - run_id = ( - f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}_" - f"{os.getpid()}_{uuid.uuid4().hex[:8]}" - ) - artifact_dir = ARTIFACTS_ROOT / (test_name or "unnamed_test") / commit[:12] / run_id - artifact_dir.mkdir(parents=True, exist_ok=False) - metadata = ArtifactMetadata( - commit=commit, - branch=_git("branch", "--show-current"), - test_nodeid=test_nodeid, - created_at_utc=datetime.now(timezone.utc).isoformat(), - python_executable=sys.executable, - artifact_dir=str(artifact_dir), - ) - (artifact_dir / "run_metadata.json").write_text( - metadata.model_dump_json(indent=2) + "\n", - encoding="utf-8", + return _create_artifact_dir( + test_nodeid, + artifacts_root=ARTIFACTS_ROOT, + suite_name=SUITE_NAME, ) - return artifact_dir diff --git a/tests/integration/megatron/train_inf_mismatch/output_parity.py b/tests/integration/megatron/train_inf_mismatch/output_parity.py index 8fd5c0584..5a2125c85 100644 --- a/tests/integration/megatron/train_inf_mismatch/output_parity.py +++ b/tests/integration/megatron/train_inf_mismatch/output_parity.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Sequence import hashlib import json import math @@ -23,12 +24,13 @@ # 4.606% mean_abs_pct while staying under the KL gate, so its gate is 5%. BF16_FWD_MEAN_ABS_PCT_LIMIT = 4.0 BF16_FWD_MEAN_ABS_PCT_LIMIT_BY_MODEL_KEY = { - "qwen3_moe": 7.0, + "qwen3_moe": 8.0, "qwen3_5_moe": 5.0, } TOP20_KL_CANDIDATE_TO_TARGET_LIMIT = 0.002 MEAN_ABS_PCT_DENOMINATOR_EPS = 1e-18 TOP_K = 20 +ScoreRecord = tuple[int, float, list[int], list[float]] RolloutMode = Literal["native_lora", "merged"] EngineSide = Literal["megatron", "vllm"] @@ -38,11 +40,11 @@ class Topology(BaseModel): model_config = ConfigDict(frozen=True) - tp: int = 2 + tp: int = 1 ep: int = 2 etp: int = 1 dp: int = 1 - cp: int = 1 + cp: int = 2 pp: int = 1 def world_size(self) -> int: @@ -258,6 +260,20 @@ def fwd_mean_abs_pct_limit_for_model( ) +def top20_kl_candidate_to_target_limit_for_model( + base_model: str, + *, + allow_unvalidated_arch: bool = False, +) -> float: + from art.megatron.model_support.registry import get_model_support_spec + + get_model_support_spec( + base_model, + allow_unvalidated_arch=allow_unvalidated_arch, + ) + return TOP20_KL_CANDIDATE_TO_TARGET_LIMIT + + def model_support_is_moe( base_model: str, *, @@ -653,6 +669,51 @@ def _gather_context_parallel_logits(logits: Any, *, full_sequence_length: int) - return gathered +def _packed_valid_lengths(packed_tensors: dict[str, Any]) -> list[int]: + return [ + int((packed_tensors["group_ids"][row_index] != -1).sum().item()) + for row_index in range(int(packed_tensors["group_ids"].shape[0])) + ] + + +def logical_logit_uids( + *, + packed_tensors: dict[str, Any], + logical_tokens: Sequence[LogicalToken], + sample_id_to_row: dict[int, int] | None = None, +) -> list[int]: + valid_lengths = _packed_valid_lengths(packed_tensors) + row_offsets: list[int] = [] + cursor = 0 + for valid_length in valid_lengths: + row_offsets.append(cursor) + cursor += valid_length + uids: list[int] = [] + for token in logical_tokens: + row_index = ( + sample_id_to_row[token.sample_id] + if sample_id_to_row is not None + else token.sample_id + ) + if row_index < 0 or row_index >= len(valid_lengths): + raise RuntimeError( + "Logical token sample does not map to a packed row: " + f"sample_id={token.sample_id}, row={row_index}" + ) + if ( + token.art_logit_index < 0 + or token.art_logit_index >= valid_lengths[row_index] + ): + raise RuntimeError( + "Logical token logit index is outside packed valid tokens: " + f"sample_id={token.sample_id}, row={row_index}, " + f"logit_index={token.art_logit_index}, " + f"valid_length={valid_lengths[row_index]}" + ) + uids.append(row_offsets[row_index] + token.art_logit_index) + return uids + + def _lora_target_modules(config: TrainInfOutputParityConfig) -> list[str]: from art.dev.get_model_config import default_target_modules @@ -692,7 +753,7 @@ def _build_deterministic_nonzero_lora( def _merge_sharded_lora(shards_by_rank: list[dict[str, Any]]) -> dict[str, Any]: - from art.megatron.weights.merge import merge_sharded_adapter_entries + from art.megatron.weights.lora_publish import merge_sharded_adapter_entries entries_by_key: dict[str, list[tuple[dict[str, Any], Any]]] = {} for rank_entry in shards_by_rank: @@ -789,16 +850,31 @@ def _run_logits( ) -> Any: import torch - from art.megatron.flex_attention import create_shared_prefix_attention_state + from art.megatron.shared_prefix_state import create_shared_prefix_state + from art.megatron.training.trace import ( + packed_sequence_token_uids, + prepare_replay_local_input_token_uids, + ) + from art.preprocessing.pack import PackedTensors device = next(runtime.model[0].parameters()).device input_ids = packed_tensors["tokens"].to(device=device) position_ids = packed_tensors["input_pos"].to(device=device) group_ids = packed_tensors["group_ids"].to(device=device) parent_ids = packed_tensors["parent_ids"].to(device=device) - attention_state = create_shared_prefix_attention_state( + attention_state = create_shared_prefix_state( group_ids=group_ids, parent_ids=parent_ids, + build_gdn_execution_spec=bool( + getattr(runtime.model_support_handler, "build_gdn_execution_spec", False) + ), + attention_head_dim=getattr(runtime.provider, "kv_channels", None), + attention_value_head_dim=getattr(runtime.provider, "kv_channels", None), + ) + prepare_replay_local_input_token_uids( + runtime.moe_routing_replay_controller, + packed_sequence_token_uids(cast(PackedTensors, packed_tensors), device=device), + attention_state, ) with torch.no_grad(): logits = runtime.model[0]( @@ -825,6 +901,275 @@ def _run_logits( return logits +def _batch_seq_logits(logits: Any, labels: Any) -> Any: + if int(logits.ndim) != 3: + raise RuntimeError( + f"Expected logits [B, S, V] or [S, B, V], got {logits.shape}" + ) + if tuple(logits.shape[:2]) == tuple(labels.shape): + return logits + if tuple(logits.shape[:2]) == (int(labels.shape[1]), int(labels.shape[0])): + return logits.transpose(0, 1).contiguous() + raise RuntimeError( + "Logits do not align with local labels: " + f"logits={tuple(logits.shape)}, labels={tuple(labels.shape)}" + ) + + +def _local_score_records_from_logits( + *, + logits: Any, + labels: Any, + token_uids: Any, + desired_uids: set[int], +) -> dict[int, ScoreRecord]: + import torch + + if token_uids is None: + raise RuntimeError("CP train/inf scoring requires local token_uids") + logits = _batch_seq_logits(logits, labels) + if tuple(token_uids.shape) != tuple(labels.shape): + raise RuntimeError( + "CP token uid shape does not match labels: " + f"uids={tuple(token_uids.shape)}, labels={tuple(labels.shape)}" + ) + if not desired_uids: + return {} + records: dict[int, ScoreRecord] = {} + log_probs = torch.log_softmax(logits.detach().float(), dim=-1) + labels_cpu = labels.detach().to(device="cpu") + token_uids_cpu = token_uids.detach().to(device="cpu") + mask = (labels_cpu != -100) & (token_uids_cpu >= 0) + for batch_index, seq_index in torch.nonzero(mask, as_tuple=False).tolist(): + uid = int(token_uids_cpu[batch_index, seq_index].item()) + if uid not in desired_uids: + continue + row = log_probs[batch_index, seq_index] + token_id = int(labels_cpu[batch_index, seq_index].item()) + values, indices = torch.topk(row, TOP_K) + records[uid] = ( + token_id, + float(row[token_id].item()), + [int(value) for value in indices.tolist()], + [float(value) for value in values.tolist()], + ) + return records + + +def _merge_score_records( + shards: Sequence[dict[int, ScoreRecord]], +) -> dict[int, ScoreRecord]: + merged: dict[int, ScoreRecord] = {} + for shard in shards: + for uid, record in shard.items(): + previous = merged.get(uid) + if previous is not None and previous != record: + raise RuntimeError(f"Duplicate CP score record for uid={uid}") + merged[uid] = record + return merged + + +def _score_bundle_from_records( + *, + records: dict[int, ScoreRecord], + logical_tokens: Sequence[LogicalToken], + logical_uids: Sequence[int], + side: EngineSide, + weight_state: WeightState, + rollout_mode: RolloutMode | None, +) -> ScoreBundle: + target_logprobs: list[float] = [] + topk: list[TokenTopK] = [] + missing: list[int] = [] + for token, uid in zip(logical_tokens, logical_uids, strict=True): + record = records.get(uid) + if record is None: + missing.append(uid) + continue + token_id, target_logprob, topk_ids, topk_logprobs = record + if token_id != token.token_id: + raise RuntimeError( + "CP score record target token does not match logical token: " + f"uid={uid}, record={token_id}, logical={token.token_id}" + ) + target_logprobs.append(target_logprob) + topk.append(TokenTopK(token_ids=topk_ids, logprobs=topk_logprobs)) + if missing: + raise RuntimeError( + "Missing CP score records for logical tokens: " + f"{missing[:16]} of {len(missing)} missing" + ) + return ScoreBundle( + side=side, + weight_state=weight_state, + rollout_mode=rollout_mode, + target_logprobs=target_logprobs, + topk=topk, + ) + + +def _score_context_parallel_once( + *, + runtime: Any, + packed_tensors: dict[str, Any], + logical_tokens: Sequence[LogicalToken], + sample_id_to_row: dict[int, int] | None, + side: EngineSide, + weight_state: WeightState, + rollout_mode: RolloutMode | None, +) -> ScoreBundle: + from megatron.core import parallel_state as ps + from megatron.core import tensor_parallel + import torch + import torch.distributed as dist + + from art.megatron.context_parallel.types import ParallelTopology + from art.megatron.training.microbatches import _prepare_current_rl_micro + from art.megatron.training.trace import ( + attach_trace_token_uids, + prepare_replay_local_input_token_uids, + ) + + model_chunks = cast(list[Any], runtime.model) + device = next(model_chunks[0].parameters()).device + topology = ParallelTopology( + tp=ps.get_tensor_model_parallel_world_size(), + cp=ps.get_context_parallel_world_size(), + dp=ps.get_data_parallel_world_size(), + pp=ps.get_pipeline_model_parallel_world_size(), + sp=bool(getattr(runtime.provider, "sequence_parallel", False)), + ) + prepared_micro, pending = _prepare_current_rl_micro( + cast(Any, packed_tensors), + device=device, + topology=topology, + provider=runtime.provider, + model_support_handler=runtime.model_support_handler, + ref_logprobs=None, + trace_token_uids=True, + pending_prepared_micro=None, + ) + if pending is not None: + raise RuntimeError("CP train/inf scoring unexpectedly returned lookahead state") + prepare_replay_local_input_token_uids( + runtime.moe_routing_replay_controller, + prepared_micro.local_token_uids, + prepared_micro.attention_state, + ) + with ( + torch.no_grad(), + attach_trace_token_uids( + model_chunks, + prepared_micro.local_token_uids, + ), + ): + logits = model_chunks[0]( + input_ids=prepared_micro.model_tokens, + position_ids=prepared_micro.model_input_pos, + attention_mask=torch.zeros((1, 1, 1, 1), dtype=torch.bool, device=device), + labels=None, + packed_seq_params=prepared_micro.packed_seq_params, + **runtime.model_support_handler.get_forward_kwargs( + model_chunks[0], + attention_bias=prepared_micro.attention_state, + ), + ) + if ps.get_tensor_model_parallel_world_size() > 1: + logits = tensor_parallel.gather_from_tensor_model_parallel_region(logits) + logical_uids = logical_logit_uids( + packed_tensors=packed_tensors, + logical_tokens=logical_tokens, + sample_id_to_row=sample_id_to_row, + ) + local_records: dict[int, ScoreRecord] = {} + if ps.get_tensor_model_parallel_rank() == 0: + local_records = _local_score_records_from_logits( + logits=logits, + labels=prepared_micro.model_labels, + token_uids=prepared_micro.local_token_uids, + desired_uids=set(logical_uids), + ) + gathered_records: list[dict[int, ScoreRecord]] = [ + {} for _ in range(dist.get_world_size()) + ] + dist.all_gather_object(gathered_records, local_records) + return _score_bundle_from_records( + records=_merge_score_records(gathered_records), + logical_tokens=logical_tokens, + logical_uids=logical_uids, + side=side, + weight_state=weight_state, + rollout_mode=rollout_mode, + ) + + +def score_context_parallel_runtime( + *, + runtime: Any, + packed_tensors: dict[str, Any], + logical_map: LogicalTokenMap, + weight_state: WeightState, + rollout_mode: RolloutMode | None = "native_lora", + global_grad_accumulation_sequences: int, +) -> ScoreBundle: + import torch + + controller = runtime.moe_routing_replay_controller + if controller is None: + return _score_context_parallel_once( + runtime=runtime, + packed_tensors=packed_tensors, + logical_tokens=logical_map.tokens, + sample_id_to_row=None, + side="megatron", + weight_state=weight_state, + rollout_mode=rollout_mode, + ) + + target_logprobs: list[float] = [] + topk: list[TokenTopK] = [] + tokens_by_sample: dict[int, list[LogicalToken]] = {} + for token in logical_map.tokens: + tokens_by_sample.setdefault(token.sample_id, []).append(token) + num_sequences = int(packed_tensors["tokens"].shape[0]) + for sample_index in range(num_sequences): + sample_tensors = { + key: ( + value[sample_index : sample_index + 1] + if isinstance(value, torch.Tensor) + and value.shape[:1] == packed_tensors["tokens"].shape[:1] + else value + ) + for key, value in packed_tensors.items() + } + step_index = sample_index // global_grad_accumulation_sequences + controller.set_step( + step_index=step_index, + sample_index=sample_index, + global_grad_accumulation_sequences=global_grad_accumulation_sequences, + ) + controller.begin_micro(sample_index, sample_index) + sample_score = _score_context_parallel_once( + runtime=runtime, + packed_tensors=sample_tensors, + logical_tokens=tokens_by_sample.get(sample_index, []), + sample_id_to_row={sample_index: 0}, + side="megatron", + weight_state=weight_state, + rollout_mode=rollout_mode, + ) + controller.finalize_step() + target_logprobs.extend(sample_score.target_logprobs) + topk.extend(sample_score.topk) + return ScoreBundle( + side="megatron", + weight_state=weight_state, + rollout_mode=rollout_mode, + target_logprobs=target_logprobs, + topk=topk, + ) + + def _extract_scores_from_logits( *, logits: Any, diff --git a/tests/integration/megatron/train_inf_mismatch/real_path.py b/tests/integration/megatron/train_inf_mismatch/real_path.py index a4399b91b..07bce52f8 100644 --- a/tests/integration/megatron/train_inf_mismatch/real_path.py +++ b/tests/integration/megatron/train_inf_mismatch/real_path.py @@ -21,7 +21,6 @@ from .artifacts import REPO_ROOT from .output_parity import ( - TOP20_KL_CANDIDATE_TO_TARGET_LIMIT, TOP_K, LogicalTokenMap, PairComparison, @@ -46,6 +45,8 @@ compare_topk, fwd_mean_abs_pct_limit_for_model, model_support_is_moe, + score_context_parallel_runtime, + top20_kl_candidate_to_target_limit_for_model, ) @@ -482,10 +483,7 @@ async def _score_base_real_generation_path( is_moe: bool, ) -> RealPathBaseDiagnosticBundle: import art - from art.megatron.routing_replay import ( - build_moe_routing_replay_bundle_from_packed_tensors, - ) - from art.megatron.runtime.backend import MegatronBackend + from art.megatron.backend import MegatronBackend from art.preprocessing.moe_routing import MoeRoutingPackStats from art.preprocessing.pack import packed_tensors_to_dir @@ -532,11 +530,11 @@ async def _score_base_real_generation_path( name=f"{served_name}_client", project="train_inf_mismatch", base_model=parity_config.base_model, - _internal_config=art.dev.InternalModelConfig( - init_args={ + _internal_config={ + "init_args": { "max_seq_length": parity_config.packed.sequence_length, }, - ), + }, ) object.__setattr__(model, "inference_base_url", f"http://{host}:{port}/v1") object.__setattr__(model, "inference_api_key", "EMPTY") @@ -580,14 +578,13 @@ async def _score_base_real_generation_path( global_grad_accumulation_sequences = int(packed_tensors["tokens"].shape[0]) if is_moe: routing_replay_dir = artifact_dir / "real_path_base_moe_routing_replay" - build_moe_routing_replay_bundle_from_packed_tensors( + _build_real_path_moe_routing_replay_bundle( packed_tensors=packed_tensors, + config=parity_config, global_grad_accumulation_sequences=global_grad_accumulation_sequences, ).to_dir(routing_replay_dir) routing_replay_path = str(routing_replay_dir) - moe_routing_replay = packed_tensors["moe_routing_replay"] - assert moe_routing_replay is not None - stats = moe_routing_replay.pack_stats + stats = packed_tensors["moe_routing_replay"].pack_stats else: stats = MoeRoutingPackStats() @@ -657,6 +654,37 @@ def _move_adapter_to_step_zero(*, adapter_path: str, model: Any, backend: Any) - return step_zero +def _routing_topology_from_config(config: TrainInfOutputParityConfig) -> Any: + from art.megatron.routing_replay import ParallelTopology + + return ParallelTopology( + tp=config.topology.tp, + ep=config.topology.ep, + etp=config.topology.etp, + dp=config.topology.dp, + sp=config.topology.tp > 1, + cp=config.topology.cp, + pp=config.topology.pp, + ) + + +def _build_real_path_moe_routing_replay_bundle( + *, + packed_tensors: Any, + config: TrainInfOutputParityConfig, + global_grad_accumulation_sequences: int, +) -> Any: + from art.megatron.routing_replay import ( + build_moe_routing_replay_bundle_from_packed_tensors, + ) + + return build_moe_routing_replay_bundle_from_packed_tensors( + packed_tensors=packed_tensors, + global_grad_accumulation_sequences=global_grad_accumulation_sequences, + topology=_routing_topology_from_config(config), + ) + + def _make_nonzero_adapter( *, config: TrainInfOutputParityConfig, @@ -720,6 +748,58 @@ def _run_logits_with_replay( return torch.cat(logits_by_sample, dim=0) +def _score_megatron_runtime( + *, + runtime: Any, + packed_tensors: dict[str, Any], + logical_map: LogicalTokenMap, + weight_state: WeightState, + global_grad_accumulation_sequences: int, + forward_trace_capture: Any | None, + forward_trace_dir: str | None, +) -> ScoreBundle: + from megatron.core import parallel_state as ps + import torch + + if int(ps.get_context_parallel_world_size()) > 1: + if forward_trace_capture is not None or forward_trace_dir is not None: + if forward_trace_capture is not None: + forward_trace_capture.close() + raise RuntimeError( + "CP train/inf mismatch scoring uses sparse UID records, not gathered " + "full logits, so forward trace logits capture is unsupported here." + ) + return score_context_parallel_runtime( + runtime=runtime, + packed_tensors=packed_tensors, + logical_map=logical_map, + weight_state=weight_state, + rollout_mode="native_lora", + global_grad_accumulation_sequences=global_grad_accumulation_sequences, + ) + + try: + logits = _run_logits_with_replay( + runtime=runtime, + packed_tensors=packed_tensors, + global_grad_accumulation_sequences=global_grad_accumulation_sequences, + ) + if forward_trace_capture is not None and forward_trace_dir is not None: + trace_dir = Path(forward_trace_dir) + forward_trace_capture.save_current_step(trace_dir) + torch.save(logits.detach().cpu(), trace_dir / "logits.pt") + finally: + if forward_trace_capture is not None: + forward_trace_capture.close() + return _extract_scores_from_logits( + logits=logits, + logical_map=logical_map, + side="megatron", + weight_state=weight_state, + rollout_mode="native_lora", + ) + + def _real_path_megatron_worker( request: RealPathMegatronWorkerRequest, *, @@ -728,7 +808,7 @@ def _real_path_megatron_worker( import torch from art.megatron import train as megatron_train - from art.megatron.weights.merge import load_lora_adapter_state_dict + from art.megatron.model_support.lora_disk import load_lora_tensors_for_megatron from art.preprocessing.pack import packed_tensors_from_dir local_rank = int(os.environ["LOCAL_RANK"]) @@ -786,7 +866,7 @@ def _configure_worker_bundle(bundle: Any) -> None: adapter_path = artifact_dir / "real_path_active_lora" else: adapter_path = Path(request.adapter_path) - adapter_model = load_lora_adapter_state_dict( + adapter_model = load_lora_tensors_for_megatron( str(adapter_path), handler=runtime.model_support_handler, allow_unvalidated_arch=request.config.allow_unvalidated_arch, @@ -828,25 +908,14 @@ def _configure_worker_bundle(bundle: Any) -> None: 0, list(range(int(packed_tensors["tokens"].shape[0]))), ) - try: - logits = _run_logits_with_replay( - runtime=runtime, - packed_tensors=cast(dict[str, Any], packed_tensors), - global_grad_accumulation_sequences=request.global_grad_accumulation_sequences, - ) - if forward_trace_capture is not None and request.forward_trace_dir is not None: - trace_dir = Path(request.forward_trace_dir) - forward_trace_capture.save_current_step(trace_dir) - torch.save(logits.detach().cpu(), trace_dir / "logits.pt") - finally: - if forward_trace_capture is not None: - forward_trace_capture.close() - score = _extract_scores_from_logits( - logits=logits, + score = _score_megatron_runtime( + runtime=runtime, + packed_tensors=cast(dict[str, Any], packed_tensors), logical_map=logical_map, - side="megatron", weight_state=request.weight_state, - rollout_mode="native_lora", + global_grad_accumulation_sequences=request.global_grad_accumulation_sequences, + forward_trace_capture=forward_trace_capture, + forward_trace_dir=request.forward_trace_dir, ) if torch.distributed.get_rank() == 0: # type: ignore[possibly-missing-attribute] @@ -947,10 +1016,7 @@ async def run_real_path_train_inf_mismatch( artifact_dir: Path, ) -> RealPathTrainInfReport: import art - from art.megatron.routing_replay import ( - build_moe_routing_replay_bundle_from_packed_tensors, - ) - from art.megatron.runtime.backend import MegatronBackend + from art.megatron.backend import MegatronBackend from art.preprocessing.pack import packed_tensors_to_dir parity_config = config.output_parity @@ -974,23 +1040,23 @@ async def run_real_path_train_inf_mismatch( name=f"train-inf-real-{uuid.uuid4().hex[:8]}", project="train_inf_mismatch", base_model=parity_config.base_model, - _internal_config=art.dev.InternalModelConfig( - trainer_gpu_ids=parity_config.trainer_gpu_ids, - inference_gpu_ids=parity_config.inference_gpu_ids, - rollout_weights_mode="lora", - allow_unvalidated_arch=parity_config.allow_unvalidated_arch, - engine_args=art.dev.EngineArgs( - tensor_parallel_size=len(parity_config.inference_gpu_ids), - enable_expert_parallel=is_moe + _internal_config={ + "trainer_gpu_ids": parity_config.trainer_gpu_ids, + "inference_gpu_ids": parity_config.inference_gpu_ids, + "rollout_weights_mode": "lora", + "allow_unvalidated_arch": parity_config.allow_unvalidated_arch, + "engine_args": { + "tensor_parallel_size": len(parity_config.inference_gpu_ids), + "enable_expert_parallel": is_moe and len(parity_config.inference_gpu_ids) > 1, - max_model_len=parity_config.packed.sequence_length + 8, - max_logprobs=TOP_K, + "max_model_len": parity_config.packed.sequence_length + 8, + "max_logprobs": TOP_K, **parity_config.engine_args, - ), - init_args={ + }, + "init_args": { "max_seq_length": parity_config.packed.sequence_length, }, - ), + }, ) _move_adapter_to_step_zero(adapter_path=adapter_path, model=model, backend=backend) @@ -1022,8 +1088,9 @@ async def run_real_path_train_inf_mismatch( global_grad_accumulation_sequences = int(packed_tensors["tokens"].shape[0]) routing_replay_path: str | None = None if is_moe: - build_moe_routing_replay_bundle_from_packed_tensors( + _build_real_path_moe_routing_replay_bundle( packed_tensors=packed_tensors, + config=parity_config, global_grad_accumulation_sequences=global_grad_accumulation_sequences, ).to_dir(routing_replay_dir) routing_replay_path = str(routing_replay_dir) @@ -1037,7 +1104,6 @@ async def run_real_path_train_inf_mismatch( ) if is_moe: routing_replay = packed_tensors["moe_routing_replay"] - assert routing_replay is not None stats = routing_replay.pack_stats else: from art.preprocessing.moe_routing import MoeRoutingPackStats @@ -1108,10 +1174,14 @@ async def run_real_path_train_inf_mismatch( parity_config.base_model, allow_unvalidated_arch=parity_config.allow_unvalidated_arch, ) + top20_kl_limit = top20_kl_candidate_to_target_limit_for_model( + parity_config.base_model, + allow_unvalidated_arch=parity_config.allow_unvalidated_arch, + ) passed = ( comparison.mean_abs_pct <= mean_abs_pct_limit and topk_comparison.top20_intersection_kl_candidate_to_target - <= TOP20_KL_CANDIDATE_TO_TARGET_LIMIT + <= top20_kl_limit ) report = RealPathTrainInfReport( base_model=parity_config.base_model, @@ -1170,7 +1240,7 @@ async def run_real_path_train_inf_mismatch( stats.shared_prefix_compared_slots ), mean_abs_pct_limit=mean_abs_pct_limit, - top20_kl_candidate_to_target_limit=TOP20_KL_CANDIDATE_TO_TARGET_LIMIT, + top20_kl_candidate_to_target_limit=top20_kl_limit, passed=passed, ) _write_json( diff --git a/tests/integration/megatron/train_inf_mismatch/test_qwen35_vllm_lora_layout.py b/tests/integration/megatron/train_inf_mismatch/test_qwen35_vllm_lora_layout.py deleted file mode 100644 index 5fe449f44..000000000 --- a/tests/integration/megatron/train_inf_mismatch/test_qwen35_vllm_lora_layout.py +++ /dev/null @@ -1,227 +0,0 @@ -import torch - -from art.megatron.model_support.handlers import QWEN3_5_MOE_HANDLER - - -def _config(base_model: str, *, rank: int) -> dict: - return { - "base_model_name_or_path": base_model, - "r": rank, - "lora_alpha": rank, - "target_modules": [ - "in_proj_qkv", - "in_proj_z", - "out_proj", - "gate_proj", - "up_proj", - "down_proj", - ], - "bias": "none", - } - - -def _small_q_gate_config(*, rank: int) -> dict: - config = _config("Qwen/Qwen3.5-35B-A3B", rank=rank) - config.update( - { - "num_attention_heads": 4, - "num_key_value_heads": 2, - "head_dim": 3, - } - ) - return config - - -def _sentinel( - expert: int, - module_id: int, - lora_id: int, - shape: tuple[int, int], -) -> torch.Tensor: - return ( - torch.arange(shape[0] * shape[1], dtype=torch.float32).reshape(shape) - + expert * 10_000 - + module_id * 1_000 - + lora_id * 100 - ) - - -def _qwen35_art_moe_tensors( - prefix: str, - *, - num_experts: int, - rank: int, - hidden: int, - intermediate: int, -) -> dict[str, torch.Tensor]: - tensors: dict[str, torch.Tensor] = {} - module_ids = {"gate_up_proj": 1, "down_proj": 2} - for expert in range(num_experts): - for module, module_id in module_ids.items(): - in_dim = intermediate if module == "down_proj" else hidden - out_dim = hidden if module == "down_proj" else 2 * intermediate - module_prefix = f"{prefix}.mlp.experts.{expert}.{module}" - tensors[f"{module_prefix}.lora_A.weight"] = _sentinel( - expert, - module_id, - 0, - (rank, in_dim), - ) - tensors[f"{module_prefix}.lora_B.weight"] = _sentinel( - expert, - module_id, - 1, - (out_dim, rank), - ) - return tensors - - -def _q_proj_lora_b_to_vllm_expected( - tensor: torch.Tensor, - *, - num_heads: int, - num_groups: int, - head_dim: int, -) -> torch.Tensor: - heads_per_group = num_heads // num_groups - grouped = tensor.reshape(num_groups, 2 * heads_per_group, head_dim, tensor.shape[1]) - query = grouped[:, :heads_per_group] - gate = grouped[:, heads_per_group:] - return torch.cat((query, gate), dim=2).reshape(tensor.shape).contiguous() - - -def test_qwen35_q_proj_lora_b_translates_grouped_gate_layout() -> None: - rank = 2 - num_heads = 4 - num_groups = 2 - head_dim = 3 - rows = num_groups * 2 * (num_heads // num_groups) * head_dim - art_key = "base_model.model.model.layers.0.self_attn.q_proj.lora_B.weight" - vllm_key = ( - "base_model.model.model.language_model.layers.0.self_attn.q_proj.lora_B.weight" - ) - art_tensor = torch.arange(rows * rank, dtype=torch.float32).reshape(rows, rank) - adapter_config = _small_q_gate_config(rank=rank) - - vllm_tensors, vllm_config = QWEN3_5_MOE_HANDLER.to_vllm_lora_tensors( - {art_key: art_tensor}, - adapter_config=adapter_config, - ) - - assert vllm_config == adapter_config - assert torch.equal( - vllm_tensors[vllm_key], - _q_proj_lora_b_to_vllm_expected( - art_tensor, - num_heads=num_heads, - num_groups=num_groups, - head_dim=head_dim, - ), - ) - roundtrip = QWEN3_5_MOE_HANDLER.from_vllm_lora_tensors( - vllm_tensors, - adapter_config=adapter_config, - ) - assert torch.equal(roundtrip[art_key], art_tensor) - - -def test_qwen35_moe_layout_exports_vllm_3d_without_rank_rewrite() -> None: - rank = 2 - hidden = 3 - intermediate = 4 - num_experts = 4 - art_prefix = "base_model.model.model.layers.0" - vllm_prefix = "base_model.model.model.language_model.layers.0.mlp.experts" - art_tensors = _qwen35_art_moe_tensors( - art_prefix, - num_experts=num_experts, - rank=rank, - hidden=hidden, - intermediate=intermediate, - ) - - vllm_tensors, vllm_config = QWEN3_5_MOE_HANDLER.to_vllm_lora_tensors( - art_tensors, - adapter_config=_config("Qwen/Qwen3.5-35B-A3B", rank=rank), - ) - - assert vllm_config["r"] == rank - assert vllm_config["lora_alpha"] == rank - assert vllm_config["target_modules"] == [ - "in_proj_qkv", - "in_proj_z", - "out_proj", - "experts", - ] - assert set(vllm_tensors) == { - f"{vllm_prefix}.base_layer.lora_A.weight", - f"{vllm_prefix}.base_layer.lora_B.weight", - f"{vllm_prefix}.lora_A.weight", - f"{vllm_prefix}.lora_B.weight", - } - assert vllm_tensors[f"{vllm_prefix}.base_layer.lora_A.weight"].shape == ( - num_experts * rank, - hidden, - ) - assert vllm_tensors[f"{vllm_prefix}.base_layer.lora_B.weight"].shape == ( - 2 * intermediate, - num_experts * rank, - ) - assert vllm_tensors[f"{vllm_prefix}.lora_A.weight"].shape == ( - num_experts * rank, - intermediate, - ) - assert vllm_tensors[f"{vllm_prefix}.lora_B.weight"].shape == ( - hidden, - num_experts * rank, - ) - roundtrip = QWEN3_5_MOE_HANDLER.from_vllm_lora_tensors( - vllm_tensors, - adapter_config=vllm_config, - ) - assert set(roundtrip) == set(art_tensors) - for key, tensor in art_tensors.items(): - assert torch.equal(roundtrip[key], tensor), key - - -def test_qwen35_moe_path_keeps_dense_lora_rank_when_moe_is_present() -> None: - rank = 1 - num_heads = 4 - num_groups = 2 - head_dim = 3 - rows = num_groups * 2 * (num_heads // num_groups) * head_dim - art_prefix = "base_model.model.model.layers.0" - art_key = f"{art_prefix}.self_attn.q_proj.lora_B.weight" - vllm_key = ( - "base_model.model.model.language_model.layers.0.self_attn.q_proj.lora_B.weight" - ) - art_tensor = torch.arange(rows * rank, dtype=torch.float32).reshape(rows, rank) - art_tensors = { - **_qwen35_art_moe_tensors( - art_prefix, - num_experts=1, - rank=rank, - hidden=3, - intermediate=4, - ), - art_key: art_tensor, - } - - vllm_tensors, vllm_config = QWEN3_5_MOE_HANDLER.to_vllm_lora_tensors( - art_tensors, - adapter_config=_small_q_gate_config(rank=rank), - ) - - expected = _q_proj_lora_b_to_vllm_expected( - art_tensor, - num_heads=num_heads, - num_groups=num_groups, - head_dim=head_dim, - ) - assert vllm_config["r"] == rank - assert torch.equal(vllm_tensors[vllm_key], expected) - roundtrip = QWEN3_5_MOE_HANDLER.from_vllm_lora_tensors( - vllm_tensors, - adapter_config=vllm_config, - ) - assert torch.equal(roundtrip[art_key], art_tensor) diff --git a/tests/integration/megatron/train_inf_mismatch/workflow_stage.py b/tests/integration/megatron/train_inf_mismatch/workflow_stage.py index b04776c59..3977c3a94 100644 --- a/tests/integration/megatron/train_inf_mismatch/workflow_stage.py +++ b/tests/integration/megatron/train_inf_mismatch/workflow_stage.py @@ -8,6 +8,19 @@ from .artifacts import REPO_ROOT, TEST_ROOT, create_artifact_dir +DEFAULT_ATTEMPTS = 3 +MAX_ATTEMPTS = 5 + + +class TrainInfMismatchAttemptReport(BaseModel): + attempt: int + returncode: int + stdout_path: str + stderr_path: str + passed_count: int + failed_count: int + skipped_count: int + class TrainInfMismatchReport(BaseModel): base_model: str @@ -20,6 +33,9 @@ class TrainInfMismatchReport(BaseModel): passed_count: int failed_count: int skipped_count: int + attempt_count: int + max_attempts: int + attempts: list[TrainInfMismatchAttemptReport] def _pytest_counts(output: str) -> dict[str, int]: @@ -37,10 +53,17 @@ def _pytest_counts(output: str) -> dict[str, int]: return counts +def _attempt_limit() -> int: + raw = os.environ.get("ART_TRAIN_INF_MISMATCH_ATTEMPTS") + attempts = DEFAULT_ATTEMPTS if raw is None else int(raw) + if attempts < 1: + raise ValueError("ART_TRAIN_INF_MISMATCH_ATTEMPTS must be positive") + return min(attempts, MAX_ATTEMPTS) + + def run_train_inf_mismatch(*, base_model: str) -> TrainInfMismatchReport: artifact_dir = create_artifact_dir("workflow::train_inf_mismatch") - stdout_path = artifact_dir / "pytest_stdout.txt" - stderr_path = artifact_dir / "pytest_stderr.txt" + max_attempts = _attempt_limit() env = os.environ.copy() env["BASE_MODEL"] = base_model env["ART_RUN_TRAIN_INF_MISMATCH_LIVE"] = "1" @@ -53,34 +76,55 @@ def run_train_inf_mismatch(*, base_model: str) -> TrainInfMismatchReport: if not existing_pythonpath else f"{tests_dir}{os.pathsep}{existing_pythonpath}" ) - result = subprocess.run( - [ - sys.executable, - "-m", - "pytest", - "-q", - str(TEST_ROOT), - f"--ignore={TEST_ROOT / 'artifacts'}", - "--tb=short", - ], - cwd=Path(REPO_ROOT), - env=env, - capture_output=True, - text=True, - check=False, - ) - stdout_path.write_text(result.stdout, encoding="utf-8") - stderr_path.write_text(result.stderr, encoding="utf-8") - counts = _pytest_counts(result.stdout + "\n" + result.stderr) + attempts: list[TrainInfMismatchAttemptReport] = [] + selected: TrainInfMismatchAttemptReport | None = None + for attempt in range(1, max_attempts + 1): + stdout_path = artifact_dir / f"attempt_{attempt}_pytest_stdout.txt" + stderr_path = artifact_dir / f"attempt_{attempt}_pytest_stderr.txt" + result = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-q", + str(TEST_ROOT / "test_live_real_path_output_parity.py"), + "--tb=short", + ], + cwd=Path(REPO_ROOT), + env=env, + capture_output=True, + text=True, + check=False, + ) + stdout_path.write_text(result.stdout, encoding="utf-8") + stderr_path.write_text(result.stderr, encoding="utf-8") + counts = _pytest_counts(result.stdout + "\n" + result.stderr) + selected = TrainInfMismatchAttemptReport( + attempt=attempt, + returncode=result.returncode, + stdout_path=str(stdout_path), + stderr_path=str(stderr_path), + passed_count=counts["passed"], + failed_count=counts["failed"], + skipped_count=counts["skipped"], + ) + attempts.append(selected) + if result.returncode == 0: + break + if selected is None: + raise RuntimeError("train/inf mismatch retry loop did not run") return TrainInfMismatchReport( base_model=base_model, - passed=result.returncode == 0, - returncode=result.returncode, + passed=selected.returncode == 0, + returncode=selected.returncode, artifact_dir=str(artifact_dir), test_root=str(TEST_ROOT), - stdout_path=str(stdout_path), - stderr_path=str(stderr_path), - passed_count=counts["passed"], - failed_count=counts["failed"], - skipped_count=counts["skipped"], + stdout_path=selected.stdout_path, + stderr_path=selected.stderr_path, + passed_count=selected.passed_count, + failed_count=selected.failed_count, + skipped_count=selected.skipped_count, + attempt_count=len(attempts), + max_attempts=max_attempts, + attempts=attempts, ) diff --git a/tests/integration/megatron/trainability/yes_no_trainability.py b/tests/integration/megatron/trainability/yes_no_trainability.py index 8f4850505..46675535e 100644 --- a/tests/integration/megatron/trainability/yes_no_trainability.py +++ b/tests/integration/megatron/trainability/yes_no_trainability.py @@ -17,12 +17,12 @@ import art from art import dev from art.local import LocalBackend +from art.megatron.backend import MegatronBackend from art.megatron.model_support.registry import ( get_model_support_spec, model_uses_expert_parallel, ) from art.megatron.model_support.spec import RolloutWeightsMode -from art.megatron.runtime.backend import MegatronBackend from ..model_support.oracle_harness import Topology, oracle_topology from ..model_support.oracle_worker import provider_topology_env @@ -33,8 +33,8 @@ _TRAINABILITY_ROOT = ( Path(__file__).resolve().parents[4] / ".local" / "model_support_validation" ) -_SHARED_MEGATRON_TOPOLOGY = Topology(tp=2, ep=2, etp=1, dp=1, sp=True) -_DENSE_SHARED_MEGATRON_TOPOLOGY = Topology(tp=2, ep=1, etp=1, dp=1, sp=True) +_SHARED_MEGATRON_TOPOLOGY = Topology(tp=1, ep=2, etp=1, dp=1, cp=2, sp=False) +_DENSE_SHARED_MEGATRON_TOPOLOGY = Topology(tp=1, ep=1, etp=1, dp=1, cp=2, sp=False) _VARIANT_NAME = Literal[ "megatron_shared", "megatron_dedicated", @@ -309,6 +309,23 @@ def _wandb_disabled() -> Iterator[None]: os.environ[name] = value +@contextmanager +def _temporary_env(updates: dict[str, str] | None) -> Iterator[None]: + if not updates: + yield + return + saved = {name: os.environ.get(name) for name in updates} + os.environ.update(updates) + try: + yield + finally: + for name, value in saved.items(): + if value is None: + os.environ.pop(name, None) + else: + os.environ[name] = value + + def _artifact_dir(base_model: str, variant_name: _VARIANT_NAME) -> Path: path = ( _TRAINABILITY_ROOT / _slugify(base_model) / variant_name / uuid.uuid4().hex[:8] @@ -454,8 +471,9 @@ async def _backend_context( variant: _TrainabilityVariant, *, backend_root: Path, + extra_env: dict[str, str] | None = None, ) -> AsyncIterator[LocalBackend | MegatronBackend]: - with _wandb_disabled(): + with _wandb_disabled(), _temporary_env(extra_env): topology_context = ( provider_topology_env(variant.topology) if variant.topology is not None @@ -649,6 +667,7 @@ async def run_yes_no_trainability_async( artifact_root: Path | None = None, rollout_weights_mode: RolloutWeightsMode | None = None, allow_unvalidated_arch: bool = False, + extra_env: dict[str, str] | None = None, ) -> YesNoTrainabilityReport: variant = _build_variant( variant_name, @@ -679,7 +698,9 @@ async def run_yes_no_trainability_async( ) train_kwargs = _variant_train_kwargs(variant) - async with _backend_context(variant, backend_root=backend_root) as backend: + async with _backend_context( + variant, backend_root=backend_root, extra_env=extra_env + ) as backend: await model.register(backend) output_dir = Path(model.base_path) / model.project / "models" / model.name await _warmup_model(model, base_model=base_model, prompt=prompts[0]) diff --git a/tests/integration/megatron/weight_offload/__init__.py b/tests/integration/megatron/weight_offload/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/tests/integration/megatron/weight_offload/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/integration/megatron/weight_offload/test_streaming_offload_oracle.py b/tests/integration/megatron/weight_offload/test_streaming_offload_oracle.py new file mode 100644 index 000000000..88c6fc7a5 --- /dev/null +++ b/tests/integration/megatron/weight_offload/test_streaming_offload_oracle.py @@ -0,0 +1,103 @@ +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path +from typing import Callable + +import pytest + +from art.megatron.training.streaming_weight_offload import StreamingWeightOffloadConfig + +from ..model_support.oracle_harness import ( + LIVE_TRAINING_LOG_PATH, + MetricThresholdRule, + PhasePassFn, + Topology, + VariantRunner, + VariantSpec, + available_gpu_count, + case_config, +) + +REPO_ROOT = Path(__file__).resolve().parents[4] +STREAMING_OFFLOAD_LOG_PATH = REPO_ROOT / ".local" / "streaming_weight_offload.log" +STREAMING_OFFLOAD_TOPOLOGY = Topology(tp=1, ep=2, etp=1, dp=1, cp=2, sp=False) + + +def _run_with_log(*, log_path: Path, run: Callable[[], object]) -> None: + log_path.parent.mkdir(parents=True, exist_ok=True) + LIVE_TRAINING_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + LIVE_TRAINING_LOG_PATH.write_text("", encoding="utf-8") + with log_path.open("w", encoding="utf-8") as log_file: + with redirect_stdout(log_file), redirect_stderr(log_file): + run() + + +def _phase_pass_fns() -> dict[str, PhasePassFn]: + tensor_rule = MetricThresholdRule(limits={"mean_abs_pct": 1.0}) + exact_tensor = MetricThresholdRule(limits={"relative_l2": 0.0, "mean_abs_pct": 0.0}) + exact_topk = MetricThresholdRule( + limits={"topk_mismatch_fraction": 0.0, "top1_mismatch_fraction": 0.0} + ) + return { + "forward": tensor_rule, + "outputs": tensor_rule, + "losses": tensor_rule, + "grads": tensor_rule, + "deltas": tensor_rule, + "router_scores": exact_tensor, + "router_topk_ids": exact_topk, + } + + +def test_streaming_weight_offload_matches_no_offload_oracle( + capsys: pytest.CaptureFixture[str], +) -> None: + with capsys.disabled(): + print(f"\nStreaming weight offload oracle log: {STREAMING_OFFLOAD_LOG_PATH}") + print(f"Megatron live training log: {LIVE_TRAINING_LOG_PATH}") + + gpu_count = available_gpu_count() + required_gpus = STREAMING_OFFLOAD_TOPOLOGY.world_size() + if gpu_count < required_gpus: + STREAMING_OFFLOAD_LOG_PATH.parent.mkdir(parents=True, exist_ok=True) + STREAMING_OFFLOAD_LOG_PATH.write_text( + ( + "Streaming weight offload oracle skipped. " + f"Need {required_gpus} GPUs, found {gpu_count}.\n" + ), + encoding="utf-8", + ) + pytest.skip( + "Need " + f"{required_gpus} GPUs for streaming weight offload oracle, found {gpu_count}" + ) + + config = case_config().model_copy(update={"precision": "bf16", "num_layers": 8}) + runner = VariantRunner( + case_config=config, + oracle_topology_override=STREAMING_OFFLOAD_TOPOLOGY, + oracle_slug_override="rl__cp2_ep2_no_streaming_weight_offload", + oracle_offload_between_jobs=True, + oracle_streaming_weight_offload=StreamingWeightOffloadConfig(enabled=False), + oracle_flex_backend=None, + variant_flex_backend=None, + use_fp32_lora_reference=False, + ) + variant = VariantSpec( + name="streaming_weight_offload_resident2_slots4", + topology=STREAMING_OFFLOAD_TOPOLOGY, + output_slug="rl__cp2_ep2_streaming_weight_offload_resident2_slots4", + reference_slug=runner.oracle_slug, + pass_fn_by_phase=_phase_pass_fns(), + offload_between_jobs=True, + streaming_weight_offload=StreamingWeightOffloadConfig( + enabled=True, + num_layers=8, + resident_layers=2, + num_slots=4, + ), + ) + + _run_with_log( + log_path=STREAMING_OFFLOAD_LOG_PATH, + run=lambda: runner.run_suite([variant]), + ) diff --git a/tests/integration/megatron/weight_offload/test_streaming_offload_trainability.py b/tests/integration/megatron/weight_offload/test_streaming_offload_trainability.py new file mode 100644 index 000000000..ec163ec9b --- /dev/null +++ b/tests/integration/megatron/weight_offload/test_streaming_offload_trainability.py @@ -0,0 +1,76 @@ +import json +import os +from pathlib import Path + +import pytest + +from ..trainability.yes_no_trainability import run_yes_no_trainability_async + +torch = pytest.importorskip("torch") + +DEFAULT_BASE_MODEL = "Qwen/Qwen3.5-35B-A3B" +LIVE_ENV = "ART_RUN_LIVE_YES_NO_TRAINABILITY" + + +def _require_opt_in() -> None: + if os.environ.get(LIVE_ENV) != "1": + pytest.skip(f"set {LIVE_ENV}=1 to run live yes/no trainability validation") + + +def _base_model() -> str: + return os.environ.get( + "ART_LIVE_YES_NO_BASE_MODEL", + os.environ.get("BASE_MODEL", DEFAULT_BASE_MODEL), + ) + + +def _assert_passed(report) -> None: + assert report.saturated_step is not None + assert report.saturated_step > 0 + assert report.initial_eval_reward < report.reward_threshold + assert report.final_eval_reward is not None + assert report.final_eval_reward >= report.reward_threshold + assert report.final_eval_reward > report.initial_eval_reward + assert report.latest_step > 0 + assert report.step0_name in report.model_ids_before + assert report.latest_name in report.model_ids_after + if report.rollout_weights_mode == "merged": + assert report.step0_name not in report.model_ids_after + else: + assert report.step0_name in report.model_ids_after + assert report.latest_snapshot["has_logprobs"] is True + + +def _write_report(artifact_dir: Path, name: str, report) -> None: + (artifact_dir / name).write_text( + json.dumps(report.model_dump(mode="json"), indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + +@pytest.mark.skipif( + not torch.cuda.is_available() or torch.cuda.device_count() < 2, + reason="Need at least 2 CUDA GPUs for live streaming offload trainability", +) +@pytest.mark.asyncio +async def test_megatron_dedicated_streaming_offload_yes_no_trainability_live( + artifact_dir: Path, +) -> None: + _require_opt_in() + report = await run_yes_no_trainability_async( + base_model=_base_model(), + variant_name="megatron_dedicated", + artifact_root=artifact_dir / "megatron_dedicated_streaming_offload_workspace", + extra_env={ + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD": "1", + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_NUM_LAYERS": "8", + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_RESIDENT_LAYERS": "2", + "ART_MEGATRON_STREAMING_WEIGHT_OFFLOAD_NUM_SLOTS": "4", + }, + ) + _write_report( + artifact_dir, + "megatron_dedicated_streaming_offload_yes_no_trainability.json", + report, + ) + _assert_passed(report) diff --git a/tests/unit/test_moe_routing_replay.py b/tests/unit/test_moe_routing_replay.py index ca16abc21..2f2febb66 100644 --- a/tests/unit/test_moe_routing_replay.py +++ b/tests/unit/test_moe_routing_replay.py @@ -321,6 +321,7 @@ def test_controller_uses_native_router_replay_target_indices() -> None: controller.install_router_patches([chunk]) controller.set_step(step_index=0, sample_index=[0]) controller.begin_micro(0, 0) + controller.set_local_input_token_uids(torch.arange(4, dtype=torch.int64)) _probs, routing_map = router.routing(torch.randn((4, 3), dtype=torch.float32)) expected_map = torch.zeros((4, 3), dtype=torch.bool) @@ -333,6 +334,74 @@ def test_controller_uses_native_router_replay_target_indices() -> None: controller.remove_router_patches() +def test_controller_explicit_token_uids_refresh_native_router_replay() -> None: + bundle, route = _make_bundle() + controller = MoeRoutingReplayController(bundle=bundle, strict=True, device="cpu") + chunk = _FakeChunk() + router = _fake_chunk_router(chunk) + replay = cast(_FakeRouterReplay, router.router_replay) + + controller.install_router_patches([chunk]) + controller.set_step(step_index=0, sample_index=[0]) + controller.begin_micro(0, 0) + controller.set_local_input_token_uids(torch.tensor([3, 1], dtype=torch.int64)) + assert replay.targets_seen == [] + _probs, routing_map = router.routing(torch.randn((2, 3), dtype=torch.float32)) + + expected_indices = route.expert_indices.index_select( + 0, torch.tensor([3, 1], dtype=torch.long) + ) + expected_map = torch.zeros((2, 3), dtype=torch.bool) + rows = torch.arange(2).unsqueeze(1) + expected_map[rows, expected_indices.to(torch.long)] = True + _assert_target(replay, expected_indices, index=0) + assert torch.equal(routing_map.cpu(), expected_map) + + controller.finalize_step() + controller.remove_router_patches() + + +def test_controller_switches_prestaged_layout_targets() -> None: + bundle, route = _make_bundle() + controller = MoeRoutingReplayController(bundle=bundle, strict=True, device="cpu") + chunk = _FakeChunk() + router = _fake_chunk_router(chunk) + replay = cast(_FakeRouterReplay, router.router_replay) + + controller.install_router_patches([chunk]) + controller.set_step(step_index=0, sample_index=[0]) + controller.begin_micro(0, 0) + controller.prepare_micro_targets( + { + "attention": torch.tensor([0, 1], dtype=torch.int64), + "gdn": torch.tensor([3, 1], dtype=torch.int64), + } + ) + assert replay.targets_seen == [] + + _probs, attention_map = router.routing(torch.randn((2, 3), dtype=torch.float32)) + controller.set_active_token_uid_key("gdn") + _probs, gdn_map = router.routing(torch.randn((2, 3), dtype=torch.float32)) + + attention_indices = route.expert_indices.index_select( + 0, torch.tensor([0, 1], dtype=torch.long) + ) + gdn_indices = route.expert_indices.index_select( + 0, torch.tensor([3, 1], dtype=torch.long) + ) + _assert_target(replay, attention_indices, index=0) + _assert_target(replay, gdn_indices, index=1) + assert torch.equal( + attention_map.cpu(), _expected_routing_map(_make_route([[0, 2], [1, 0]])) + ) + assert torch.equal( + gdn_map.cpu(), _expected_routing_map(_make_route([[1, 0], [1, 0]])) + ) + + controller.finalize_step() + controller.remove_router_patches() + + def test_controller_finalize_fails_when_unconsumed_calls_remain() -> None: bundle, _route = _make_bundle() controller = MoeRoutingReplayController(bundle=bundle, strict=True, device="cpu") @@ -353,11 +422,13 @@ def test_controller_reuses_route_for_recompute_with_same_active_micro() -> None: controller.set_step(step_index=0, sample_index=[0, 1]) controller.begin_micro(0, 0) + controller.set_local_input_token_uids(torch.arange(2, dtype=torch.int64)) _probs, routing_map = router.routing(torch.randn((2, 3), dtype=torch.float32)) _probs, recompute_routing_map = router.routing( torch.randn((2, 3), dtype=torch.float32) ) controller.begin_micro(1, 1) + controller.set_local_input_token_uids(torch.arange(2, dtype=torch.int64)) _probs, next_routing_map = router.routing(torch.randn((2, 3), dtype=torch.float32)) calls = bundle.steps[0].routers[bundle.router_keys[0]].calls diff --git a/tests/unit/test_pipeline_trainer_local_backend.py b/tests/unit/test_pipeline_trainer_local_backend.py index 65a9db9d6..fd0fc530c 100644 --- a/tests/unit/test_pipeline_trainer_local_backend.py +++ b/tests/unit/test_pipeline_trainer_local_backend.py @@ -1,4 +1,5 @@ import asyncio +from contextlib import asynccontextmanager from datetime import datetime, timezone import json from pathlib import Path @@ -289,7 +290,7 @@ def strategy(context: CheckpointRetentionContext) -> set[int]: ) trainer.state.completed_eval_steps = {2, 3} trainer._checkpoint_lease_counts[3] = 1 - trainer._scheduled_eval_steps.add(4) + trainer._checkpoint_lease_counts[4] = 1 await trainer._run_checkpoint_retention(5) @@ -492,7 +493,7 @@ def reload_model_params(self) -> None: optimizer = FakeOptimizer() adapter_model = {"weight": torch.tensor([1.0])} - load_adapter_into_model([module], adapter_model, optimizer) + load_adapter_into_model(cast(Any, [module]), adapter_model, optimizer) assert module.loaded_adapter is adapter_model assert optimizer.reload_calls == 1 @@ -615,3 +616,72 @@ async def test_local_backend_adapter_lease_pins_inference_name_and_prune( assert backend._model_inference_name(model) == f"{model.name}@5" service.prune_loaded_adapters.assert_awaited_once_with(retain_steps={3, 4, 5}) + + +@pytest.mark.asyncio +async def test_local_backend_adapter_retention_lease_does_not_pin_inference( + tmp_path: Path, +) -> None: + model = TrainableModel( + name="local-backend-adapter-retention-lease", + project="pipeline-tests", + base_model="test-model", + base_path=str(tmp_path), + _internal_config=InternalModelConfig( + trainer_gpu_ids=[0], + inference_gpu_ids=[1], + ), + ) + backend = LocalBackend(path=str(tmp_path)) + service = SimpleNamespace( + _latest_step=5, + prune_loaded_adapters=AsyncMock(), + ) + backend._services[model.name] = cast(Any, service) + + async with backend.adapter_retention_lease(model, 3): + assert backend._model_inference_name(model) == f"{model.name}@5" + await backend.prune_model_adapters(model, retain_steps={5}) + + service.prune_loaded_adapters.assert_awaited_once_with(retain_steps={3, 5}) + + +@pytest.mark.asyncio +async def test_pipeline_trainer_scheduled_eval_holds_retention_lease( + tmp_path: Path, +) -> None: + model = TrainableModel( + name="pipeline-scheduled-eval-lease", + project="pipeline-tests", + base_model="test-model", + base_path=str(tmp_path), + ) + + class BackendWithRetentionLease: + def __init__(self) -> None: + self.active_steps: set[int] = set() + + @asynccontextmanager + async def adapter_retention_lease(self, _model: TrainableModel, step: int): + self.active_steps.add(step) + try: + yield + finally: + self.active_steps.discard(step) + + backend = BackendWithRetentionLease() + trainer = _make_trainer(model=model, backend=backend) + trainer._eval_queue = asyncio.Queue() + + await trainer._schedule_eval_step(7) + + assert trainer._scheduled_eval_steps == {7} + assert backend.active_steps == {7} + assert trainer._protected_checkpoint_steps(8) == {7, 8} + assert await trainer._eval_queue.get() == 7 + + await trainer._release_scheduled_eval_lease(7) + + assert trainer._scheduled_eval_steps == set() + assert backend.active_steps == set() + assert trainer._protected_checkpoint_steps(8) == {8} diff --git a/uv.lock b/uv.lock index 3b91efca3..a81c9e699 100644 --- a/uv.lock +++ b/uv.lock @@ -2,12 +2,18 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and sys_platform == 'linux'", - "python_full_version < '3.13' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform != 'linux'", - "python_full_version == '3.13.*' and sys_platform != 'linux'", - "python_full_version < '3.13' and sys_platform != 'linux'", + "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine != 's390x' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and platform_machine != 's390x' and sys_platform != 'linux'", + "python_full_version < '3.13' and platform_machine != 's390x' and sys_platform != 'linux'", + "python_full_version >= '3.14' and platform_machine == 's390x' and sys_platform != 'linux'", + "python_full_version == '3.13.*' and platform_machine == 's390x' and sys_platform != 'linux'", + "python_full_version < '3.13' and platform_machine == 's390x' and sys_platform != 'linux'", ] [manifest] @@ -16,8 +22,10 @@ overrides = [ { name = "megatron-core", specifier = "==0.17.0" }, { name = "numpy", specifier = "<2" }, { name = "nvidia-resiliency-ext", specifier = "<0.5" }, - { name = "quack-kernels", specifier = "==0.2.5" }, + { name = "quack-kernels", specifier = "==0.3.7" }, + { name = "torch", specifier = "==2.11.0", index = "https://download.pytorch.org/whl/cu128" }, { name = "transformer-engine", specifier = "==2.11.0" }, + { name = "transformers", specifier = "==5.2.0" }, ] excludes = [ "emerging-optimizers", @@ -35,7 +43,7 @@ version = "1.2.1+9af0e0d" [[manifest.dependency-metadata]] name = "transformer-engine-torch" -version = "0.5.18" +version = "2.11.0" requires-dist = ["einops", "onnx", "onnxscript", "packaging", "pydantic", "torch", "transformer-engine-cu12"] [[package]] @@ -79,14 +87,14 @@ wheels = [ [[package]] name = "aiodns" -version = "4.0.0" +version = "4.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycares" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/da/97235e953109936bfeda62c1f9f1a7c5652d4dc49f2b5911f9ae1043afa9/aiodns-4.0.0.tar.gz", hash = "sha256:17be26a936ba788c849ba5fd20e0ba69d8c46e6273e846eb5430eae2630ce5b1", size = 26204, upload-time = "2026-01-10T22:33:27.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/22/a2d928e0e42baad0471d12ec44c71152ac870486e8298dddb2893b888c29/aiodns-4.0.4.tar.gz", hash = "sha256:cb10e0c0d2591636716ad2fe402e977c16d71bdaf76bb8cb49e8a6633596f736", size = 29918, upload-time = "2026-05-20T01:54:15.557Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/60/14ac40c03e8a26216e4f2642497b776e52f9e3214e4fd537628829bbb082/aiodns-4.0.0-py3-none-any.whl", hash = "sha256:a188a75fb8b2b7862ac8f84811a231402fb74f5b4e6f10766dc8a4544b0cf989", size = 11334, upload-time = "2026-01-10T22:33:25.65Z" }, + { url = "https://files.pythonhosted.org/packages/7f/70/72e4ab117425ccdc4d10bd523a94c1baa051a15586057d64a4c6888f9e3f/aiodns-4.0.4-py3-none-any.whl", hash = "sha256:c24dd605bac70a1676ce503f967a98483ff163507198557d8e9db16267e6cfd2", size = 12696, upload-time = "2026-05-20T01:54:14.134Z" }, ] [[package]] @@ -100,16 +108,16 @@ wheels = [ [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, ] [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -120,76 +128,76 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, ] [package.optional-dependencies] @@ -274,38 +282,38 @@ sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d [[package]] name = "anyio" -version = "4.12.1" +version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] name = "apache-tvm-ffi" -version = "0.1.9" +version = "0.1.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/60/1e787a0b5ebf318483235be2a689ee367173983067e441b8379564f667c0/apache_tvm_ffi-0.1.9.tar.gz", hash = "sha256:d2d402587e8906de0a07f4746aa78f3d452c7efe3625d4bb39ac2ad693bce530", size = 2513731, upload-time = "2026-02-27T19:28:06.602Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/3d/4b9226cd45aa800a6904603dda9b323d728f3c3869952a673f3483b78b19/apache_tvm_ffi-0.1.11.tar.gz", hash = "sha256:153cd2c5a9717804cb0bcd9b2709f22a1e5f80ed05b5a490faf5949b136eedba", size = 2798354, upload-time = "2026-05-04T17:48:43.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/f2/b8c4b151169f6d7ba8773c8af68b2e0c1013d7fb3f1bdf87573f47157ce9/apache_tvm_ffi-0.1.9-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:49e52350b0470654847de752e65603b604a4d3323e7e9f5e8a982f44acc4c143", size = 2041756, upload-time = "2026-02-27T19:27:23.931Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c0/6d3d54f50012255b41bc3e24944c086f63c4707c8686c7c6780e9283eb96/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d503029e66c43b1a1cb1a42a1e9bb428c8a28dcbdec31c28e705472ca648a3a", size = 2203712, upload-time = "2026-02-27T19:27:25.867Z" }, - { url = "https://files.pythonhosted.org/packages/c6/dd/2bab4c6cd86257dbf99e93452a1af833113f8dc3e25a25579f6e4e4c8a94/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28241371934ea8af10d5067087ba1229ebddded7b2c02d33a258ec2a96df8c46", size = 2299704, upload-time = "2026-02-27T19:27:27.477Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4a/b469bcb2e1014cb84d336d2a59f42958a058251c577a4c2680cacad346e2/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87cacce81df55685fc6a76e1e3c5db1200e85e87bf5974b692c59d131b7bc622", size = 2130865, upload-time = "2026-02-27T19:27:29.092Z" }, - { url = "https://files.pythonhosted.org/packages/70/ef/5402da5d37f5270fd88ea0348acca78dba9be8bdbf6c2bcae0935eb03ef1/apache_tvm_ffi-0.1.9-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f45eb43499acac45ff6c93564f0ff2d3ca27b69656d540fd56ce59d51c0b4c65", size = 2278991, upload-time = "2026-02-27T19:27:30.729Z" }, - { url = "https://files.pythonhosted.org/packages/b5/23/1b7dc5f0807f83098183a57db6ee85b2c93b646d74a6e03781c9208aaeb0/apache_tvm_ffi-0.1.9-cp312-abi3-win_amd64.whl", hash = "sha256:d1dcf4c041d5ec05e3da1d545800c33cdbb95c113baa7705085ff79fa262752b", size = 1973200, upload-time = "2026-02-27T19:27:32.367Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1e/991ae65e64ce132c1ba665562db6638f5696d6133f580e20c653de33b9af/apache_tvm_ffi-0.1.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c3349f72ddb8ce206472d0380a729f213017a2180707096f8d57114b81097dd1", size = 2072944, upload-time = "2026-02-27T19:27:34.261Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a7/1e0643949e683fb3cfababd87058c0cfef122d1a3bb6ce703f719051b842/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d1f4d2b7ec7b1213632e9a104e9330bfc3dec48decffa62114c33aa188c9f43a", size = 2215954, upload-time = "2026-02-27T19:27:35.872Z" }, - { url = "https://files.pythonhosted.org/packages/d6/06/5016191ab61d2db4c3a7d754a3c1184e0836f575a7d08491669738c5e4b9/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4f01d16ba53fe118e363f7257253f07003797e4abe6fc9567f23b6a930dbff2", size = 2307291, upload-time = "2026-02-27T19:27:37.527Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f5/40bf0667330938efbfc0a51743cc53c79e41b4ece1a8abad3076192c9674/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c0581dd6bfbce7b017ef85cfda08bbe38891cc4b3afbcfaa8bc2d383728e426", size = 2143850, upload-time = "2026-02-27T19:27:40.437Z" }, - { url = "https://files.pythonhosted.org/packages/72/4a/421cbd4ed32e8bad3b88af3e8fa145c1f6f493bdd05be15b6f2d9b3cb7d6/apache_tvm_ffi-0.1.9-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dfa14be2a49347791ef21222a8225ce7f99bfec17104a676cb4f1bf3a107088", size = 2289038, upload-time = "2026-02-27T19:27:41.972Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1a/c8923d819b49872a612033b90d29299c0be73a7cbed1ddb3dc78dfe5e9f1/apache_tvm_ffi-0.1.9-cp314-cp314t-win_amd64.whl", hash = "sha256:a42d7ca27dce83efbdce7ec970fe3e773a69c31d928730ee5d9badb1229d106c", size = 2039007, upload-time = "2026-02-27T19:27:43.618Z" }, + { url = "https://files.pythonhosted.org/packages/05/9d/0f81ca556e5836b3ca64818cdae3f47dc7822bd35d22ddef7a54106d801d/apache_tvm_ffi-0.1.11-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:6ae51cc7df415b5f373a9df4baa1165a65608e519bea81e7dd23428f00eeb689", size = 2418793, upload-time = "2026-05-04T17:47:57.879Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a9/f48e5dd4ae1f6f0c5ffac259c0a9531b7d6a7c0a4c45bc2229d55de6adf8/apache_tvm_ffi-0.1.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da2c8d07fdc737d1ba75f4de25c29f156905b9dc980f1da90c395b4db525f522", size = 2605176, upload-time = "2026-05-04T17:47:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/36/99/2848df4e8ed5bf51df1d286d1718510584fa61e88adbc9c5b23d71b38f7c/apache_tvm_ffi-0.1.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:78aa1857b04a2ea718317041ab3f01288b3d496e6036eb1b99ebdc9da0fdaef5", size = 2725887, upload-time = "2026-05-04T17:48:01.381Z" }, + { url = "https://files.pythonhosted.org/packages/7d/80/963c991934a4eb0fa0c0178f51963333fe14a96b732009da642b6bf6b42e/apache_tvm_ffi-0.1.11-cp312-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a8b845c8dff498fb981c1dda36c954549204191b485a385845e604966594d0b2", size = 2513121, upload-time = "2026-05-04T17:48:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/4d/18/95569107ee83619d61a3bb0d28743a0599f85c5161981e3e098c82c2b185/apache_tvm_ffi-0.1.11-cp312-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2843f084cdc94dedacd8b257a395a2b71b8a3dc7fc99711b148bf1d161983128", size = 2697683, upload-time = "2026-05-04T17:48:05.222Z" }, + { url = "https://files.pythonhosted.org/packages/dc/99/f352cf1cce8f6f05584c4adf11de9eca07e6d217229bad6af35fb372926c/apache_tvm_ffi-0.1.11-cp312-abi3-win_amd64.whl", hash = "sha256:bd67e03759d25ff59f4e0ed9c8630a16872afc9dd8792f46ac3c927554015e60", size = 2365545, upload-time = "2026-05-04T17:48:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/27/ae/09242a668eb75ea06282d7cdc3947004cda69040885c340005a23b0aefe3/apache_tvm_ffi-0.1.11-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f47435e41bf8a2018ef126fad41f18e0c8fe8be4d25fb3ed04b615278b7806d4", size = 2481373, upload-time = "2026-05-04T17:48:09.388Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d1/dc0c26cf68635a1184ba39cccb6cb3cf9675c7030f135f47205e56bdd2b6/apache_tvm_ffi-0.1.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a05b36530d7cd5bb93b1a21a3b81ff060968c20456c4870b1a80d65966d5114f", size = 2639857, upload-time = "2026-05-04T17:48:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ad/4e3d4c5ec36e2ecadf6e5eb81cde065c69218cf722606b73af0ea6fdab75/apache_tvm_ffi-0.1.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9b158f93bdfc497ead9fce5ffdd4d132708de60970ffc97d890dd62fa39d9fb4", size = 2755683, upload-time = "2026-05-04T17:48:13.016Z" }, + { url = "https://files.pythonhosted.org/packages/51/37/54deceea6bac0e93844bd572a2fae8549e86e6309c732a0acaeb07a88c6b/apache_tvm_ffi-0.1.11-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f77406e2773ad18109369417b5ccf6aee3c813867dbd5d2d97170bfa7b491f1", size = 2552014, upload-time = "2026-05-04T17:48:14.692Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/52c9544be5850c7c0e5edce08f2dc9d05c3ecb10b7ae9b3a9313d1b2857e/apache_tvm_ffi-0.1.11-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78f0c9dc69727665de58faebacf6a3f4a1d75a355591e963e1bc691fc9bf5cd5", size = 2730358, upload-time = "2026-05-04T17:48:16.39Z" }, + { url = "https://files.pythonhosted.org/packages/93/b2/afe8a6b8553f51255afdd8063c5d6fd3f4e1978aad424de706440c59fdba/apache_tvm_ffi-0.1.11-cp314-cp314t-win_amd64.whl", hash = "sha256:2f5d417da48dbabbe08933a4d0964b3d2f43d1a4a2c3a6c0092de670c71a8a87", size = 2476516, upload-time = "2026-05-04T17:48:18.022Z" }, ] [[package]] @@ -385,43 +393,31 @@ wheels = [ [[package]] name = "av" -version = "15.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/c3/83e6e73d1592bc54436eae0bc61704ae0cff0c3cfbde7b58af9ed67ebb49/av-15.1.0.tar.gz", hash = "sha256:39cda2dc810e11c1938f8cb5759c41d6b630550236b3365790e67a313660ec85", size = 3774192, upload-time = "2025-08-30T04:41:56.076Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/58/de78b276d20db6ffcd4371283df771721a833ba525a3d57e753d00a9fe79/av-15.1.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:40c5df37f4c354ab8190c6fd68dab7881d112f527906f64ca73da4c252a58cee", size = 21760991, upload-time = "2025-08-30T04:40:00.801Z" }, - { url = "https://files.pythonhosted.org/packages/56/cc/45f85775304ae60b66976360d82ba5b152ad3fd91f9267d5020a51e9a828/av-15.1.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:af455ce65ada3d361f80c90c810d9bced4db5655ab9aa513024d6c71c5c476d5", size = 26953097, upload-time = "2025-08-30T04:40:03.998Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/2d781e5e71d02fc829487e775ccb1185e72f95340d05f2e84eb57a11e093/av-15.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86226d2474c80c3393fa07a9c366106029ae500716098b72b3ec3f67205524c3", size = 38319710, upload-time = "2025-08-30T04:40:07.701Z" }, - { url = "https://files.pythonhosted.org/packages/ac/13/37737ef2193e83862ccacff23580c39de251da456a1bf0459e762cca273c/av-15.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:11326f197e7001c4ca53a83b2dbc67fd39ddff8cdf62ce6be3b22d9f3f9338bd", size = 39915519, upload-time = "2025-08-30T04:40:11.066Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e8032c7b8f2a4129a03f63f896544f8b7cf068e2db2950326fa2400d5c47/av-15.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a631ea879cc553080ee62874f4284765c42ba08ee0279851a98a85e2ceb3cc8d", size = 40286166, upload-time = "2025-08-30T04:40:14.561Z" }, - { url = "https://files.pythonhosted.org/packages/e2/23/612c0fd809444d04b8387a2dfd942ccc77829507bd78a387ff65a9d98c24/av-15.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8f383949b010c3e731c245f80351d19dc0c08f345e194fc46becb1cb279be3ff", size = 41150592, upload-time = "2025-08-30T04:40:17.951Z" }, - { url = "https://files.pythonhosted.org/packages/15/74/6f8e38a3b0aea5f28e72813672ff45b64615f2c69e6a4a558718c95edb9f/av-15.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d5921aa45f4c1f8c1a8d8185eb347e02aa4c3071278a2e2dd56368d54433d643", size = 31336093, upload-time = "2025-08-30T04:40:21.393Z" }, - { url = "https://files.pythonhosted.org/packages/2e/bc/78b2ffa8235eeffc29aa4a8cc47b02e660cfec32f601f39a00975fb06d0e/av-15.1.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2f77853c3119c59d1bff4214ccbe46e3133eccff85ed96adee51c68684443f4e", size = 21726244, upload-time = "2025-08-30T04:40:24.14Z" }, - { url = "https://files.pythonhosted.org/packages/1a/99/66d69453a2dce028e6e8ebea085d90e880aac03d3a3ab7d8ec16755ffd75/av-15.1.0-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:c0bc4471c156a0a1c70a607502434f477bc8dfe085eef905e55b4b0d66bcd3a5", size = 26918663, upload-time = "2025-08-30T04:40:27.557Z" }, - { url = "https://files.pythonhosted.org/packages/fa/51/1a7dfbeda71f2772bc46d758af0e7fab1cc8388ce4bc7f24aecbc4bfd764/av-15.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:37839d4fa1407f047af82560dfc0f94d8d6266071eff49e1cbe16c4483054621", size = 38041408, upload-time = "2025-08-30T04:40:30.811Z" }, - { url = "https://files.pythonhosted.org/packages/d7/97/2c4e0288ad4359b6064cb06ae79c2ff3a84ac73d27e91f2161b75fcd86fa/av-15.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:729179cd8622815e8b6f6854d13a806fe710576e08895c77e5e4ad254609de9a", size = 39642563, upload-time = "2025-08-30T04:40:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/ea/94/2362502149e276d00957edabcc201a5f4d5109a8a7b4fd30793714a532f3/av-15.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4abdf085bfa4eec318efccff567831b361ea56c045cc38366811552e3127c665", size = 40022119, upload-time = "2025-08-30T04:40:37.703Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/1a0ce1b3835d9728da0a7a54aeffaa0a2b1a88405eaed9322efd55212a54/av-15.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f985661644879e4520d28a995fcb2afeb951bc15a1d51412eb8e5f36da85b6fe", size = 40885158, upload-time = "2025-08-30T04:40:40.952Z" }, - { url = "https://files.pythonhosted.org/packages/30/e6/054bb64e424d90b77ed5fc6a7358e4013fb436154c998fc90a89a186313f/av-15.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:7d7804a44c8048bb4b014a99353dd124663a12cd1d4613ba2bd3b457c3b1d539", size = 31312256, upload-time = "2025-08-30T04:40:44.224Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/89eae6dca10d7d2b83c131025a31ccc750be78699ac0304439faa1d1df99/av-15.1.0-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:5dd73c6447947edcb82e5fecf96e1f146aeda0f169c7ad4c54df4d9f66f63fde", size = 21730645, upload-time = "2025-08-30T04:40:47.259Z" }, - { url = "https://files.pythonhosted.org/packages/a3/f0/abffaf69405ed68041524be12a1e294faf396971d6a0e70eb00e93687df7/av-15.1.0-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:a81cd515934a5d51290aa66b059b7ed29c4a212e704f3c5e99e32877ff1c312c", size = 26913753, upload-time = "2025-08-30T04:40:50.445Z" }, - { url = "https://files.pythonhosted.org/packages/37/9e/7af078bcfc3cd340c981ac5d613c090ab007023d2ac13b05acd52f22f069/av-15.1.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:57cc7a733a7e7d7a153682f35c9cf5d01e8269367b049c954779de36fc3d0b10", size = 38027048, upload-time = "2025-08-30T04:40:54.076Z" }, - { url = "https://files.pythonhosted.org/packages/02/76/1f9dac11ad713e3619288993ea04e9c9cf4ec0f04e5ee81e83b8129dd8f3/av-15.1.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a77b75bdb6899a64302ff923a5246e0747b3f0a3ecee7d61118db407a22c3f53", size = 39565396, upload-time = "2025-08-30T04:40:57.84Z" }, - { url = "https://files.pythonhosted.org/packages/8b/32/2188c46e2747247458ffc26b230c57dd28e61f65ff7b9e6223a411af5e98/av-15.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d0a1154ce081f1720082a133cfe12356c59f62dad2b93a7a1844bf1dcd010d85", size = 40015050, upload-time = "2025-08-30T04:41:01.091Z" }, - { url = "https://files.pythonhosted.org/packages/1e/41/b57fbce9994580619d7574817ece0fe0e7b822cde2af57904549d0150b8d/av-15.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a7bf5a34dee15c86790414fa86a144e6d0dcc788bc83b565fdcbc080b4fbc90", size = 40821225, upload-time = "2025-08-30T04:41:04.349Z" }, - { url = "https://files.pythonhosted.org/packages/b1/36/e85cd1f0d3369c6764ad422882895d082f7ececb66d3df8aeae3234ef7a6/av-15.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:e30c9a6fd9734784941384a2e25fad3c22881a7682f378914676aa7e795acdb7", size = 31311750, upload-time = "2025-08-30T04:41:07.744Z" }, - { url = "https://files.pythonhosted.org/packages/80/d8/08a681758a4e49adfda409a6a35eff533f42654c6a6cfa102bc5cae1a728/av-15.1.0-cp314-cp314t-macosx_13_0_arm64.whl", hash = "sha256:60666833d7e65ebcfc48034a072de74349edbb62c9aaa3e6722fef31ca028eb6", size = 21828343, upload-time = "2025-08-30T04:41:10.81Z" }, - { url = "https://files.pythonhosted.org/packages/4a/52/29bec3fe68669b21f7d1ab5d94e21f597b8dfd37f50a3e3c9af6a8da925c/av-15.1.0-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:53fbdae45aa2a49a22e864ff4f4017416ef62c060a172085d3247ba0a101104e", size = 27001666, upload-time = "2025-08-30T04:41:13.822Z" }, - { url = "https://files.pythonhosted.org/packages/9d/54/2c1d1faced66d708f5df328e800997cb47f90b500a214130c3a0f2ad601e/av-15.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e6c51061667983dc801502aff9140bbc4f0e0d97f879586f17fb2f9a7e49c381", size = 39496753, upload-time = "2025-08-30T04:41:16.759Z" }, - { url = "https://files.pythonhosted.org/packages/c3/76/06ded5e52c4dcc2d9b5184c6da8de5ea77bd7ecb79a59a2b9700f1984949/av-15.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:2f80ec387f04aa34868662b11018b5f09654ae1530a61e24e92a142a24b10b62", size = 40784729, upload-time = "2025-08-30T04:41:20.491Z" }, - { url = "https://files.pythonhosted.org/packages/52/ef/797b76f3b39c99a96e387f501bbc07dca340b27d3dda12862fe694066b63/av-15.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4975e03177d37d8165c99c8d494175675ba8acb72458fb5d7e43f746a53e0374", size = 41284953, upload-time = "2025-08-30T04:41:23.949Z" }, - { url = "https://files.pythonhosted.org/packages/31/47/e4656f00e62fd059ea5a40b492dea784f5aecfe1dfac10c0d7a0664ce200/av-15.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8f78f3dad11780b4cdd024cdb92ce43cb170929297c00f2f4555c2b103f51e55", size = 41985340, upload-time = "2025-08-30T04:41:27.561Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c9/15bb4fd7a1f39d70db35af2b9c20a0ae19e4220eb58a8b8446e903b98d72/av-15.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a20c5eba3ec49c2f4b281797021923fc68a86aeb66c5cda4fd0252fa8004951", size = 31487337, upload-time = "2025-08-30T04:41:30.591Z" }, +version = "17.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/f0/8c8dca97ae0cf00e8e2a53bb5cb9aca5fd484f585ef3e9b412200aff3ebd/av-17.0.1.tar.gz", hash = "sha256:fbcbd4aa43bca6a8691816283112d1659a27f407bbeb66d1397023691339f5d4", size = 4411938, upload-time = "2026-04-18T17:12:34.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/82/e7007dcef7bd2d2c377e2e85977701384f42d19fc808c2ccb3a99eaf58f2/av-17.0.1-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:987f4f46ceae4da6c614dcbd2b8149be9dbf680c3bb7a6841c58af9cff4d9230", size = 23238802, upload-time = "2026-04-18T17:11:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/6b/aa/858b09a08ea6f83f91be44b5a5adad13ae8d9ac8b80fda27e73c24bfb160/av-17.0.1-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:d97f54e55b18a74912f479c1978aadd1341d38d892dee95bb5c2f2dccfa72f32", size = 18709338, upload-time = "2026-04-18T17:11:53.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8b/8de3fd21c4b0b74d44337421abeab0e71462337fb6a28fff888e0c356cbd/av-17.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e6eee84afa48d0e9321047cd3e4facd44b401493f6bdc753e2e1d1e7c9e6d13e", size = 34007351, upload-time = "2026-04-18T17:11:56.116Z" }, + { url = "https://files.pythonhosted.org/packages/02/28/167b291356c2cc315a2d62a95b0ceace72b5b0bf547de30b89313110f032/av-17.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c58c71bffd9383908c85695ac61d3184c668accb04a5bd1b262e0fb8d09f60a5", size = 36345295, upload-time = "2026-04-18T17:11:59.125Z" }, + { url = "https://files.pythonhosted.org/packages/04/fa/aae56f2ff2c204c408641e1120f5ca5ce9c3390cf5362245c6f1158704b5/av-17.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:42d6745d30a410ec9b22aef79a52a7ab5a001eb8f5adfd952946606a30983318", size = 35183754, upload-time = "2026-04-18T17:12:01.697Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bd/776046f27093aef80155a204ca7d82a887ae4ee72ba4ef8411b46ea7898c/av-17.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3ed6bcd7021fe55832f95b8ef78dd01a4cb21faf3cd71f1e1bf4f20bf100b278", size = 37430809, upload-time = "2026-04-18T17:12:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d5/3261bd2c6b7f6c0aa8379fc970d1ecf496330990b992ad28607785074268/av-17.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:9af524e8632a54032e361d6b88895bd3e7c6212ca560de60f5ccc525323c764c", size = 28889649, upload-time = "2026-04-18T17:12:07.04Z" }, + { url = "https://files.pythonhosted.org/packages/98/39/381104e427a0c7231d2ec0d25d538d58fc20fc0458846b95860d3ef8073b/av-17.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:50e58a473d65ea29b645e45c9fd8518a6783737135683ecc40571a91592bdfe4", size = 21918412, upload-time = "2026-04-18T17:12:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8c/bb1498f031abb6157b30b7fc2379359176953821b6ba59fbd89dbb56f61f/av-17.0.1-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:1d33871742d1e71562db3c8e752cacc5a62766d7efc3ae408bff1c3e26ebb46e", size = 23484157, upload-time = "2026-04-18T17:12:11.67Z" }, + { url = "https://files.pythonhosted.org/packages/1a/58/dedaef187b797243cd5762722e376c69c5ad95ab23db44127f09afc2cd66/av-17.0.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1229e879f4b6431bc00f69d7f8891fe9a683b0a6e0e009e6c98eb7e449f0383d", size = 18920872, upload-time = "2026-04-18T17:12:14.826Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/5c550231651d6285e6a5c4f6f4a0e67459bfe2b622a7c9352be8cca8c819/av-17.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4744837f4116964280bcc72285e3cdd51361e98a696205aadd924203440ef511", size = 37471077, upload-time = "2026-04-18T17:12:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/9807b89a9d775c6f015677996c48bce48aaff70b5d95885adf39e59832a2/av-17.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3d0a7d45d9599bf9df9f8249827113d4f36df1cd6b5356227b997f0552dbc98e", size = 39566981, upload-time = "2026-04-18T17:12:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/5c/72/a22a657abc3de652f5b4f46cbbebdf7cba629752112791b81f05d340991d/av-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9acd0b6a6e02af2b37f63d97a03ee2c47936d58e82425c3cd075a95245937c59", size = 38397369, upload-time = "2026-04-18T17:12:22.909Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b2/f4e83e41c1e3c186f34b7df506779d0cd7e40499e2e19519c7ece148cd20/av-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3d3a36204cb1f1e7691e6446afa8d6b7097b09946dae732c71c5d05ce09e506e", size = 40582445, upload-time = "2026-04-18T17:12:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/8676188b72eed09d48ce6cfaf0f22b0bb9f3cfd74d388ee2b7fdf960536d/av-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:b87b98afe971cde123953073bc9c95ab0b7efd2ecc082dd2dbd11f9d9abf190e", size = 29217136, upload-time = "2026-04-18T17:12:29.189Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/0a6e1d2a845988039f6c197fa7269b5e9abbe17354fb41cc9d75bb260fcb/av-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa", size = 22072676, upload-time = "2026-04-18T17:12:31.836Z" }, ] [[package]] name = "awscli" -version = "1.44.64" +version = "1.45.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, @@ -431,9 +427,9 @@ dependencies = [ { name = "rsa" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/bc/aa2a85ab1066c27f49f27d5dab52a7f4135a6bb858be760b03d45fa67521/awscli-1.44.64.tar.gz", hash = "sha256:9264cd799ea99d3af05fe8ae78a9bd6654b0dda8462ecf998c0c69bc6b96f55b", size = 1886278, upload-time = "2026-03-23T19:34:05.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/95/74e59fe408b38a08b32f8eb3afd94f7f4be5da39c1207e7b494feae1f6cf/awscli-1.45.17.tar.gz", hash = "sha256:a355f4a20a8f04b473f5465bf7904bb061f71500564d1a725db479b1868c8df2", size = 1894987, upload-time = "2026-05-28T19:39:13.584Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/bc/820678c635bb19a8c83f027ff444e301ef65cd5d79b8339efe4624df652d/awscli-1.44.64-py3-none-any.whl", hash = "sha256:7ab849fcb5d9263e2d067949da73b9e72860423f766db2665c51fdf3dd33b5f9", size = 4623760, upload-time = "2026-03-23T19:34:01.612Z" }, + { url = "https://files.pythonhosted.org/packages/e2/dc/8645cb28f9802db8a772e5772ffe28a8879806342e7e3cdcf10cf36241d7/awscli-1.45.17-py3-none-any.whl", hash = "sha256:dd262425e764ad40b92246a3bd60ff3f0e15527881155bd1f0b1cf08b7db15b5", size = 4646787, upload-time = "2026-05-28T19:39:11.354Z" }, ] [[package]] @@ -447,15 +443,15 @@ wheels = [ [[package]] name = "azure-core" -version = "1.39.0" +version = "1.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f3/b416179e408990df5db0d516283022dde0f5d0111d98c1a848e41853e81c/azure_core-1.41.0.tar.gz", hash = "sha256:f46ff5dfcd230f25cf1c19e8a34b8dc08a337b2503e268bb600a16c00db8ad5a", size = 381042, upload-time = "2026-05-07T23:30:54.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/325c6d7312d2200251c52323878281045aaffcb5586612296484e4280eaa/azure_core-1.41.0-py3-none-any.whl", hash = "sha256:522b4011e8180b1a3dcd2024396a4e7fe9ac37fb8597db47163d230b5efe892d", size = 220920, upload-time = "2026-05-07T23:30:56.357Z" }, ] [[package]] @@ -485,61 +481,49 @@ wheels = [ [[package]] name = "backports-zstd" -version = "1.3.0" +version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/b1/36a5182ce1d8ef9ef32bff69037bd28b389bbdb66338f8069e61da7028cb/backports_zstd-1.3.0.tar.gz", hash = "sha256:e8b2d68e2812f5c9970cabc5e21da8b409b5ed04e79b4585dbffa33e9b45ebe2", size = 997138, upload-time = "2025-12-29T17:28:06.143Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/d4/356da49d3053f4bc50e71a8535631b57bc9ca4e8c6d2442e073e0ab41c44/backports_zstd-1.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f4a292e357f3046d18766ce06d990ccbab97411708d3acb934e63529c2ea7786", size = 435972, upload-time = "2025-12-29T17:26:18.752Z" }, - { url = "https://files.pythonhosted.org/packages/30/8f/dbe389e60c7e47af488520f31a4aa14028d66da5bf3c60d3044b571eb906/backports_zstd-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb4c386f38323698991b38edcc9c091d46d4713f5df02a3b5c80a28b40e289ea", size = 362124, upload-time = "2025-12-29T17:26:19.995Z" }, - { url = "https://files.pythonhosted.org/packages/55/4b/173beafc99e99e7276ce008ef060b704471e75124c826bc5e2092815da37/backports_zstd-1.3.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f52523d2bdada29e653261abdc9cfcecd9e5500d305708b7e37caddb24909d4e", size = 506378, upload-time = "2025-12-29T17:26:21.855Z" }, - { url = "https://files.pythonhosted.org/packages/df/c8/3f12a411d9a99d262cdb37b521025eecc2aa7e4a93277be3f4f4889adb74/backports_zstd-1.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3321d00beaacbd647252a7f581c1e1cdbdbda2407f2addce4bfb10e8e404b7c7", size = 476201, upload-time = "2025-12-29T17:26:23.047Z" }, - { url = "https://files.pythonhosted.org/packages/43/dc/73c090e4a2d5671422512e1b6d276ca6ea0cc0c45ec4634789106adc0d66/backports_zstd-1.3.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:88f94d238ef36c639c0ae17cf41054ce103da9c4d399c6a778ce82690d9f4919", size = 581659, upload-time = "2025-12-29T17:26:24.189Z" }, - { url = "https://files.pythonhosted.org/packages/08/4f/11bfcef534aa2bf3f476f52130217b45337f334d8a287edb2e06744a6515/backports_zstd-1.3.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97d8c78fe20c7442c810adccfd5e3ea6a4e6f4f1fa4c73da2bc083260ebead17", size = 640388, upload-time = "2025-12-29T17:26:25.47Z" }, - { url = "https://files.pythonhosted.org/packages/71/17/8faea426d4f49b63238bdfd9f211a9f01c862efe0d756d3abeb84265a4e2/backports_zstd-1.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eefda80c3dbfbd924f1c317e7b0543d39304ee645583cb58bae29e19f42948ed", size = 494173, upload-time = "2025-12-29T17:26:26.736Z" }, - { url = "https://files.pythonhosted.org/packages/ba/9d/901f19ac90f3cd999bdcfb6edb4d7b4dc383dfba537f06f533fc9ac4777b/backports_zstd-1.3.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2ab5d3b5a54a674f4f6367bb9e0914063f22cd102323876135e9cc7a8f14f17e", size = 568628, upload-time = "2025-12-29T17:26:28.12Z" }, - { url = "https://files.pythonhosted.org/packages/60/39/4d29788590c2465a570c2fae49dbff05741d1f0c8e4a0fb2c1c310f31804/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7558fb0e8c8197c59a5f80c56bf8f56c3690c45fd62f14e9e2081661556e3e64", size = 482233, upload-time = "2025-12-29T17:26:29.399Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4b/24c7c9e8ef384b19d515a7b1644a500ceb3da3baeff6d579687da1a0f62b/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:27744870e38f017159b9c0241ea51562f94c7fefcfa4c5190fb3ec4a65a7fc63", size = 509806, upload-time = "2025-12-29T17:26:30.605Z" }, - { url = "https://files.pythonhosted.org/packages/3f/7e/7ba1aeecf0b5859f1855c0e661b4559566b64000f0627698ebd9e83f2138/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b099750755bb74c280827c7d68de621da0f245189082ab48ff91bda0ec2db9df", size = 586037, upload-time = "2025-12-29T17:26:32.201Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1a/18f0402b36b9cfb0aea010b5df900cfd42c214f37493561dba3abac90c4e/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5434e86f2836d453ae3e19a2711449683b7e21e107686838d12a255ad256ca99", size = 566220, upload-time = "2025-12-29T17:26:33.5Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d9/44c098ab31b948bbfd909ec4ae08e1e44c5025a2d846f62991a62ab3ebea/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:407e451f64e2f357c9218f5be4e372bb6102d7ae88582d415262a9d0a4f9b625", size = 630847, upload-time = "2025-12-29T17:26:35.273Z" }, - { url = "https://files.pythonhosted.org/packages/30/33/e74cb2cfb162d2e9e00dad8bcdf53118ca7786cfd467925d6864732f79cc/backports_zstd-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58a071f3c198c781b2df801070290b7174e3ff61875454e9df93ab7ea9ea832b", size = 498665, upload-time = "2025-12-29T17:26:37.123Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a9/67a24007c333ed22736d5cd79f1aa1d7209f09be772ff82a8fd724c1978e/backports_zstd-1.3.0-cp312-cp312-win32.whl", hash = "sha256:21a9a542ccc7958ddb51ae6e46d8ed25d585b54d0d52aaa1c8da431ea158046a", size = 288809, upload-time = "2025-12-29T17:26:38.373Z" }, - { url = "https://files.pythonhosted.org/packages/42/24/34b816118ea913debb2ea23e71ffd0fb2e2ac738064c4ac32e3fb62c18bb/backports_zstd-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:89ea8281821123b071a06b30b80da8e4d8a2b40a4f57315a19850337a21297ac", size = 313815, upload-time = "2025-12-29T17:26:39.665Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2f/babd02c9fc4ca35376ada7c291193a208165c7be2455f0f98bc1e1243f31/backports_zstd-1.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:f6843ecb181480e423b02f60fe29e393cbc31a95fb532acdf0d3a2c87bd50ce3", size = 288927, upload-time = "2025-12-29T17:26:40.923Z" }, - { url = "https://files.pythonhosted.org/packages/0c/7d/53e8da5950cdfc5e8fe23efd5165ce2f4fed5222f9a3292e0cdb03dd8c0d/backports_zstd-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e86e03e3661900955f01afed6c59cae9baa63574e3b66896d99b7de97eaffce9", size = 435463, upload-time = "2025-12-29T17:26:42.152Z" }, - { url = "https://files.pythonhosted.org/packages/da/78/f98e53870f7404071a41e3d04f2ff514302eeeb3279d931d02b220f437aa/backports_zstd-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:41974dcacc9824c1effe1c8d2f9d762bcf47d265ca4581a3c63321c7b06c61f0", size = 361740, upload-time = "2025-12-29T17:26:43.377Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ed/2c64706205a944c9c346d95c17f632d4e3468db3ce60efb6f5caa7c0dcae/backports_zstd-1.3.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:3090a97738d6ce9545d3ca5446df43370928092a962cbc0153e5445a947e98ed", size = 505651, upload-time = "2025-12-29T17:26:44.495Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7b/22998f691dc6e0c7e6fa81d611eb4b1f6a72fb27327f322366d4a7ca8fb3/backports_zstd-1.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc874638abf03ea1ff3b0525b4a26a8d0adf7cb46a448c3449f08e4abc276b3", size = 475859, upload-time = "2025-12-29T17:26:45.722Z" }, - { url = "https://files.pythonhosted.org/packages/0b/78/0cde898339a339530e5f932634872d2d64549969535447a48d3b98959e11/backports_zstd-1.3.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:db609e57b8ed88b3472930c87e93c08a4bbd5ffeb94608cd9c7c6f0ac0e166c6", size = 581339, upload-time = "2025-12-29T17:26:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/e2/1d/e0973e0eebe678c12c146473af2c54cda8a3e63b179785ca1a20727ad69c/backports_zstd-1.3.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f13033a3dd95f323c067199f2e61b4589a7880188ef4ef356c7ffbdb78a9f11", size = 642182, upload-time = "2025-12-29T17:26:48.545Z" }, - { url = "https://files.pythonhosted.org/packages/82/a2/ac67e79e137eb98aead66c7162bafe3cffcb82ef9cdeb6367ec18d88fbce/backports_zstd-1.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c4c7bcda5619a754726e7f5b391827f5efbe4bed8e62e9ec7490d42bff18aa6", size = 490807, upload-time = "2025-12-29T17:26:49.789Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/3514b1d065801ae7dce05246e9389003ed8fb1d7c3d71f85aa07a80f41e6/backports_zstd-1.3.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884a94c40f27affe986f394f219a4fd3cbbd08e1cff2e028d29d467574cd266e", size = 566103, upload-time = "2025-12-29T17:26:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/1b/03/10ddb54cbf032e5fe390c0776d3392611b1fc772d6c3cb5a9bcdff4f915f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497f5765126f11a5b3fd8fedfdae0166d1dd867e7179b8148370a3313d047197", size = 481614, upload-time = "2025-12-29T17:26:52.255Z" }, - { url = "https://files.pythonhosted.org/packages/5c/13/21efa7f94c41447f43aee1563b05fc540a235e61bce4597754f6c11c2e97/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a6ff6769948bb29bba07e1c2e8582d5a9765192a366108e42d6581a458475881", size = 509207, upload-time = "2025-12-29T17:26:53.496Z" }, - { url = "https://files.pythonhosted.org/packages/de/e7/12da9256d9e49e71030f0ff75e9f7c258e76091a4eaf5b5f414409be6a57/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1623e5bff1acd9c8ef90d24fc548110f20df2d14432bfe5de59e76fc036824ef", size = 585765, upload-time = "2025-12-29T17:26:54.99Z" }, - { url = "https://files.pythonhosted.org/packages/24/bf/59ca9cb4e7be1e59331bb792e8ef1331828efe596b1a2f8cbbc4e3f70d75/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:622c28306dcc429c8f2057fc4421d5722b1f22968d299025b35d71b50cfd4e03", size = 563852, upload-time = "2025-12-29T17:26:56.371Z" }, - { url = "https://files.pythonhosted.org/packages/7c/ee/5a3eaed9a73bdf2c35dc0c7adc0616a99588e0de28f5ab52f3e0caaaa96f/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09a2785e410ed2e812cb39b684ef5eb55083a5897bfd0e6f5de3bbd2c6345f70", size = 632549, upload-time = "2025-12-29T17:26:57.598Z" }, - { url = "https://files.pythonhosted.org/packages/75/b9/c823633afc48a1ac56d6ad34289c8f51b0234685142531bfa8197ca91777/backports_zstd-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ade1f4127fdbe36a02f8067d75aa79c1ea1c8a306bf63c7b818bb7b530e1beaa", size = 495104, upload-time = "2025-12-29T17:26:58.826Z" }, - { url = "https://files.pythonhosted.org/packages/a3/8f/6f7030f18fa7307f87b0f57108a50a3a540b6350e2486d1739c0567629a3/backports_zstd-1.3.0-cp313-cp313-win32.whl", hash = "sha256:668e6fb1805b825cb7504c71436f7b28d4d792bb2663ee901ec9a2bb15804437", size = 288447, upload-time = "2025-12-29T17:27:00.036Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/b1df1bbbe4e6d3ffd364d0bcffdeb6c4361115c1eccd91238dbdd0c07fec/backports_zstd-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:385bdadf0ea8fe6ba780a95e4c7d7f018db7bafdd630932f0f9f0fad05d608ff", size = 313664, upload-time = "2025-12-29T17:27:01.267Z" }, - { url = "https://files.pythonhosted.org/packages/45/0f/60918fe4d3f2881de8f4088d73be4837df9e4c6567594109d355a2d548b6/backports_zstd-1.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:4321a8a367537224b3559fe7aeb8012b98aea2a60a737e59e51d86e2e856fe0a", size = 288678, upload-time = "2025-12-29T17:27:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b9/35f423c0bcd85020d5e7be6ab8d7517843e3e4441071beb5c3bd8c5216cb/backports_zstd-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:10057d66fa4f0a7d3f6419ffb84b4fe61088da572e3ac4446134a1c8089e4166", size = 436155, upload-time = "2025-12-29T17:27:03.859Z" }, - { url = "https://files.pythonhosted.org/packages/f6/14/e504daea24e8916f14ecbc223c354b558d8410cfc846606668ab91d96b38/backports_zstd-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4abf29d706ba05f658ca0247eb55675bcc00e10f12bca15736e45b05f1f2d2dc", size = 362436, upload-time = "2025-12-29T17:27:05.076Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f7/06e178dbab7edb88c2872aebd68b54137e07a169eba1aeedf614014f7036/backports_zstd-1.3.0-cp313-cp313t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:127b0d73c745b0684da3d95c31c0939570810dad8967dfe8231eea8f0e047b2f", size = 507600, upload-time = "2025-12-29T17:27:06.254Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f1/2ce499b81c4389d6fa1eeea7e76f6e0bad48effdbb239da7cbcdaaf24b76/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0205ef809fb38bb5ca7f59fa03993596f918768b9378fb7fbd8a68889a6ce028", size = 475496, upload-time = "2025-12-29T17:27:07.939Z" }, - { url = "https://files.pythonhosted.org/packages/18/1e/c82a586f2866aabf3a601a521af3c58756d83d98b724fda200016ac5e7e2/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c389b667b0b07915781aa28beabf2481f11a6062a1a081873c4c443b98601a7", size = 580919, upload-time = "2025-12-29T17:27:09.1Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a3/eb5d9b7c4cb69d1b8ccd011abe244ba6815693b70bed07ed4b77ddda4535/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8e7ac5ef693d49d6fb35cd7bbb98c4762cfea94a8bd2bf2ab112027004f70b11", size = 639913, upload-time = "2025-12-29T17:27:10.433Z" }, - { url = "https://files.pythonhosted.org/packages/11/2c/7296b99df79d9f31174a99c81c1964a32de8996ce2b3068f5bc66b413615/backports_zstd-1.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d5543945aae2a76a850b23f283249424f535de6a622d6002957b7d971e6a36d", size = 494800, upload-time = "2025-12-29T17:27:11.59Z" }, - { url = "https://files.pythonhosted.org/packages/f9/fc/b8ae6e104ba72d20cd5f9dfd9baee36675e89c81d432434927967114f30f/backports_zstd-1.3.0-cp313-cp313t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38be15ebce82737deda2c9410c1f942f1df9da74121049243a009810432db75", size = 570396, upload-time = "2025-12-29T17:27:13.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/56/60a7a9de7a5bc951ea1106358b413c95183c93480394f3abc541313c8679/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3e3f58c76f4730607a4e0130d629173aa114ae72a5c8d3d5ad94e1bf51f18d8", size = 481980, upload-time = "2025-12-29T17:27:14.317Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bb/93fc1e8e81b8ecba58b0e53a14f7b44375cf837db6354410998f0c4cb6ff/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b808bf889722d889b792f7894e19c1f904bb0e9092d8c0eb0787b939b08bad9a", size = 511358, upload-time = "2025-12-29T17:27:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/ae/0f/b165c2a6080d22306975cd86ce97270208493f31a298867e343110570370/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f7be27d56f2f715bcd252d0c65c232146d8e1e039c7e2835b8a3ad3dc88bc508", size = 585492, upload-time = "2025-12-29T17:27:16.986Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/85b4bde76e982b24a7eb57a2fb9868807887bef4d2114a3654a6530a67ef/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:cbe341c7fcc723893663a37175ba859328b907a4e6d2d40a4c26629cc55efb67", size = 568309, upload-time = "2025-12-29T17:27:18.28Z" }, - { url = "https://files.pythonhosted.org/packages/83/64/9490667827a320766fb883f358a7c19171fdc04f19ade156a8c341c36967/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:b4116a9e12dfcd834dd9132cf6a94657bf0d328cba5b295f26de26ea0ae1adc8", size = 630518, upload-time = "2025-12-29T17:27:19.525Z" }, - { url = "https://files.pythonhosted.org/packages/ea/43/258587233b728bbff457bdb0c52b3e08504c485a8642b3daeb0bdd5a76bc/backports_zstd-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1049e804cc8754290b24dab383d4d6ed0b7f794ad8338813ddcb3907d15a89d0", size = 499429, upload-time = "2025-12-29T17:27:21.063Z" }, - { url = "https://files.pythonhosted.org/packages/32/04/cfab76878f360f124dbb533779e1e4603c801a0f5ada72ae5c742b7c4d7d/backports_zstd-1.3.0-cp313-cp313t-win32.whl", hash = "sha256:7d3f0f2499d2049ec53d2674c605a4b3052c217cc7ee49c05258046411685adc", size = 289389, upload-time = "2025-12-29T17:27:22.287Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ff/dbcfb6c9c922ab6d98f3d321e7d0c7b34ecfa26f3ca71d930fe1ef639737/backports_zstd-1.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eb2f8fab0b1ea05148394cb34a9e543a43477178765f2d6e7c84ed332e34935e", size = 314776, upload-time = "2025-12-29T17:27:23.458Z" }, - { url = "https://files.pythonhosted.org/packages/01/4b/82e4baae3117806639fe1c693b1f2f7e6133a7cefd1fa2e38018c8edcd68/backports_zstd-1.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c66ad9eb5bfbe28c2387b7fc58ddcdecfb336d6e4e60bcba1694a906c1f21a6c", size = 289315, upload-time = "2025-12-29T17:27:24.601Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d4/05/480d439b482edf59b786bc19b474d990c61942e372f5de3dc14acac8154d/backports_zstd-1.5.0.tar.gz", hash = "sha256:a5e622a82eb183b4fbe18032755ce0a15fa9a82f2adb9b621620b91247aaedb7", size = 998556, upload-time = "2026-05-11T19:54:24.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/71/29ed213344f8f62b7520745d7df3752d88db456aff9d8b706bdf5eb99a3c/backports_zstd-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1858cacdb3e50105a1b60acdc3dd5b18650077d12dce243e19d5c88e8172bd71", size = 437170, upload-time = "2026-05-11T19:52:53.204Z" }, + { url = "https://files.pythonhosted.org/packages/d0/e3/a58a3eb8fc54d4e3e4f684ed7b1f688da02e5bda5ae5e2809e94cf2ead2f/backports_zstd-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ccffc0a1974ecc2cc42afa4c15f56d036a4b2bae0abc46e6ba9b3358d9b1c037", size = 363265, upload-time = "2026-05-11T19:52:55.153Z" }, + { url = "https://files.pythonhosted.org/packages/3f/03/9d13840d206dec1c4698c803f61c58379b3578cb9dc6140ba5fa4ce2f31d/backports_zstd-1.5.0-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:ab3430ab4d4ac3fb1bc1e4174d137731e51363b6abd5e51a1599690fe9c7d61d", size = 507527, upload-time = "2026-05-11T19:52:57.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8f/8dc4b5736dca218cbca9609549a8f6dc202990abdb49afdc6112442f5360/backports_zstd-1.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c737c1cb4a10c2d0f6cba9a347522858094f0a737b4558c67a777bcaa4a795cd", size = 477352, upload-time = "2026-05-11T19:52:59.425Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/65a66976a761b5b62eacbaed5ed418c694b24b5c480399315d799751de62/backports_zstd-1.5.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0379c66510681a6b2780d3f3ef2cff54d01204b52448d64bde1855d40f856a04", size = 582799, upload-time = "2026-05-11T19:53:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/ee93a66cd28cb3ad7f3c04d1105325a5428671b18bd41ba9ed8b43bc44cf/backports_zstd-1.5.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c7474b291e264c9609358d3875cf539623f7a65339c2b533020992b1a4c095b", size = 641530, upload-time = "2026-05-11T19:53:03.082Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4b/2cecd4d6679f175f28ae02022bd2050ff4023e38902fae104dbe2e231911/backports_zstd-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb73c22444617bc5a3abf32dd27b3f2085898cfe3b95e6855300e9189898a3bd", size = 495324, upload-time = "2026-05-11T19:53:05.005Z" }, + { url = "https://files.pythonhosted.org/packages/4d/20/ee21e4e791e31f38f7a70b3961eb64b350d9be802a335e7a04c02b41b197/backports_zstd-1.5.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6cd7f6c33afd89354f74469e315e72754e3040f91f7b685061e225d9e36e3e8e", size = 569796, upload-time = "2026-05-11T19:53:07.011Z" }, + { url = "https://files.pythonhosted.org/packages/76/da/86c9a2ea384885b60638b3e47113198449568d0e36ef3834d1f969623092/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2106309071f279b38d3663c55c7fed192733b4f332b50eb3fa707e54bad6967a", size = 483367, upload-time = "2026-05-11T19:53:08.674Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f0/c95c6e4dd28fc314547782a482839e422283d62c2aaf45d30672109a4a1e/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:56fffa80be74cb11ac843333bbdc56e466c87967706886b3efd6b16d83830d90", size = 510976, upload-time = "2026-05-11T19:53:10.339Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a2/72777b7e1872228a13b09b0bf77ae6cf626008d462cc2e1a0ae64721fd55/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5e8b8251eec80e67e30ec79dfc5b3b1ada069b9ac48b56b102f3e2c6f8281062", size = 587190, upload-time = "2026-05-11T19:53:12.205Z" }, + { url = "https://files.pythonhosted.org/packages/f5/a1/db5d1aee59da308eadeaa189764a4ec68e98495c309a13dcb8da5718fef1/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f334dd17ffead361aa9090e40151bd123507ce213a62733121b7145c6711cbde", size = 567395, upload-time = "2026-05-11T19:53:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/00/0f/39ca1a6e8c5c2dc81da9e06c44d1990cc464f4b16dae214e877afd7adfc0/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:78cbfd061255fef6de5070a54e0f9c00e8aabad5c99dd2ad884a3a7d1acc09ae", size = 632048, upload-time = "2026-05-11T19:53:16.234Z" }, + { url = "https://files.pythonhosted.org/packages/73/fd/a438ee4fc615016dbe96112b709b6805ee19eb215f46e208c8fbce086d8d/backports_zstd-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f55d70df44f49d599e20033013bc1ae705202735c45d4bca8eb963b225e15fd", size = 499833, upload-time = "2026-05-11T19:53:17.85Z" }, + { url = "https://files.pythonhosted.org/packages/f7/42/f544fde4de32687e28c514288ae3c11106ba644e9dd580992cbd704bbb49/backports_zstd-1.5.0-cp312-cp312-win32.whl", hash = "sha256:a8b096e0383a3bcab34f8c97b79e1a52051189d11258bbc2bc1145997a15dd1d", size = 289876, upload-time = "2026-05-11T19:53:19.486Z" }, + { url = "https://files.pythonhosted.org/packages/ad/31/9c29cd3175892e5ee909f5e8d14707fa07815301ff24b5c697d1cea62a77/backports_zstd-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:e2802899ba4ef1a062ffe4bb1292c5df32011a54b4c3004c54f46ec975f39554", size = 314933, upload-time = "2026-05-11T19:53:20.942Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/1a50acd6446c0d57c4f93ad6ce68e1a631ad920737a6b2d0bbbc47de7f42/backports_zstd-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:3c0353e66942afbd45518788cfbd1e9e117828ceb390fa50517f46f291850d8e", size = 291665, upload-time = "2026-05-11T19:53:22.686Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e6/252521e3a847eb200bc0a1d528542d651b9c8dc7953e231c39ed2890d5ff/backports_zstd-1.5.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:02a57ee8598dd863c0b11c7af00042ce6bc045bf6f4249fa4c322c62614ca1fd", size = 400134, upload-time = "2026-05-11T19:53:24.28Z" }, + { url = "https://files.pythonhosted.org/packages/36/43/27ef105ffa2da3d52218d4a7b2e14037974283953b3ee790358af6e9b4df/backports_zstd-1.5.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c56c11eb3173d540e1fb0216f7ab477cbd3a204eca41f5f329059ee8a5d2ad47", size = 454225, upload-time = "2026-05-11T19:53:25.874Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/cdcba1244347500d00567ce2cd6bf04c92d1b0fb6405fb8e13c07715eb46/backports_zstd-1.5.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ef98f632026aa8e6ce05d786977092798efbe78677aa71219f22d31787809c90", size = 357229, upload-time = "2026-05-11T19:53:27.661Z" }, + { url = "https://files.pythonhosted.org/packages/df/da/cea04dab3ffb940bde9a59866bde6f2594a7b3ef2948a63fb3898f73d311/backports_zstd-1.5.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:c3712300b18f9d07f788b03594b2f34dfad89d77df96938a640c5007522a6b69", size = 365907, upload-time = "2026-05-11T19:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/da/c4/6a71df2e65033f9b7d8017d77ea2bb572fc2ebc814ea383fdcda4187597a/backports_zstd-1.5.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:bdbc75d1f54df70b65bcfbc8aa0cac21475f79665bb045960af606dc07b56090", size = 446453, upload-time = "2026-05-11T19:53:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/66/e7/f98ad1a6a249c27884df9d28cf6ebc3c368e0e3288a741c1d51a572bb3d7/backports_zstd-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93d306300d25e59f1cbe98cda494bf295be03a20e8b2c5602ee5ddc03ded29f2", size = 436634, upload-time = "2026-05-11T19:53:32.484Z" }, + { url = "https://files.pythonhosted.org/packages/ba/42/d0393ecc64e2ab6ae1b5ca7edbe26e3fe5196885f15d6cc4bce7254e29cd/backports_zstd-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:305d2e4ae9a595d0fd9d5bea5a7a2163306c6c4dcc5eec35ecd5008219d4580e", size = 362867, upload-time = "2026-05-11T19:53:34.385Z" }, + { url = "https://files.pythonhosted.org/packages/41/fe/87aa9404763bada695d06e5cb9d0575bae033cbf3a2e4e3bd648760178f7/backports_zstd-1.5.0-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:c8f0967bf8d806b250fb1e905a6b8190e7ae83656d5308989243f84e01fa3774", size = 506844, upload-time = "2026-05-11T19:53:36.023Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/3af7ce637d148e0b0acb1298b61afe9a934ed425bad9ff05e87afbf6766d/backports_zstd-1.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76b7314ca9a253171e3e9524960e9e6411997323cf10aecbbc330faa7a90278d", size = 476975, upload-time = "2026-05-11T19:53:37.885Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6c/dc2aa1b48296ac6effc3bacb5a3061d40ed74bf73082dfe38eed2ba8362b/backports_zstd-1.5.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1d0bf16bba86b1071731ced389f184e8de61c1afcafa584244f7f726632f92f", size = 582496, upload-time = "2026-05-11T19:53:39.812Z" }, + { url = "https://files.pythonhosted.org/packages/f6/38/dd49d3dd27eda9b165ccd63d70538fea016a3e9e42923bbbc1d89fae8a43/backports_zstd-1.5.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:96709d27d406008575ef759405169d538040156704b457d8c0ac035127a46b67", size = 643257, upload-time = "2026-05-11T19:53:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/59/75/78e819272450aec2462f97a1bceb90bde481f9dba435bf9e76d580b4dec4/backports_zstd-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5737402c29b2bd5bc661d4cde08aed531ed326f2b59a7ad98dc07650dc99a2c9", size = 491958, upload-time = "2026-05-11T19:53:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/d860f9cf21cb59d583a12166353bf71a439538e2b669f4a7736e400ca596/backports_zstd-1.5.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b65f37ddd375114dbf84658e7dd168e10f5a93394940bfefa7fafc2d3234450", size = 567198, upload-time = "2026-05-11T19:53:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/38/7c/b175d4c9ff60f964c8f6dd43211de905227cfde5a41eb5f654df58483025/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fae7825dde4f81c28b4c66b1e997f893e296c3f1668351952b3ed085eb9f8cd", size = 482792, upload-time = "2026-05-11T19:53:47.323Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e3/f7b50cf891a10da5f9c412ed4a9c4a772df4d4186d98a41e75c9b462f148/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3aa10e77c0e712d2dfb950910b50591c2fb11f0f1328814e23acc0b4950766df", size = 510363, upload-time = "2026-05-11T19:53:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/be/50/e7841fd4a65661d527697a0e2dab97295868965ccd4e3e12474472719a60/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:518b2ef54ce0fee6d29379cfd64ef66e639456f1b18943466e929b19677f135f", size = 586917, upload-time = "2026-05-11T19:53:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7c/57e985dbd621f0307b8c57cabb258eb976793f2aeaf8a5bc020e15b4a793/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:673a1e5fdaa6cb0c7a967eb33066b6dd564871b3498a93e11e2972998047d11f", size = 565004, upload-time = "2026-05-11T19:53:53.774Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8f/855ffcd1ee0fcf44c3fe62e36db8e7362292d450cc7c4b3f43edccbcd37a/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1277c07ff2d731586aa05aebd946a1b30184620d886a735dd5d5bf94a4a1061e", size = 633737, upload-time = "2026-05-11T19:53:56.036Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c4129a03d268699200dfebe1ccab97c7c332d2794571afb372a62e4ed098/backports_zstd-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aff334c7c38b4aea2a899f3138a99c1d58f0686ad7815c74bff506ecf4333296", size = 496309, upload-time = "2026-05-11T19:53:57.591Z" }, + { url = "https://files.pythonhosted.org/packages/8e/33/34152316dd244dcd43d5300ded3cf6e1b46d343e4e92620c23e533fa91df/backports_zstd-1.5.0-cp313-cp313-win32.whl", hash = "sha256:b932834c4d85360f46d1e7fbf3eee1e26ba594e0eb5c3ee1281e89bc1d48d06f", size = 289560, upload-time = "2026-05-11T19:53:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/71/c5/f759bc87fd77c88f4fdad2d878535fb7e9537c6a05876d206e6690bf33c6/backports_zstd-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:c71dfbeced720326a8917a6edf921c568dc2396228c6432205c6d7e7fe7f3707", size = 314812, upload-time = "2026-05-11T19:54:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/47/96/d7970dbb2fef34b549b34146090f48f41903cc7268b1ed1c7542eaa1852e/backports_zstd-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:7b5798b20ffff71ee4620a01f56fe0b50271724b4251db08c90a069446cc4752", size = 291411, upload-time = "2026-05-11T19:54:02.541Z" }, ] [[package]] @@ -579,7 +563,7 @@ wheels = [ [[package]] name = "black" -version = "26.3.1" +version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -589,24 +573,24 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, - { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, - { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, - { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] @@ -635,30 +619,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.74" +version = "1.43.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/ec/636ab2aa7ad9e6bf6e297240ac2d44dba63cc6611e2d5038db318436d449/boto3-1.42.74.tar.gz", hash = "sha256:dbacd808cf2a3dadbf35f3dbd8de97b94dc9f78b1ebd439f38f552e0f9753577", size = 112739, upload-time = "2026-03-23T19:34:09.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/6b/80f6bf4f6253c2221c70a2b70af72038bb6e8820ac4547f2ba7d4efcb6be/boto3-1.43.17.tar.gz", hash = "sha256:8cf48babdd52ff0e2d891dc661143780b361d3776a3be06cd719da0696995074", size = 113167, upload-time = "2026-05-28T19:39:18.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/16/a264b4da2af99f4a12609b93fea941cce5ec41da14b33ed3fef77a910f0c/boto3-1.42.74-py3-none-any.whl", hash = "sha256:4bf89c044d618fe4435af854ab820f09dd43569c0df15d7beb0398f50b9aa970", size = 140557, upload-time = "2026-03-23T19:34:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/30b218998dee295873f33c591bb5daf08c42ec27e5fb0ebb13977677e96f/boto3-1.43.17-py3-none-any.whl", hash = "sha256:f6b3862a0b14e237f9323223ee76b0563e87a6bbe6d94a42e7b008a901ba8950", size = 140538, upload-time = "2026-05-28T19:39:15.75Z" }, ] [[package]] name = "botocore" -version = "1.42.74" +version = "1.43.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/c7/cab8a14f0b69944bd0dd1fd58559163455b347eeda00bf836e93ce2684e4/botocore-1.42.74.tar.gz", hash = "sha256:9cf5cdffc6c90ed87b0fe184676806182588be0d0df9b363e9fe3e2923ac8e80", size = 15014379, upload-time = "2026-03-23T19:33:57.692Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/37/a9227caa820189bce55564b6cfc9bbf22f6c984e6b0f0c614348424fb84a/botocore-1.43.17.tar.gz", hash = "sha256:27f4ecb80cf1e5be70415fc4a4d3db3907d41ef8178c9df822364f275427d375", size = 15417107, upload-time = "2026-05-28T19:39:04.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/65/75852e04de5423c9b0c5b88241d0bdea33e6c6f454c88b71377d230216f2/botocore-1.42.74-py3-none-any.whl", hash = "sha256:3a76a8af08b5de82e51a0ae132394e226e15dbf21c8146ac3f7c1f881517a7a7", size = 14688218, upload-time = "2026-03-23T19:33:52.677Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ff/1625713b2ecac9f9bb65c7a51e71cb206b3089ba38f86ba5eff34e947176/botocore-1.43.17-py3-none-any.whl", hash = "sha256:499af7c942ecfd404322974e82c6b5d05a8ea16e9f19320b353e16f401adc5b4", size = 15097131, upload-time = "2026-05-28T19:38:59.775Z" }, ] [[package]] @@ -731,11 +715,11 @@ wheels = [ [[package]] name = "cachetools" -version = "7.0.5" +version = "7.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, ] [[package]] @@ -752,22 +736,22 @@ wheels = [ [[package]] name = "causal-conv1d" -version = "1.6.1" +version = "1.6.2.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ninja" }, { name = "packaging" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/15/ec51d77a2df03ee93410f8ee97fceeb7181da213813c51243e9dd6d7e144/causal_conv1d-1.6.1.tar.gz", hash = "sha256:e4a697ec2db3906f012e675125569f8b510b4559bc53e3095143d91369e1221b", size = 29426, upload-time = "2026-03-10T08:56:35.305Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/5c/2403b8410122d159405c4bd8456340c7251c193358fa24d30cb273fb5048/causal_conv1d-1.6.2.post1.tar.gz", hash = "sha256:245e314ea21064ded7a5bf6b3b842b644aa6f92e45cecfe3e935629744c35ff4", size = 29434, upload-time = "2026-05-09T13:00:51.622Z" } [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.5.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] [[package]] @@ -829,99 +813,106 @@ wheels = [ [[package]] name = "chardet" -version = "7.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/94/7af830a4c63df020644aa99d76147d003a1463f255d0a054958978be5a8a/chardet-7.2.0.tar.gz", hash = "sha256:4ef7292b1342ea805c32cce58a45db204f59d080ed311d6cdaa7ca747fcc0cd5", size = 516522, upload-time = "2026-03-18T00:07:23.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/f2/5b4bfc3c93458c2d618d71f79e34def05552f178b4d452555a8333696f1a/chardet-7.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4604344380a6f9b982c28855c1edfd23a45a2c9142b9a34bc0c08986049f398", size = 547261, upload-time = "2026-03-18T00:07:00.869Z" }, - { url = "https://files.pythonhosted.org/packages/38/fd/3effc8151d19b6ced8d1de427df5a039b1cce4cef79a3ac6f3c1d1135502/chardet-7.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:195c54d8f04a7a9c321cb7cebececa35b1c818c7aa7c195086bae10fcbb3391f", size = 539283, upload-time = "2026-03-18T00:07:02.419Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/c1990fcafa601fcebe9308ae23026906f1e04b53b53ed38e6a81499acd30/chardet-7.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd03a67fca8c91287f8718dfbe3f94c2c1aa1fd3a82433b693f5b868dedf319", size = 561023, upload-time = "2026-03-18T00:07:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/19/5e/4ddbef974a1036416431ef6ceb13dae8c5ab2193a301f2b58c5348855f1b/chardet-7.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f6af0fa005b9488c8fbf8fec2ad7023531970320901d6334c50844ccca9b117", size = 564598, upload-time = "2026-03-18T00:07:05.341Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6b/045858a8b6a54777e64ff4880058018cc05e547e49808f84f7a41a45615a/chardet-7.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8853c71ea1261bcc1b8f8b171acb7c272a5cfd06b57729c460241ee38705049", size = 531154, upload-time = "2026-03-18T00:07:07.061Z" }, - { url = "https://files.pythonhosted.org/packages/65/3e/456ceb2f562dc7969ffaec1e989d9315ad82a023d62a27703a5a5ffdb986/chardet-7.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6cdbe9404534cda0d28f172e91fa50db7655ae6262d093b0337a5aa47a47a5f6", size = 547207, upload-time = "2026-03-18T00:07:08.635Z" }, - { url = "https://files.pythonhosted.org/packages/83/f1/5ef3b6f87e67d73049c632c931baa554364a3826a3522684c4b494e458f8/chardet-7.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427d091994456cc16dbd1e20ae73fee068b9a31f3c90b75072f722d5dbbf156f", size = 539189, upload-time = "2026-03-18T00:07:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/4d/48/8886c21375ff29493bad014fd2b258bb686ac635968b34343e94f8d38745/chardet-7.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad2cd094dfb14cfcb86b0a77568d23375b0005ea0144a726910df6f5c8a46b8", size = 560639, upload-time = "2026-03-18T00:07:10.99Z" }, - { url = "https://files.pythonhosted.org/packages/e6/19/f474429b3c6f829b0eeaaeb964c06737c7dc148c97822937b1a2def55b40/chardet-7.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23e6acd1a58050d7c2aeecca700c0cf27b5ec4f6153a82c3b51c31b94c6ebfad", size = 564172, upload-time = "2026-03-18T00:07:12.536Z" }, - { url = "https://files.pythonhosted.org/packages/dd/be/4fc8c10513cdb9421e731a0a0752973bf2477dad29c490c1dbab7cd0e8db/chardet-7.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5d034faa5b4a2a3af54e24881b2caef9b41fea00a4dddccf97a1e8ec51a213", size = 531024, upload-time = "2026-03-18T00:07:14.11Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7f/0157f588bf8e40e75cc5ca5b3b1cf19cf27b90ea177e3ccd56b73a8adab0/chardet-7.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:719c572c4751c201f42134bd2aa0826928ed5113d29dfa482338c1a89bb925fa", size = 546726, upload-time = "2026-03-18T00:07:15.3Z" }, - { url = "https://files.pythonhosted.org/packages/bd/30/6d216eb2d928ee8db2f30ed7c1451cc7e1a68aa80c551ee9b8ff967e8a38/chardet-7.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:13a94d2c0dace263b8dcb61593c165d5749d60e2e2314231938eb87755c9de9f", size = 539207, upload-time = "2026-03-18T00:07:16.649Z" }, - { url = "https://files.pythonhosted.org/packages/69/4e/fd878a7dc50fe0ece1b3f8baa0c7dcbfc25503d72199200a6f510684549e/chardet-7.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1f081a0f3fce8e1c8f5d6b3691a4960aacc33f213f77ef8b89a6b5f0af4cadf", size = 561383, upload-time = "2026-03-18T00:07:18.269Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/aab35a20545b2d70811bfdc8b55f70161856d9e264ab8ba5259fc09af355/chardet-7.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b56152a17d19249388ae99a85a31c35bb8d5b421b90581226de34b2b316be806", size = 564083, upload-time = "2026-03-18T00:07:19.904Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c0/773b4f0557fdfd6af538e166488824b99a996db558f1c930b1ca27b4775f/chardet-7.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7077dc2435b95163db4206aa71ebc329da5bcddb8bfce69440ff8ecf637400bf", size = 530790, upload-time = "2026-03-18T00:07:21.309Z" }, - { url = "https://files.pythonhosted.org/packages/c2/47/97786f40be59ff5ff10ec5ebcb1ef0ad28dd915717cb210cee89ae7a83a6/chardet-7.2.0-py3-none-any.whl", hash = "sha256:f8ea866b9fbd8df5f19032d765a4d81dcbf6194a3c7388b44d378d02c9784170", size = 414953, upload-time = "2026-03-18T00:07:22.48Z" }, +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870, upload-time = "2026-04-13T21:33:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859, upload-time = "2026-04-13T21:33:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032, upload-time = "2026-04-13T21:33:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283, upload-time = "2026-04-13T21:33:10.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974, upload-time = "2026-04-13T21:33:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973, upload-time = "2026-04-13T21:33:12.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, - { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, - { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, - { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, - { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, - { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] @@ -977,7 +968,7 @@ wheels = [ [[package]] name = "comet-ml" -version = "3.57.4" +version = "3.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dulwich" }, @@ -996,9 +987,9 @@ dependencies = [ { name = "wrapt" }, { name = "wurlitzer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/c6/3885cbc9fe99617ee492403d464906dc15bf17943397c31022fba0997e73/comet_ml-3.57.4.tar.gz", hash = "sha256:42b06f5b473ea270f665409477983f52fa5356ee88e9447f07fc610e47850b5e", size = 585959, upload-time = "2026-04-29T13:37:36.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/95/13dce1808d6101b6d341e59209f7a913d255385fd4b591b91808326508b3/comet_ml-3.58.0.tar.gz", hash = "sha256:9a02fa0b768c321666d66b7e3038fecae4b2d76aaed70998ad46e3647cc56c1c", size = 588845, upload-time = "2026-05-22T17:03:11.321Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/fb/d6c7c9df3fffcd8f3ab6d9926bd6dcf7eedd14daa78f5f76dc4b50140707/comet_ml-3.57.4-py3-none-any.whl", hash = "sha256:8fc913b9b50fa60d372d8e2190f8543fffe4d6a0c9fddd9582b394826906e0e3", size = 787005, upload-time = "2026-04-29T13:37:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8a/2f4336a7519a479335cec06ab3bd4201aea24beeed6974b109c280fba4b3/comet_ml-3.58.0-py3-none-any.whl", hash = "sha256:f781959924e909bea736f091fffbabd91efd80962e8908a1cba2f0d9452185b7", size = 790550, upload-time = "2026-05-22T17:03:09.132Z" }, ] [[package]] @@ -1087,86 +1078,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +version = "7.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, + { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, + { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, + { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, + { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, + { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, + { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, ] [[package]] @@ -1200,46 +1191,89 @@ wheels = [ [[package]] name = "cuda-bindings" -version = "12.9.4" +version = "12.9.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-pathfinder" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, - { url = "https://files.pythonhosted.org/packages/df/6b/9c1b1a6c01392bfdd758e9486f52a1a72bc8f49e98f9355774ef98b5fb4e/cuda_bindings-12.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:696ca75d249ddf287d01b9a698b8e2d8a05046495a9c051ca15659dc52d17615", size = 11586961, upload-time = "2025-10-21T14:51:45.394Z" }, - { url = "https://files.pythonhosted.org/packages/05/8b/b4b2d1c7775fa403b64333e720cfcfccef8dcb9cdeb99947061ca5a77628/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf8bfaedc238f3b115d957d1fd6562b7e8435ba57f6d0e2f87d0e7149ccb2da5", size = 11570071, upload-time = "2025-10-21T14:51:47.472Z" }, - { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, - { url = "https://files.pythonhosted.org/packages/05/d0/d0e4e2e047d8e899f023fa15ad5e9894ce951253f4c894f1cd68490fdb14/cuda_bindings-12.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:a2e82c8985948f953c2be51df45c3fe11c812a928fca525154fb9503190b3e64", size = 11556719, upload-time = "2025-10-21T14:51:52.248Z" }, - { url = "https://files.pythonhosted.org/packages/ec/07/6aff13bc1e977e35aaa6b22f52b172e2890c608c6db22438cf7ed2bf43a6/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3adf4958dcf68ae7801a59b73fb00a8b37f8d0595060d66ceae111b1002de38d", size = 11566797, upload-time = "2025-10-21T14:51:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, - { url = "https://files.pythonhosted.org/packages/4d/3c/972edfddb4ae8a9fccd3c3766ed47453b6f805b6026b32f10209dd4b8ad4/cuda_bindings-12.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b32d8b685f0e66f5658bcf4601ef034e89fc2843582886f0a58784a4302da06c", size = 11894363, upload-time = "2025-10-21T14:51:58.633Z" }, - { url = "https://files.pythonhosted.org/packages/1e/b5/96a6696e20c4ffd2b327f54c7d0fde2259bdb998d045c25d5dedbbe30290/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f53a7f453d4b2643d8663d036bafe29b5ba89eb904c133180f295df6dc151e5", size = 11624530, upload-time = "2025-10-21T14:52:01.539Z" }, - { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, - { url = "https://files.pythonhosted.org/packages/e6/87/652796522cc1a7af559460e1ce59b642e05c1468b9c08522a9a096b4cf04/cuda_bindings-12.9.4-cp314-cp314-win_amd64.whl", hash = "sha256:53a10c71fdbdb743e0268d07964e5a996dd00b4e43831cbfce9804515d97d575", size = 11517716, upload-time = "2025-10-21T14:52:06.013Z" }, - { url = "https://files.pythonhosted.org/packages/39/73/d2fc40c043bac699c3880bf88d3cebe9d88410cd043795382826c93a89f0/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20f2699d61d724de3eb3f3369d57e2b245f93085cab44fd37c3bea036cea1a6f", size = 11565056, upload-time = "2025-10-21T14:52:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, - { url = "https://files.pythonhosted.org/packages/ab/52/a30f46e822bfa6b4a659d1e8de8c4a4adf908ea075dac568b55362541bd8/cuda_bindings-12.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:53e11991a92ff6f26a0c8a98554cd5d6721c308a6b7bfb08bebac9201e039e43", size = 12055608, upload-time = "2025-10-21T14:52:12.335Z" }, + { url = "https://files.pythonhosted.org/packages/32/45/557d4ed1fa54f0c7db8aee083229f624990d69f7d00f55477eed5c7e169a/cuda_bindings-12.9.7-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0666d3c082ef8f4b2d670950589373550e9f3bf564d635dd883f24a0b40402ff", size = 7071026, upload-time = "2026-05-27T18:44:13.356Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/e3c6e58ece26a053419ba0a18444b5443cfc64451bbf37f84e8143b8bdca/cuda_bindings-12.9.7-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c7ef48c5e13ae90f3b2ecfb72f8e99ac43c8f4c43e67e1325b8aae331453687", size = 7611059, upload-time = "2026-05-27T18:44:15.252Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/afaa3de4d491a55af8961081e0b69c08d51bfbe471c359a7bddb4a28ca41/cuda_bindings-12.9.7-cp312-cp312-win_amd64.whl", hash = "sha256:3c089aaf4f5f570ec50244c68f5a2b00a2c9a8e01e04219fd2e36e340be0d88b", size = 7400841, upload-time = "2026-05-27T18:44:17.164Z" }, + { url = "https://files.pythonhosted.org/packages/eb/7b/f1575e41e1a17dc2f2a408b2e8e864c9324e41e3e23f6401e5efc54c152a/cuda_bindings-12.9.7-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:266379e4942051f544a8e7ea1a30ead8d7e8199b6b30fcdc8917cae2bf614e61", size = 6978549, upload-time = "2026-05-27T18:44:18.839Z" }, + { url = "https://files.pythonhosted.org/packages/9d/dc/62d62eb4f91eb721bcf46da51b13e9872ccd8fa7e60eb8ba7b7baeac72c6/cuda_bindings-12.9.7-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59cf4a37b0d662ba15037c9ceebe1a306ebf2c01a8235a09be13cd07094fdb74", size = 7457675, upload-time = "2026-05-27T18:44:20.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/753fe88151001d0dc23f56a8e119fe06b991b0d1a885fa02f9852b12f523/cuda_bindings-12.9.7-cp313-cp313-win_amd64.whl", hash = "sha256:5bd89dcb78475a6d8a4620ea94b74edf0cbbeacee6d1622d8f94452c1e8d3f15", size = 7360097, upload-time = "2026-05-27T18:44:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/f9/77/94d9b85f26add6fe9c9cb7c4ec3b96bc598f7ea5cfbd7490cc0a36adf5be/cuda_bindings-12.9.7-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dbcd4801954eb3508f4dc2fa0d0c8eb93eb3f45326fd61be2731418c371e7a0", size = 6870886, upload-time = "2026-05-27T18:44:24.164Z" }, + { url = "https://files.pythonhosted.org/packages/04/dd/3ec34b569e1b990b11276feba306bf8f446656cc38e8ed0f49b5facfeffa/cuda_bindings-12.9.7-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3747ea132642416786a8e31bf229032df3a7856911ae5426a7be53d032df183d", size = 7345663, upload-time = "2026-05-27T18:44:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c8/d79a20ba396e7ab2dfdd4b72b62356972b25b88aee2ded49a70c797ddea1/cuda_bindings-12.9.7-cp313-cp313t-win_amd64.whl", hash = "sha256:64f7ade7a7a3b69001489753acc21706d9dbda32db8deb68a767a0a0aab30b68", size = 7780136, upload-time = "2026-05-27T18:44:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/68/e4/075052d42872cf8162da53f14447a4b8abc004c3750e4b724ee502428da0/cuda_bindings-12.9.7-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:775960ac9e530717f3b48e165cc6f68684fa9a4141764fd923e4c1a9820acc73", size = 7060090, upload-time = "2026-05-27T18:44:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/ec/cd/3289c810a4d45e5364a3387a74b4c9b6f6f57ee96ae0e5b537cc61dec242/cuda_bindings-12.9.7-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c47ec1a7a441d91aab32339951df7a1be53451121a12c094bba51467717a35a", size = 7504419, upload-time = "2026-05-27T18:44:31.992Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a0/c429fdcfa5aae181415504c5085ea5944f782b417dd16a7f2a14be0da80d/cuda_bindings-12.9.7-cp314-cp314-win_amd64.whl", hash = "sha256:1e2a4f2ec5b67408c04bb4fbed45d214b66de1f00ee2e972865cacb8708d4e1e", size = 7493876, upload-time = "2026-05-27T18:44:33.618Z" }, + { url = "https://files.pythonhosted.org/packages/11/43/472a6281c3d94e71687e27c657a8f60718d3579b4d94c41deea503165f8a/cuda_bindings-12.9.7-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00a833d399b31071fab4cf3de2929840ae462dc4848116eeff033d09219e7116", size = 6899146, upload-time = "2026-05-27T18:44:35.556Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/10c1d0b32a9da65142d213e0733d748457fb3fd066aee4317335266f15c6/cuda_bindings-12.9.7-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11aeafa2b33995f890086b3fb0f062075176d956e9b6a6fe1a699dddc413f6ad", size = 7369087, upload-time = "2026-05-27T18:44:37.359Z" }, + { url = "https://files.pythonhosted.org/packages/33/10/c71a07cd2a1d4db119bada1848b4752a874ccfe4927d419bfdd05f250920/cuda_bindings-12.9.7-cp314-cp314t-win_amd64.whl", hash = "sha256:ece8dfbc22e6de96a26940ab9887eb3cfe1fc1bc3966169391cdb866bb82bb64", size = 8208198, upload-time = "2026-05-27T18:44:39.053Z" }, ] [[package]] name = "cuda-pathfinder" -version = "1.4.4" +version = "1.5.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/66/7b2c3d23dac4bb9629b4d9702f1f796bd41c01142c2b47be6fcfdeaf4ee4/cuda_pathfinder-1.4.4-py3-none-any.whl", hash = "sha256:1a9e7feccae0d969ad88545d0462f2ed2750df8e6732309798dc1e1ca603a28b", size = 48834, upload-time = "2026-03-23T20:50:00.706Z" }, + { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" }, ] [[package]] name = "cuda-python" -version = "12.9.4" +version = "12.9.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-bindings" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/af/f3/6b032a554019cfb3447e671798c1bd3e79b5f1af20d10253f56cea269ef2/cuda_python-12.9.4-py3-none-any.whl", hash = "sha256:d2cacea882a69863f1e7d27ee71d75f0684f4c76910aff839067e4f89c902279", size = 7594, upload-time = "2025-10-21T14:55:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9d/05e753afbaac3f92691059b3ba875589c98a425d69e5808cec32b31b580c/cuda_python-12.9.7-py3-none-any.whl", hash = "sha256:23a1fc406d491eef7a7e985095725cb7b20a04a7bd9b7a66400e5c86e082e0aa", size = 7597, upload-time = "2026-05-27T19:50:32.605Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "12.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/c8/7dce3a0b15b42a3b58e7d96eb22a687d3bf2c44e01d149a6874629cd9938/cuda_toolkit-12.8.1-py2.py3-none-any.whl", hash = "sha256:adc7906af4ecbf9a352f9dca5734eceb21daec281ccfcf5675e1d2f724fc2cba", size = 2283, upload-time = "2025-08-13T02:03:07.842Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile-cu12", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1282,16 +1316,16 @@ wheels = [ [[package]] name = "databricks-sdk" -version = "0.102.0" +version = "0.112.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/b3/41ff1c3afe092df9085e084e0dc81c45bca5ed65f7b60dc59df0ade43c76/databricks_sdk-0.102.0.tar.gz", hash = "sha256:8fa5f82317ee27cc46323c6e2543d2cfefb4468653f92ba558271043c6f72fb9", size = 887450, upload-time = "2026-03-19T08:15:54.428Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/b7/8b5579a3abce4c8b3b677bcab757d712df7bf4fff732c85dfa1d800180f1/databricks_sdk-0.112.0.tar.gz", hash = "sha256:39ed2fc6a0a1110e64ad8903a471daea0570ca544811ba88163bbb199a67dea7", size = 954943, upload-time = "2026-05-27T09:16:24.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/8c/d082bd5f72d7613524d5b35dfe1f71732b2246be2704fad68cd0e3fdd020/databricks_sdk-0.102.0-py3-none-any.whl", hash = "sha256:75d1253276ee8f3dd5e7b00d62594b7051838435e618f74a8570a6dbd723ec12", size = 838533, upload-time = "2026-03-19T08:15:52.248Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/d7f2bc1125a0e68a3554cb96e656b117980633656921cbc03830d8839cc1/databricks_sdk-0.112.0-py3-none-any.whl", hash = "sha256:2121c0852eef39c20d6381e6a2ac52f580610b268891722e39a3b53d92da78b7", size = 901369, upload-time = "2026-05-27T09:16:22.893Z" }, ] [[package]] @@ -1348,11 +1382,11 @@ wheels = [ [[package]] name = "decorator" -version = "5.2.1" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] [[package]] @@ -1370,20 +1404,17 @@ wheels = [ ] [[package]] -name = "deprecated" -version = "1.3.1" +name = "detect-installer" +version = "0.1.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/ce/6897d812825e9d4c53e3c7112726e800cc5231b013b2223bf64f653ff362/detect_installer-0.1.0.tar.gz", hash = "sha256:00ad7ba0a36e3cf7d08a40d3643011746dbc112597c7d475cc91c416710ca4e7", size = 3049, upload-time = "2026-02-23T10:40:22.567Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, + { url = "https://files.pythonhosted.org/packages/cc/34/8cc73273414405086c58852916e4031812a6a30fe04c057e37ad99397b7f/detect_installer-0.1.0-py3-none-any.whl", hash = "sha256:034fb20fd665c36e6ba52b8821525ea07fb4f7f938cac459df889fb33801528a", size = 4539, upload-time = "2026-02-23T10:40:23.807Z" }, ] [[package]] name = "diffusers" -version = "0.37.0" +version = "0.37.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -1396,9 +1427,9 @@ dependencies = [ { name = "requests" }, { name = "safetensors" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/3b/01d0ff800b811c5ad8bba682f4c6abf1d7071cd81464c01724333fefb7ba/diffusers-0.37.0.tar.gz", hash = "sha256:408789af73898585f525afd07ca72b3955affea4216a669558e9f59b5b1fe704", size = 4141136, upload-time = "2026-03-05T14:58:39.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/5c/f4c2eb8d481fe8784a7e2331fbaab820079c06676185fa6d2177b386d590/diffusers-0.37.1.tar.gz", hash = "sha256:2346c21f77f835f273b7aacbaada1c34a596a3a2cc6ddc99d149efcd0ec298fa", size = 4135139, upload-time = "2026-03-25T08:04:04.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/55/586a3a2b9c95f371c9c3cb048c3cac15aedcce8d6d53ebd6bbc46860722d/diffusers-0.37.0-py3-none-any.whl", hash = "sha256:7eab74bf896974250b5e1027cae813aba1004f02d97c9b44891b83713386aa08", size = 5000449, upload-time = "2026-03-05T14:58:37.361Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/51c38785ce5e1c287b5ad17ba550edaaaffce0deb0da4857019c6700fbaf/diffusers-0.37.1-py3-none-any.whl", hash = "sha256:0537c0b28cb53cf39d6195489bcf8f833986df556c10f5e28ab7427b86fc8b90", size = 5001536, upload-time = "2026-03-25T08:04:02.385Z" }, ] [[package]] @@ -1462,11 +1493,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] @@ -1480,31 +1511,31 @@ wheels = [ [[package]] name = "duckdb" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/62/590caabec6c41003f46a244b6fd707d35ca2e552e0c70cbf454e08bf6685/duckdb-1.5.1.tar.gz", hash = "sha256:b370d1620a34a4538ef66524fcee9de8171fa263c701036a92bc0b4c1f2f9c6d", size = 17995082, upload-time = "2026-03-23T12:12:15.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/06/be4c62f812c6e23898733073ace0482eeb18dffabe0585d63a3bf38bca1e/duckdb-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6f7361d66cc801d9eb4df734b139cd7b0e3c257a16f3573ebd550ddb255549e6", size = 30113703, upload-time = "2026-03-23T12:11:02.536Z" }, - { url = "https://files.pythonhosted.org/packages/44/03/1794dcdda75ff203ab0982ff7eb5232549b58b9af66f243f1b7212d6d6be/duckdb-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6acc2040bec1f05de62a2f3f68f4c12f3ec7d6012b4317d0ab1a195af26225", size = 15991802, upload-time = "2026-03-23T12:11:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/87/03/293bccd838a293d42ea26dec7f4eb4f58b57b6c9ffcfabc6518a5f20a24a/duckdb-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed6d23a3f806898e69c77430ebd8da0c79c219f97b9acbc9a29a653e09740c59", size = 14246803, upload-time = "2026-03-23T12:11:09.624Z" }, - { url = "https://files.pythonhosted.org/packages/15/2c/7b4f11879aa2924838168b4640da999dccda1b4a033d43cb998fd6dc33ea/duckdb-1.5.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6af347debc8b721aa72e48671166282da979d5e5ae52dbc660ab417282b48e23", size = 19271654, upload-time = "2026-03-23T12:11:13.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d6/8f9a6b1fbcc669108ec6a4d625a70be9e480b437ed9b70cd56b78cd577a6/duckdb-1.5.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8150c569b2aa4573b51ba8475e814aa41fd53a3d510c1ffb96f1139f46faf611", size = 21386100, upload-time = "2026-03-23T12:11:16.758Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fe/8d02c6473273468cf8d43fd5d73c677f8cdfcd036c1e884df0613f124c2b/duckdb-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:054ad424b051b334052afac58cb216f3b1ebb8579fc8c641e60f0182e8725ea9", size = 13083506, upload-time = "2026-03-23T12:11:19.785Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/2be786b9c153eb263bf5d3d5f7ab621b14a715d7e70f92b24ecf8536369e/duckdb-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:6ba302115f63f6482c000ccfd62efdb6c41d9d182a5bcd4a90e7ab8cd13856eb", size = 13888862, upload-time = "2026-03-23T12:11:22.84Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f2/af476945e3b97417945b0f660b5efa661863547c0ea104251bb6387342b1/duckdb-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:26e56b5f0c96189e3288d83cf7b476e23615987902f801e5788dee15ee9f24a9", size = 30113759, upload-time = "2026-03-23T12:11:26.5Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9d/5a542b3933647369e601175190093597ce0ac54909aea0dd876ec51ffad4/duckdb-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:972d0dbf283508f9bc446ee09c3838cb7c7f114b5bdceee41753288c97fe2f7c", size = 15991463, upload-time = "2026-03-23T12:11:30.025Z" }, - { url = "https://files.pythonhosted.org/packages/53/a5/b59cff67f5e0420b8f337ad86406801cffacae219deed83961dcceefda67/duckdb-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:482f8a13f2600f527e427f73c42b5aa75536f9892868068f0aaf573055a0135f", size = 14246482, upload-time = "2026-03-23T12:11:33.33Z" }, - { url = "https://files.pythonhosted.org/packages/e9/12/d72a82fe502aae82b97b481bf909be8e22db5a403290799ad054b4f90eb4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da137802688190835b4c863cafa77fd7e29dff662ee6d905a9ffc14f00299c91", size = 19270816, upload-time = "2026-03-23T12:11:36.79Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c3/ee49319b15f139e04c067378f0e763f78336fbab38ba54b0852467dd9da4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d4147422d91ccdc2d2abf6ed24196025e020259d1d267970ae20c13c2ce84b1", size = 21385695, upload-time = "2026-03-23T12:11:40.465Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f5/a15498e75a27a136c791ca1889beade96d388dadf9811375db155fc96d1a/duckdb-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:05fc91767d0cfc4cf2fa68966ab5b479ac07561752e42dd0ae30327bd160f64a", size = 13084065, upload-time = "2026-03-23T12:11:43.763Z" }, - { url = "https://files.pythonhosted.org/packages/93/81/b3612d2bbe237f75791095e16767c61067ea5d31c76e8591c212dac13bd0/duckdb-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:a28531cee2a5a42d89f9ba4da53bfeb15681f12acc0263476c8705380dadce07", size = 13892892, upload-time = "2026-03-23T12:11:47.222Z" }, - { url = "https://files.pythonhosted.org/packages/ad/75/e9e7893542ca738bcde2d41d459e3438950219c71c57ad28b049dc2ae616/duckdb-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:eba81e0b3011c1f23df7ea47ef4ffaa8239817959ae291515b6efd068bde2161", size = 30123677, upload-time = "2026-03-23T12:11:51.511Z" }, - { url = "https://files.pythonhosted.org/packages/df/db/f7420ee7109a922124c02f377ae1c56156e9e4aa434f4726848adaef0219/duckdb-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:afab8b4b1f4469c3879bb049dd039f8fce402712050324e9524a43d7324c5e87", size = 15996808, upload-time = "2026-03-23T12:11:54.964Z" }, - { url = "https://files.pythonhosted.org/packages/df/57/2c4c3de1f1110417592741863ba58b4eca2f7690a421712762ddbdcd72e6/duckdb-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:71dddcebbc5a70e946a06c30b59b5dd7999c9833d307168f90fb4e4b672ab63e", size = 14248990, upload-time = "2026-03-23T12:11:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e173b33ffac53124a3e39e97fb60a538f26651a0df6e393eb9bf7540126c/duckdb-1.5.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac2804043bd1bc10b5da18f8f4c706877197263a510c41be9b4c0062f5783dcc", size = 19276013, upload-time = "2026-03-23T12:12:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/d4/4c/47e838393aa90d3d78549c8c04cb09452efeb14aaae0ee24dc0bd61c3a41/duckdb-1.5.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8843bd9594e1387f1e601439e19ad73abdf57356104fd1e53a708255bb95a13d", size = 21387569, upload-time = "2026-03-23T12:12:05.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9b/ce65743e0e85f5c984d2f7e8a81bc908d0bac345d6d8b6316436b29430e7/duckdb-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:d68c5a01a283cb13b79eafe016fe5869aa11bff8c46e7141c70aa0aac808010f", size = 13603876, upload-time = "2026-03-23T12:12:09.344Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ac/f9e4e731635192571f86f52d86234f537c7f8ca4f6917c56b29051c077ef/duckdb-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:a3be2072315982e232bfe49c9d3db0a59ba67b2240a537ef42656cc772a887c7", size = 14370790, upload-time = "2026-03-23T12:12:12.497Z" }, +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/00/d579dcb2a536b6ea3a2563cdad6844f77d81a9b2d4b22a858097f2468acf/duckdb-1.5.3.tar.gz", hash = "sha256:df39428eb130faa35ae96fd35245bdeae6ecf43936250b116b5fead568eb9f16", size = 18026640, upload-time = "2026-05-20T11:55:31.901Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c4/2e34929b16c8d544ef664fad8f7f3a2a9db05746aae1e7c8c4ee3a8b23e4/duckdb-1.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ff11a457258148337ef9a392148a8cdbd1069b6c27c21958816c7b67fe6c542d", size = 32626494, upload-time = "2026-05-20T11:54:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/3a/53/3af681793d03771365ae3e2215331151c196a3ac8193f613344840694671/duckdb-1.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fd25f533cb1b6b2c84cc767a9a9bab7769bb1aa44571a2a0bfc91ac3e4a38ac", size = 17301121, upload-time = "2026-05-20T11:54:36.928Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/c80af1eac2ab5d35fc2c372ef0a84668842e549fbbf7799277b3fccf3e39/duckdb-1.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10960400ed60cdf0fe05bab2086fa8eb733889cb0ceca18d07ff9a00c0e0be7b", size = 15449283, upload-time = "2026-05-20T11:54:39.777Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9a/c63af233c9f761bf5178a5210437e1bc6bcb30fa8a9073de6398cfb12c03/duckdb-1.5.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5f18e7561403054433706c187589e86629a7af09a7efc23a06a8b308e6acc68", size = 19332762, upload-time = "2026-05-20T11:54:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/21/cc/2d77af4fff86012f334ef82e6d54a995a86c8745e58074f1218ed7d25171/duckdb-1.5.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fb7516255a8764545e30f7efacea408cc847764a3027b3b0b3e7d1a7bebbc5c", size = 21453290, upload-time = "2026-05-20T11:54:45.272Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/9bc4817a98feb4dab83e56f2245cd3a30d00ee646d4dec7926464e2b3f28/duckdb-1.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:8001eccbc28be244dfd04d708526f34ddd6460b47a8aeb5d0e39d6f7f9e3fe15", size = 13118308, upload-time = "2026-05-20T11:54:48.058Z" }, + { url = "https://files.pythonhosted.org/packages/81/35/e3f32e4e53e2450ddb1db8312a17d1ce455d60cc4941b6ad2cfc908794b0/duckdb-1.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:6d2835e39bb6af73891f73c0f8d4324f98afe00d0b00c6d34b2a582c2256cbb0", size = 13927187, upload-time = "2026-05-20T11:54:50.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/a528eb09d8be51954c485864bd06753e616939a080cbc3dd4417e8c94a57/duckdb-1.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e75a6122c12579a99848517f6f00a4e342aebda3590c30fe9b5cc5f39d5e6afc", size = 32626254, upload-time = "2026-05-20T11:54:53.65Z" }, + { url = "https://files.pythonhosted.org/packages/ec/3c/1534c0a6db347c05eb7d0f6ecfb7aefbe74cbff398e4892a8fd1903a20e8/duckdb-1.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fd3963c1cb9d9567777f4a898a9dbe388a2fe9724681801b1e7d6d93eecf1b76", size = 17300917, upload-time = "2026-05-20T11:54:56.628Z" }, + { url = "https://files.pythonhosted.org/packages/23/fa/beafb91e6e152d2161c4a9cbc472334c87607eb61ad7104b5a7fa8d8d7b1/duckdb-1.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d5db8c0b55e072cf437948ebb5d7e23d7b9d03d905fa5f9145583e65aa447f7", size = 15449411, upload-time = "2026-05-20T11:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/50/0a/49b6fe04e2fcd63729eb607dadd44818dde77342a4f5ce086c6c92f1dd4d/duckdb-1.5.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ce80aed7a538422129a57eaca9141e3afb51f8bf562b1908b1576c9725b5b22", size = 19333120, upload-time = "2026-05-20T11:55:01.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/4c/0907c3f76adb9dd90e67610b31e0304a35814e65c4c41a354a262c09b885/duckdb-1.5.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e", size = 21453266, upload-time = "2026-05-20T11:55:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/d2f23a7803ddbbd9413f7572ecf66a15120ed5ced7ce5c73e698c1406b76/duckdb-1.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb5bb5dcdd09d62ee60f0ddbbef918e71cce304ffe28428b1131949d39ffaabf", size = 13118640, upload-time = "2026-05-20T11:55:07.389Z" }, + { url = "https://files.pythonhosted.org/packages/27/d5/7ba2316415bcdab6edd765bbbe35c2ca8a3800f2fe695cd70e3cdb997f09/duckdb-1.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2fa17ecdd5d3db122836cb71bb93601c2106a3be883c17dffddc02fbf3fa7888", size = 13926409, upload-time = "2026-05-20T11:55:10.166Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c2/d4b6f8a5e4d3bc25773be6da76a99d9661ebbf3552c007c460d2dd59dbf8/duckdb-1.5.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4bfa9a4dadf71e83e2c4eaca2f9421c82a54defecc1b0b4c0be95e2389dec4fe", size = 32636685, upload-time = "2026-05-20T11:55:13.158Z" }, + { url = "https://files.pythonhosted.org/packages/42/58/e835c8298979d29db7a62cb5acc29e9b57aeaca7cdde2fcd3ac980f5cb18/duckdb-1.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aea7baf67ad7e1829ac76f67d7dcbd7fb1f57c3eb179d55ac30952df4709ae30", size = 17308134, upload-time = "2026-05-20T11:55:16.194Z" }, + { url = "https://files.pythonhosted.org/packages/c9/46/617b51363f5613418c8b224b3cce16b58e6dde80904566bec232579c1d4e/duckdb-1.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b0b4f088a65d77e1217ce5d7eff889e63fedc44281200d899ff47c84d8ff836", size = 15449891, upload-time = "2026-05-20T11:55:18.687Z" }, + { url = "https://files.pythonhosted.org/packages/b3/72/354146656e8d9ba3853d3a5ee80a481b8c5f70edfc3d5ae80a8c4479c967/duckdb-1.5.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe8d0c1f6a120aa03fa6e0d03897c71a1842e6cf7afd31d181348391f7108fe1", size = 19338499, upload-time = "2026-05-20T11:55:21.34Z" }, + { url = "https://files.pythonhosted.org/packages/56/8f/65fc623b51448f2bfba1a9ec6ab3debb4664c0876c0113a5e782600b53ac/duckdb-1.5.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0405eae18ec6e8210a471c97dbfe87a7e4d605274b7fe572a1f276e92158f13", size = 21455828, upload-time = "2026-05-20T11:55:23.847Z" }, + { url = "https://files.pythonhosted.org/packages/2b/db/d0274cbe9f5fe219f77c0bdf900ac77103569e83c102a4225ce04cbc607d/duckdb-1.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:33ae08b3e818d7613d8936744b67718c2062c2f530376895bfd89efb51b81538", size = 13640011, upload-time = "2026-05-20T11:55:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/07/5d/8f1899b8bef291caf953992fcd6c24df9f29387a35645e58c2504a5ca473/duckdb-1.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:746433e49bbc667b4df283153415fbe37e9083e0eff6c3cd6e54de7536869cd4", size = 14411554, upload-time = "2026-05-20T11:55:29.037Z" }, ] [[package]] @@ -1598,7 +1629,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.135.2" +version = "0.136.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -1607,9 +1638,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/73/5903c4b13beae98618d64eb9870c3fac4f605523dd0312ca5c80dadbd5b9/fastapi-0.135.2.tar.gz", hash = "sha256:88a832095359755527b7f63bb4c6bc9edb8329a026189eed83d6c1afcf419d56", size = 395833, upload-time = "2026-03-23T14:12:41.697Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, + { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, ] [package.optional-dependencies] @@ -1648,9 +1679,10 @@ standard = [ [[package]] name = "fastapi-cloud-cli" -version = "0.15.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "detect-installer" }, { name = "fastar" }, { name = "httpx" }, { name = "pydantic", extra = ["email"] }, @@ -1660,81 +1692,81 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/e1/05c44e7bbc619e980fab0236cff9f5f323ac1aaa79434b4906febf98b1d3/fastapi_cloud_cli-0.15.0.tar.gz", hash = "sha256:d02515231f3f505f7669c20920343934570a88a08af9f9a6463ca2807f27ffe5", size = 45309, upload-time = "2026-03-11T22:31:32.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/1d/57221a834b0f62dfa510c2b3db6e9b682cfbc280cef41919a8811ce1ff89/fastapi_cloud_cli-0.18.0.tar.gz", hash = "sha256:95f7a79200e3a90a005e068a4d8ede49d4b04accb095ccd4fd47da998fc28c74", size = 53320, upload-time = "2026-05-22T09:53:54.462Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/cc/1ccca747f5609be27186ea8c9219449142f40e3eded2c6089bba6a6ecc82/fastapi_cloud_cli-0.15.0-py3-none-any.whl", hash = "sha256:9ffcf90bd713747efa65447620d29cfbb7b3f7de38d97467952ca6346e418d70", size = 32267, upload-time = "2026-03-11T22:31:33.499Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/1d54aabf71c003e89e73df92c3dfded311228e68db7cea5db90b3e0ef2b5/fastapi_cloud_cli-0.18.0-py3-none-any.whl", hash = "sha256:1f136fc651b0b6e2f4a9679e23c56e1c3be3405e74469c14ba6e2d5b87fdc113", size = 37087, upload-time = "2026-05-22T09:53:53.001Z" }, ] [[package]] name = "fastar" -version = "0.9.0" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/00/dab9ca274cf1fde19223fea7104631bea254751026e75bf99f2b6d0d1568/fastar-0.9.0.tar.gz", hash = "sha256:d49114d5f0b76c5cc242875d90fa4706de45e0456ddedf416608ecd0787fb410", size = 70124, upload-time = "2026-03-20T14:26:34.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/9b/300bc0dafa8495718976076db216f42d57b251a582589566a63b4ed2cb82/fastar-0.9.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7a8b5daa50d9b4c07367dffc40880467170bf1c31ca63a2286506edbe6d3d65b", size = 706914, upload-time = "2026-03-20T14:25:32.501Z" }, - { url = "https://files.pythonhosted.org/packages/95/97/f1e34c8224dc373c6fab5b33e33be0d184751fdc27013af3278b1e4e6e6c/fastar-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ec841a69fea73361c6df6d9183915c09e9ce3bd96493763fa46019e79918400", size = 627422, upload-time = "2026-03-20T14:25:20.318Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ad/e2499d136e24c2d896f2ec58183c91c6f8185d758177537724ed2f3e1b54/fastar-0.9.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad46bc23040142e9be4b4005ea366834dbf0f1b6a90b8ecdc3ec96c42dec4adf", size = 865265, upload-time = "2026-03-20T14:24:55.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/b6ad68b2ab1d7b74b0d38725d817418016bdd64880b36108be80d2460b4d/fastar-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de264da9e8ef6407aa0b23c7c47ed4e34fde867e7c1f6e3cb98945a93e5f89f2", size = 760583, upload-time = "2026-03-20T14:23:50.447Z" }, - { url = "https://files.pythonhosted.org/packages/b8/96/086116ad46e3b98f6c217919d680e619f2857ffa6b5cc0d7e46e4f214b83/fastar-0.9.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75c70be3a7da3ff9342f64c15ec3749c13ef56bc28e69075d82d03768532a8d0", size = 758000, upload-time = "2026-03-20T14:24:03.471Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e6/ea642ea61eea98d609343080399a296a9ff132bd0492a6638d6e0d9e41a7/fastar-0.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a734506b071d2a8844771fe735fbd6d67dd0eec80eef5f189bbe763ebe7a0b8", size = 923647, upload-time = "2026-03-20T14:24:16.875Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/53874aad61e4a664af555a2aa7a52fe46cfadd423db0e592fa0cfe0fa668/fastar-0.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eac084ab215aaf65fa406c9b9da1ac4e697c3d3a1a183e09c488e555802f62d", size = 816528, upload-time = "2026-03-20T14:24:42.048Z" }, - { url = "https://files.pythonhosted.org/packages/41/df/d663214d35380b07a24a796c48d7d7d4dc3a28ec0756edbcb7e2a81dc572/fastar-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb62e2369834fb23d26327157f0a2dbec40b230c709fa85b1ce96cf010e6fbf", size = 819050, upload-time = "2026-03-20T14:25:08.352Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5a/455b53f11527568100ba6d5847635430645bad62d676f0bae4173fc85c90/fastar-0.9.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f2f399fffb74bcd9e9d4507e253ace2430b5ccf61000596bda41e90414bcf4f2", size = 885257, upload-time = "2026-03-20T14:24:28.86Z" }, - { url = "https://files.pythonhosted.org/packages/4f/dd/0a8ea7b910293b07f8c82ef4e6451262ccf2a6f2020e880f184dc4abd6c2/fastar-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87006c8770dfc558aefe927590bbcdaf9648ca4472a9ee6d10dfb7c0bda4ce5b", size = 968135, upload-time = "2026-03-20T14:25:45.614Z" }, - { url = "https://files.pythonhosted.org/packages/6b/cb/5c7e9231d6ba00e225623947068db09ddd4e401800b0afaf39eece14bfee/fastar-0.9.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d012644421d669d9746157193f4eafd371e8ae56ff7aef97612a4922418664c", size = 1034940, upload-time = "2026-03-20T14:25:58.893Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b4/eccfcf7fe9d2a0cea6d71630acc48a762404058c9b3ae1323f74abcda005/fastar-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:094fd03b2e41b20a2602d340e2b52ad10051d82caa1263411cf247c1b1bc139f", size = 1073807, upload-time = "2026-03-20T14:26:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/8b/53/6ddda28545b428d54c42f341d797046467c689616a36eae9a43ba56f2545/fastar-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:59bc500d7b6bdaf2ffb2b632bc6b0f97ddfb3bb7d31b54d61ceb00b5698d6484", size = 1025314, upload-time = "2026-03-20T14:26:24.624Z" }, - { url = "https://files.pythonhosted.org/packages/03/cf/71e2a67b0a69971044ad57fe7d196287ac32ab710bfc47f34745bb4a7834/fastar-0.9.0-cp312-cp312-win32.whl", hash = "sha256:25a1fd512ce23eb5aaab514742e7c6120244c211c349b86af068c3ae35792ec3", size = 452740, upload-time = "2026-03-20T14:26:56.604Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/0ffa2fffac0d80d2283db577ff23f8d91886010ea858c657f8278c2a222c/fastar-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:b10a409797d01ee4062547e95e4a89f6bb52677b144076fd5a1f9d28d463ab10", size = 485282, upload-time = "2026-03-20T14:26:44.926Z" }, - { url = "https://files.pythonhosted.org/packages/14/20/999d72dc12e793a6c7889176fc42ad917d568d802c91b4126629e9be45a9/fastar-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea4d98fc62990986ce00d2021f08ff2aa6eae71636415c5a5f65f3a6a657dc5e", size = 461795, upload-time = "2026-03-20T14:26:36.728Z" }, - { url = "https://files.pythonhosted.org/packages/9a/26/ea9339facfe4ee224be673c6888dbf077f28b0f81185f80353966c9f4925/fastar-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7b55ae4a3a481fd90a63ac558a7e8aab652ac1dfd15d8657266e71bf65346408", size = 706740, upload-time = "2026-03-20T14:25:33.741Z" }, - { url = "https://files.pythonhosted.org/packages/77/52/f3b06867e5ca8d5b2c1c15a1563415e0037b5831f2058ee72b03960296d9/fastar-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f07c6bdeedfeb30ef459f21fa9ab06e2b6727f7e7653176d3abb7a85f447c400", size = 627615, upload-time = "2026-03-20T14:25:21.608Z" }, - { url = "https://files.pythonhosted.org/packages/52/32/021b0a633bca18bca4f831392c2938c15c4605de2d9895b783ad6d64679c/fastar-0.9.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:90f46492e05141089766699e95c79d470e8013192fbbb16ef16b576281f3b8ee", size = 864584, upload-time = "2026-03-20T14:24:56.941Z" }, - { url = "https://files.pythonhosted.org/packages/3f/54/e2e1b4c8512d670373047e5e585b1d1ff9ffd722b0a17647d22c9c9bd248/fastar-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:108bb46c080ca152bb331f1e0576177d36e9badba51b1d5724d2823542e0dd1f", size = 760246, upload-time = "2026-03-20T14:23:51.964Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7d/1e283dd8dbb3647049594bb477bdc053045c6fff2d3f06386d2dcacce7aa/fastar-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d17d311cfbb559154ba940972b6d07a3a7ac221a2a01208f119ad03495f01d32", size = 757024, upload-time = "2026-03-20T14:24:04.69Z" }, - { url = "https://files.pythonhosted.org/packages/87/ac/82d3cb64d318ce16c5d1a26a40b8aa570fcc9b23684221aece838c4cbada/fastar-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2ef34e7088f308e73460e1b8d9b0479a743f679816782a80db6ae87ee68714a", size = 921630, upload-time = "2026-03-20T14:24:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b8/3e7892f1a25a1a2054a20de6c846c0794b8fa361e5b9d3d00915b41e97bd/fastar-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c93bf4732d0dd6adae4a8b3bbebe19af76ee1072b7688bf39c5a1d120425a772", size = 815791, upload-time = "2026-03-20T14:24:43.28Z" }, - { url = "https://files.pythonhosted.org/packages/db/5e/8fcc662db1fd0985f4f8a54e79276416565a0d1fcb8da66665b2061ead30/fastar-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a67b061b1099cf3b8b6234dd3605fa16f5078ab6b51c8d77ad7a5d11c3cf834", size = 818980, upload-time = "2026-03-20T14:25:09.545Z" }, - { url = "https://files.pythonhosted.org/packages/68/ed/37291fbd6c9b5b0905712da6191bdfc25a7dc236efbf130e3a1a7d1b9440/fastar-0.9.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:912efe3121dc1f3c05940cfa1c6b09b8868d702d24566506aa1d0d96e429923a", size = 884578, upload-time = "2026-03-20T14:24:30.584Z" }, - { url = "https://files.pythonhosted.org/packages/94/19/7b3b7af978ae4f012664781554716d67549ab19ddbcb6e6d1adc04d7a5e7/fastar-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2394980cc126a3263e115600bc4ff9e7320cddde83c99fc334ab530be5b7166e", size = 967790, upload-time = "2026-03-20T14:25:46.975Z" }, - { url = "https://files.pythonhosted.org/packages/e6/38/4cce2a8e529a7d3e99e427c9bbcccd7013ff6b3ba295613e6f1c573c9e6c/fastar-0.9.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d0aff74ea98642784c941d3cd8c35943258d4b9626157858901c5b181683339b", size = 1033892, upload-time = "2026-03-20T14:26:00.22Z" }, - { url = "https://files.pythonhosted.org/packages/1a/3f/86f25d79b1b369c2756ee338b76d1696a9cac3a737e819459b0ad7822ede/fastar-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3e8a1deaf490f4ec15eca7e66127ff89cdefd20217f358739d4b7b1cb322f663", size = 1072969, upload-time = "2026-03-20T14:26:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/10/4f/6ec0c123c15bbcb9a9b82e979dc81273789ebbfbb4a2b41a1a6941577c94/fastar-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c9bd8879ebf05aa247e60e454bb7568cbdd44f016b8c58e31e5398039403e61d", size = 1025768, upload-time = "2026-03-20T14:26:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d1/cbdcdb78ca034ed51a9f53c2650885873d8b06727452c1cc33f56ad0c66a/fastar-0.9.0-cp313-cp313-win32.whl", hash = "sha256:11b35e6453a2da8715dd8415b3999ea57805125493e44ce41a32404bf9a510a7", size = 452742, upload-time = "2026-03-20T14:26:58.014Z" }, - { url = "https://files.pythonhosted.org/packages/74/ee/138d2f8e3504232a279afa224d3e5922c15dc7126613e6c135cfc8e10ec9/fastar-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:10a1e7f7bfa1c6f03e4c657fdc0a32ebe42d8e48f681403dc0c67258e1cb5bef", size = 484917, upload-time = "2026-03-20T14:26:46.135Z" }, - { url = "https://files.pythonhosted.org/packages/db/ca/f518ee9dccc45097560a2cff245590c65b7b348171c8d2f2e487cf92a69f/fastar-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:e5484ac1415e0ca8bc7b69231e3e3afb52887fed10b839ca676767635a13f06f", size = 461202, upload-time = "2026-03-20T14:26:37.937Z" }, - { url = "https://files.pythonhosted.org/packages/cf/00/99700dd33273c118d7d9ab7ad5db6650b430448d4cfae62aec6ef6ca4cb7/fastar-0.9.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ccb2289f24ee6555330eb77149486d3a2ec8926450a96157dd20c636a0eec085", size = 707059, upload-time = "2026-03-20T14:25:35.086Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a4/4808dcfa8dddb9d7f50d830a39a9084d9d148ed06fcac8b040620848bc24/fastar-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2bfee749a46666785151b33980aef8f916e6e0341c3d241bde4d3de6be23f00c", size = 627135, upload-time = "2026-03-20T14:25:23.134Z" }, - { url = "https://files.pythonhosted.org/packages/da/cb/9c92e97d760d769846cae6ce53332a5f2a9246eb07b369ac2a4ebf10480c/fastar-0.9.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f6096ec3f216a21fa9ac430ce509447f56c5bd979170c4c0c3b4f3cb2051c1a8", size = 864974, upload-time = "2026-03-20T14:24:58.624Z" }, - { url = "https://files.pythonhosted.org/packages/84/38/9dadebd0b7408b4f415827db35169bbd0741e726e38e3afd3e491b589c61/fastar-0.9.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7a806e54d429f7f57e35dc709e801da8c0ba9095deb7331d6574c05ae4537ea", size = 760262, upload-time = "2026-03-20T14:23:53.275Z" }, - { url = "https://files.pythonhosted.org/packages/d6/7d/7afc5721429515aa0873b268513f656f905d27ff1ca54d875af6be9e9bc6/fastar-0.9.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9a06abf8c7f74643a75003334683eb6e94fabef05f60449b7841eeb093a47b0", size = 757575, upload-time = "2026-03-20T14:24:06.143Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5d/7498842c62bd6057553aa598cd175a0db41fdfeda7bdfde48dab63ffb285/fastar-0.9.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e9b5c155946f20ce3f999fb1362ed102876156ad6539e1b73a921f14efb758c", size = 924827, upload-time = "2026-03-20T14:24:19.364Z" }, - { url = "https://files.pythonhosted.org/packages/69/ab/13322e98fe1a00ed6efbfa5bf06fcfff8a6979804ef7fcef884b5e0c6f85/fastar-0.9.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdedac6a84ef9ebc1cee6d777599ad51c9e98ceb8ebb386159483dcd60d0e16", size = 816536, upload-time = "2026-03-20T14:24:44.844Z" }, - { url = "https://files.pythonhosted.org/packages/fe/fd/0aa5b9994c8dba75b73a9527be4178423cb926db9f7eca562559e27ccdfd/fastar-0.9.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51df60a2f7af09f75b2a4438b25cb903d8774e24c492acf2bca8b0863026f34c", size = 818686, upload-time = "2026-03-20T14:25:10.799Z" }, - { url = "https://files.pythonhosted.org/packages/46/d6/e000cd49ef85c11a8350e461e6c48a4345ace94fb52242ac8c1d5dad1dfc/fastar-0.9.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:15016d0da7dbc664f09145fc7db549ba8fe32628c6e44e20926655b82de10658", size = 885043, upload-time = "2026-03-20T14:24:32.231Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/ee734fe273475b9b25554370d92a21fc809376cf79aa072de29d23c17518/fastar-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c66a8e1f7dae6357be8c1f83ce6330febbc08e49fc40a5a2e91061e7867bbcbf", size = 967965, upload-time = "2026-03-20T14:25:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/165b3a75f1ee8045af9478c8aae5b5e20913cca2d4a5adb1be445e8d015a/fastar-0.9.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1c6829be3f55d2978cb62921ef4d7c3dd58fe68ee994f81d49bd0a3c5240c977", size = 1034507, upload-time = "2026-03-20T14:26:01.518Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4e/4097b5015da02484468c16543db2f8dec2fe827d321a798acbd9068e0f13/fastar-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:68db849e01d49543f31d56ef2fe15527afe2b9e0fb21794edc4d772553d83407", size = 1073388, upload-time = "2026-03-20T14:26:14.448Z" }, - { url = "https://files.pythonhosted.org/packages/07/d7/3b86af4e63a551398763a1bbbbac91e1c0754ece7ac7157218b33a065f4c/fastar-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5569510407c0ded580cfeec99e46ebe85ce27e199e020c5c1ea6f570e302c946", size = 1025190, upload-time = "2026-03-20T14:26:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/39/07/8c50a60f03e095053306fcf57d9d99343bce0e99d5b758bf96de31aec849/fastar-0.9.0-cp314-cp314-win32.whl", hash = "sha256:3f7be0a34ffbead52ab5f4a1e445e488bf39736acb006298d3b3c5b4f2c5915e", size = 452301, upload-time = "2026-03-20T14:26:59.234Z" }, - { url = "https://files.pythonhosted.org/packages/ee/69/aa6d67b09485ba031408296d6ff844c7d83cdcb9f8fcc240422c6f83be87/fastar-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf7f68b98ed34ce628994c9bbd4f56cf6b4b175b3f7b8cbe35c884c8efec0a5b", size = 484948, upload-time = "2026-03-20T14:26:48.45Z" }, - { url = "https://files.pythonhosted.org/packages/20/6d/dba29d87ca929f95a5a7025c7d30720ad8478beed29fff482f29e1e8b045/fastar-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:155dae97aca4b245eabb25e23fd16bfd42a0447f9db7f7789ab1299b02d94487", size = 461170, upload-time = "2026-03-20T14:26:39.191Z" }, - { url = "https://files.pythonhosted.org/packages/96/8f/c3ea0adac50a8037987ee7f15ff94767ebb604faf6008cbd2b8efa46c372/fastar-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a63df018232623e136178953031057c7ac0dbf0acc6f0e8c1dc7dbc19e64c22f", size = 705857, upload-time = "2026-03-20T14:25:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b3/e0e1aad1778065559680a73cdf982ed07b04300c2e5bf778dec8668eda6f/fastar-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6fb44f8675ef87087cb08f9bf4dfa15e818571a5f567ff692f3ea007cff867b5", size = 626210, upload-time = "2026-03-20T14:25:24.361Z" }, - { url = "https://files.pythonhosted.org/packages/94/f3/3c117335cbea26b3bc05382c27e6028278ed048d610b8de427c68f2fec84/fastar-0.9.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81092daa991d0f095424e0e28ed589e03c81a21eeddc9b981184ddda5869bf9d", size = 864879, upload-time = "2026-03-20T14:25:00.131Z" }, - { url = "https://files.pythonhosted.org/packages/26/5d/e8d00ec3b2692d14ea111ddae25bf10e0cb60d5d79915c3d8ea393a87d5c/fastar-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e8793e2618d0d6d5a7762d6007371f57f02544364864e40e6b9d304b0f151b2", size = 759117, upload-time = "2026-03-20T14:23:54.826Z" }, - { url = "https://files.pythonhosted.org/packages/1a/61/6e080fdbc28c72dded8b6ff396035d6dc292f9b1c67b8797ac2372ca5733/fastar-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83f7ef7056791fc95b6afa987238368c9a73ad0edcedc6bc80076f9fbd3a2a78", size = 756527, upload-time = "2026-03-20T14:24:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/e8/97/2cf1a07884d171c028bd4ae5ecf7ded6f31581f79ab26711dcdad0a3d5ab/fastar-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3a456230fcc0e560823f5d04ae8e4c867300d8ee710b14ddcdd1b316ac3dd8d", size = 921763, upload-time = "2026-03-20T14:24:20.787Z" }, - { url = "https://files.pythonhosted.org/packages/f6/e3/c1d698a45f9f5dc892ed7d64badc9c38f1e5c1667048191969c438d2b428/fastar-0.9.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a60b117ebadc46c10c87852d2158a4d6489adbfbbec37be036b4cfbeca07b449", size = 815493, upload-time = "2026-03-20T14:24:46.482Z" }, - { url = "https://files.pythonhosted.org/packages/25/38/e124a404043fba75a8cb2f755ca49e4f01e18400bb6607a5f76526e07164/fastar-0.9.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6199b4ca0c092a7ae47f5f387492d46a0a2d82cb3b7aa0bf50d7f7d5d8d57f", size = 819166, upload-time = "2026-03-20T14:25:12.027Z" }, - { url = "https://files.pythonhosted.org/packages/85/4a/5b1ea5c8d0dbdfcec2fd1e6a243d6bb5a1c7cd55e132cc532eb8b1cbd6d9/fastar-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:34efe114caf10b4d5ea404069ff1f6cc0e55a708c7091059b0fc087f65c0a331", size = 883618, upload-time = "2026-03-20T14:24:33.552Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/ae46e5722a67a3c2e0ff83d539b0907d6e5092f6395840c0eb6ede81c5d6/fastar-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4d44c1f8d9c5a3e4e58e6ffb77f4ca023ba9d9ddd88e7c613b3419a8feaa3db7", size = 966294, upload-time = "2026-03-20T14:25:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/98/58/b161cf8711f4a50a3e57b6f89bc703c1aed282cad50434b3bc8524738b20/fastar-0.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d2af970a1f773965b05f1765017a417380ad080ea49590516eb25b23c039158a", size = 1033177, upload-time = "2026-03-20T14:26:02.868Z" }, - { url = "https://files.pythonhosted.org/packages/e2/76/faac7292bce9b30106a6b6a9f5ddb658fdb03abe2644688b82023c8f76b9/fastar-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1675346d7cbdde0d21869c3b597be19b5e31a36442bdf3a48d83a49765b269dc", size = 1073620, upload-time = "2026-03-20T14:26:16.121Z" }, - { url = "https://files.pythonhosted.org/packages/b8/be/dd55ffcc302d6f0ff4aba1616a0da3edc8fcefb757869cad81de74604a35/fastar-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc440daa28591aeb4d387c171e824f179ad2ab256ce7a315472395b8d5f80392", size = 1025147, upload-time = "2026-03-20T14:26:28.767Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c7/080bbb2b3c4e739fe6486fd765a09905f6c16c1068b2fcf2bb51a5e83937/fastar-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:32787880600a988d11547628034993ef948499ae4514a30509817242c4eb98b1", size = 452317, upload-time = "2026-03-20T14:27:03.243Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/00553739a7e9e35f78a0c5911d181acf6b6e132337adc9bbc3575f5f6f04/fastar-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92fa18ec4958f33473259980685d29248ac44c96eed34026ad7550f93dd9ee23", size = 483994, upload-time = "2026-03-20T14:26:52.76Z" }, - { url = "https://files.pythonhosted.org/packages/4f/36/a7af08d233624515d9a0f5d41b7a01a51fd825b8c795e41800215a3200e7/fastar-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:34f646ac4f5bed3661a106ca56c1744e7146a02aacf517d47b24fd3f25dc1ff6", size = 460604, upload-time = "2026-03-20T14:26:40.771Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, + { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, + { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, ] [[package]] @@ -1789,46 +1821,77 @@ wheels = [ [[package]] name = "fickling" -version = "0.1.10" +version = "0.1.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/06/1818b8f52267599e54041349c553d5894e17ec8a539a246eb3f9eaf05629/fickling-0.1.10.tar.gz", hash = "sha256:8c8b76abd29936f1a5932e4087b8c8becb2d7ab1cf08549e63519ebcb2f71644", size = 338062, upload-time = "2026-03-13T16:34:29.287Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/0b/0aea3be8edd5a8de3c1491a12f0149f6db5afd9467dfddaa5ed24a27bef9/fickling-0.1.11.tar.gz", hash = "sha256:3ca0dcc69967c53868b35787017d4d7d8943f096450431f7e3b3a9aadb02b0f5", size = 357476, upload-time = "2026-05-06T15:04:21.869Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/86/620960dff970da5311f05e25fc045dac8495557d51030e5a0827084b18fd/fickling-0.1.10-py3-none-any.whl", hash = "sha256:962c35c38ece1b3632fc119c0f4cb1eebc02dc6d65bfd93a1803afd42ca91d25", size = 52853, upload-time = "2026-03-13T16:34:27.821Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3b/45b8233feb53dd9da16208b039507604844a07c8b5bb3c5a4e39c520f32d/fickling-0.1.11-py3-none-any.whl", hash = "sha256:19ecb791d781d475e84ed951dc2c4a0c852108e237416d517ab0a8dd771d4098", size = 58549, upload-time = "2026-05-06T15:04:20.168Z" }, ] [[package]] name = "filelock" -version = "3.25.2" +version = "3.29.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] [[package]] name = "fla-core" -version = "0.4.2" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "einops" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/f9/9e05c48f92b1388a8a357141eb557ed0dd6d4bb936e1d05d35f01976657f/fla_core-0.4.2.tar.gz", hash = "sha256:e9fef6fcdf122029f9feb7dccfeb85eb9650e6aabc72d2a65b36558e9c590edd", size = 377722, upload-time = "2026-03-12T14:45:46.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/14/2aabd37839b9f3c6a67fbc5678f906d04d0c242c603ac234eefe02df99a6/fla_core-0.5.0.tar.gz", hash = "sha256:476dd94711702af81cc4827010d9209f6053d8cdceac8e43d3c8497071f07a81", size = 418171, upload-time = "2026-04-21T20:25:40.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/03/96e6820d176256353670b41ca56dabbbebe129674b4f4ad7b54a152b7b36/fla_core-0.5.0-py3-none-any.whl", hash = "sha256:5c826ff32daf6b629658e3e4f6125d87cf8c32eea937e3be9ba85f51951d809a", size = 595276, upload-time = "2026-04-21T20:25:37.698Z" }, +] + +[[package]] +name = "flash-attn-4" +version = "4.0.0b5" +source = { url = "https://files.pythonhosted.org/packages/24/f7/01ee2576ce41f9884d291ee21861ef194afc0b2b1ce3bd175fc7a6e1b133/flash_attn_4-4.0.0b5-py3-none-any.whl" } +dependencies = [ + { name = "apache-tvm-ffi" }, + { name = "einops" }, + { name = "nvidia-cutlass-dsl" }, + { name = "quack-kernels" }, + { name = "torch" }, + { name = "torch-c-dlpack-ext" }, + { name = "typing-extensions" }, +] wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/36/3c303f92bafea7c3f97d68bbb83d18cc42e30cd0bfb1b7cfe589360f11d6/fla_core-0.4.2-py3-none-any.whl", hash = "sha256:cba3db29380002da3cbfc0db94d6efac19aaf528900d19c05c2765e8f3cc485b", size = 510239, upload-time = "2026-03-12T14:45:43.708Z" }, + { url = "https://files.pythonhosted.org/packages/24/f7/01ee2576ce41f9884d291ee21861ef194afc0b2b1ce3bd175fc7a6e1b133/flash_attn_4-4.0.0b5-py3-none-any.whl", hash = "sha256:5239d748700ed7cf08d5703b4bb8ccb3fe26d23d12bb34fc67b694d53f8c2ecc" }, +] + +[package.metadata] +requires-dist = [ + { name = "apache-tvm-ffi", specifier = ">=0.1.5,<0.2" }, + { name = "einops" }, + { name = "nvidia-cutlass-dsl", specifier = ">=4.4.2" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "quack-kernels", specifier = ">=0.3.3" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "torch" }, + { name = "torch-c-dlpack-ext" }, + { name = "typing-extensions" }, ] +provides-extras = ["dev"] [[package]] name = "flash-linear-attention" -version = "0.4.2" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fla-core" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/cb/46cc27a829a10b308927c5dbc99176906a021bb0770253699e93f3cd81a0/flash_linear_attention-0.4.2.tar.gz", hash = "sha256:f97c01ebe7cf390323af07dd3fb65ade07da16724339bf70c78607bc0c007c34", size = 148464, upload-time = "2026-03-12T14:45:46.945Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/5c/1db76cc829c951117a3112f306d50333bd71399d2e35807fe7c99ffc2007/flash_linear_attention-0.5.0.tar.gz", hash = "sha256:22b789a47f07738b4382ecdf775d7bb40e0d803c467c34f8e2ecd6a1dc780938", size = 160419, upload-time = "2026-04-21T20:25:42.344Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/ee/a3cba17965482b35c4990af90bad108e82c32edcb59911c37f318b5f4198/flash_linear_attention-0.4.2-py3-none-any.whl", hash = "sha256:c08be006ce4dbe1be81f54938ee8e6fc7968cfba397c8d06c7669e97b8c44c0d", size = 284661, upload-time = "2026-03-12T14:45:44.905Z" }, + { url = "https://files.pythonhosted.org/packages/cc/16/7736db08806981562c728f32ea1dcb4565948fa9faffdbf4ffbf72522fbf/flash_linear_attention-0.5.0-py3-none-any.whl", hash = "sha256:92e64e989ed34355c1f838232597b2e39783ee0494ada3199b58e156aa1d8eb8", size = 319037, upload-time = "2026-04-21T20:25:39.473Z" }, ] [[package]] @@ -1863,43 +1926,43 @@ wheels = [ [[package]] name = "fonttools" -version = "4.62.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, - { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, - { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, - { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, - { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, - { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, - { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, - { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, - { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, - { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, - { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, - { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, - { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, - { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, - { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, - { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, - { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, - { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, - { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, - { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, - { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, - { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, - { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, - { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, - { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] [[package]] @@ -2031,19 +2094,19 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.46" +version = "3.1.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] [[package]] name = "google-api-core" -version = "2.30.0" +version = "2.30.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -2052,14 +2115,14 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/98/586ec94553b569080caef635f98a3723db36a38eac0e3d7eb3ea9d2e4b9a/google_api_core-2.30.0.tar.gz", hash = "sha256:02edfa9fab31e17fc0befb5f161b3bf93c9096d99aed584625f38065c511ad9b", size = 176959, upload-time = "2026-02-18T20:28:11.926Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/502a57fb0ec752026d24df1280b162294b22a0afb98a326084f9a979138b/google_api_core-2.30.3.tar.gz", hash = "sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b", size = 177001, upload-time = "2026-04-10T00:41:28.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/27/09c33d67f7e0dcf06d7ac17d196594e66989299374bfb0d4331d1038e76b/google_api_core-2.30.0-py3-none-any.whl", hash = "sha256:80be49ee937ff9aba0fd79a6eddfde35fe658b9953ab9b79c57dd7061afa8df5", size = 173288, upload-time = "2026-02-18T20:28:10.367Z" }, + { url = "https://files.pythonhosted.org/packages/03/15/e56f351cf6ef1cfea58e6ac226a7318ed1deb2218c4b3cc9bd9e4b786c5a/google_api_core-2.30.3-py3-none-any.whl", hash = "sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8", size = 173274, upload-time = "2026-04-09T22:57:16.198Z" }, ] [[package]] name = "google-api-python-client" -version = "2.193.0" +version = "2.197.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -2068,48 +2131,48 @@ dependencies = [ { name = "httplib2" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/f4/e14b6815d3b1885328dd209676a3a4c704882743ac94e18ef0093894f5c8/google_api_python_client-2.193.0.tar.gz", hash = "sha256:8f88d16e89d11341e0a8b199cafde0fb7e6b44260dffb88d451577cbd1bb5d33", size = 14281006, upload-time = "2026-03-17T18:25:29.415Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/09/081d66357118bd260f8f182cb1b2dd5bd32ca88e3714d7c93896cab946fc/google_api_python_client-2.197.0.tar.gz", hash = "sha256:32e03977eda4a66eafc6ae58dc9ec46426b6025636d5ef019c5703013eddd4e5", size = 14707398, upload-time = "2026-05-28T20:23:12.498Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/6d/fe75167797790a56d17799b75e1129bb93f7ff061efc7b36e9731bd4be2b/google_api_python_client-2.193.0-py3-none-any.whl", hash = "sha256:c42aa324b822109901cfecab5dc4fc3915d35a7b376835233c916c70610322db", size = 14856490, upload-time = "2026-03-17T18:25:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e5/e9cc221fd75230974d4ef45eb72d2261feca3c110d5554215d516bfe6534/google_api_python_client-2.197.0-py3-none-any.whl", hash = "sha256:0f8b89aa75768161dd4f5092d6bcb386c13236b32e0d9a938c02f71342094d14", size = 15287302, upload-time = "2026-05-28T20:23:09.683Z" }, ] [[package]] name = "google-auth" -version = "2.49.1" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/ad/ff781329bbbdc0974a098d996e89c9e1f7024262f9e3eec442fbb9ad1ac6/google_auth-2.53.0.tar.gz", hash = "sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c", size = 335844, upload-time = "2026-05-15T20:53:07.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/db44165ba7c581268c6d46017ef63339110378305062830104fc7fa144cb/google_auth-2.53.0-py3-none-any.whl", hash = "sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68", size = 246071, upload-time = "2026-05-15T20:53:05.609Z" }, ] [[package]] name = "google-auth-httplib2" -version = "0.3.0" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134, upload-time = "2025-12-15T22:13:51.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/b3/f192c8bc7e41e0ebdbd95afcae4783417a34b6a6af62d22daf22c3fd38fc/google_auth_httplib2-0.4.0.tar.gz", hash = "sha256:d5b030a204b7a4b4d553ba9ca701b62481ee2b74419325580be70f7d85ffed35", size = 11161, upload-time = "2026-05-07T08:03:46.878Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529, upload-time = "2025-12-15T22:13:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/97/be/954c35a62b9e31de66b0a43c225c9b6bb9e0f98d6b1dc110a2308e3644f5/google_auth_httplib2-0.4.0-py3-none-any.whl", hash = "sha256:8e55cfafa3358cba85f6cad4a886138e88e158d71e7e5c9ee5936a5c1507fb91", size = 9529, upload-time = "2026-05-07T08:02:12.375Z" }, ] [[package]] name = "google-cloud-core" -version = "2.5.0" +version = "2.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/dd/1eef226e470369b26824a505c34482c0b493bc35fe8e0c6b003b5feca21a/google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83", size = 36001, upload-time = "2026-05-07T08:04:04.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, + { url = "https://files.pythonhosted.org/packages/84/4a/98da8930ab109c73d9a5d13782a9ebb81ea8c111f6d534a567b71d23e52b/google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e", size = 29390, upload-time = "2026-05-07T08:02:34.672Z" }, ] [[package]] @@ -2154,26 +2217,26 @@ wheels = [ [[package]] name = "google-resumable-media" -version = "2.8.0" +version = "2.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265, upload-time = "2025-11-17T15:38:06.659Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/4b/0b235beccc310d0a48adbc7246b719d173cca6c88c572dfa4b090e39143c/google_resumable_media-2.9.0.tar.gz", hash = "sha256:f7cfb224846a9dd444d125115dfbe8ef02a2b893e78f087762fe716a255a734b", size = 2164534, upload-time = "2026-05-07T08:04:44.236Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340, upload-time = "2025-11-17T15:38:05.594Z" }, + { url = "https://files.pythonhosted.org/packages/07/73/3518e63deb1667c5409a4579e28daf5e84479a87a72c547e0487f7883dcd/google_resumable_media-2.9.0-py3-none-any.whl", hash = "sha256:c8901e88e389af8bed64d9696c74d8bad961865eb2236e13e0bfca9bb0a65ca3", size = 81507, upload-time = "2026-05-07T08:03:23.809Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.73.0" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/96/a0205167fa0154f4a542fd6925bdc63d039d88dab3588b875078107e6f06/googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a", size = 147323, upload-time = "2026-03-06T21:53:09.727Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/28/23eea8acd65972bbfe295ce3666b28ac510dfcb115fac089d3edb0feb00a/googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8", size = 297578, upload-time = "2026-03-06T21:52:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] @@ -2243,98 +2306,122 @@ wheels = [ [[package]] name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" }, + { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" }, + { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" }, + { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" }, + { url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" }, + { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" }, + { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" }, + { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" }, + { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" }, + { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" }, + { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" }, + { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" }, + { url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" }, + { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" }, + { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" }, + { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" }, + { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" }, + { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" }, ] [[package]] name = "grpcio" -version = "1.78.0" +version = "1.80.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, - { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, - { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, - { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, - { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, - { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, - { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, - { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, - { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, - { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, - { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, - { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, - { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, - { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, - { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, ] [[package]] name = "gunicorn" -version = "25.1.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" }, + { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, ] [[package]] @@ -2437,34 +2524,34 @@ wheels = [ [[package]] name = "hf-xet" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/08/23c84a26716382c89151b5b447b4beb19e3345f3a93d3b73009a71a57ad3/hf_xet-1.4.2.tar.gz", hash = "sha256:b7457b6b482d9e0743bd116363239b1fa904a5e65deede350fbc0c4ea67c71ea", size = 672357, upload-time = "2026-03-13T06:58:51.077Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/06/e8cf74c3c48e5485c7acc5a990d0d8516cdfb5fdf80f799174f1287cc1b5/hf_xet-1.4.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ac8202ae1e664b2c15cdfc7298cbb25e80301ae596d602ef7870099a126fcad4", size = 3796125, upload-time = "2026-03-13T06:58:33.177Z" }, - { url = "https://files.pythonhosted.org/packages/66/d4/b73ebab01cbf60777323b7de9ef05550790451eb5172a220d6b9845385ec/hf_xet-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6d2f8ee39fa9fba9af929f8c0d0482f8ee6e209179ad14a909b6ad78ffcb7c81", size = 3555985, upload-time = "2026-03-13T06:58:31.797Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e7/ded6d1bd041c3f2bca9e913a0091adfe32371988e047dd3a68a2463c15a2/hf_xet-1.4.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4642a6cf249c09da8c1f87fe50b24b2a3450b235bf8adb55700b52f0ea6e2eb6", size = 4212085, upload-time = "2026-03-13T06:58:24.323Z" }, - { url = "https://files.pythonhosted.org/packages/97/c1/a0a44d1f98934f7bdf17f7a915b934f9fca44bb826628c553589900f6df8/hf_xet-1.4.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:769431385e746c92dc05492dde6f687d304584b89c33d79def8367ace06cb555", size = 3988266, upload-time = "2026-03-13T06:58:22.887Z" }, - { url = "https://files.pythonhosted.org/packages/7a/82/be713b439060e7d1f1d93543c8053d4ef2fe7e6922c5b31642eaa26f3c4b/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c9dd1c1bc4cc56168f81939b0e05b4c36dd2d28c13dc1364b17af89aa0082496", size = 4188513, upload-time = "2026-03-13T06:58:40.858Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/cbd4188b22abd80ebd0edbb2b3e87f2633e958983519980815fb8314eae5/hf_xet-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fca58a2ae4e6f6755cc971ac6fcdf777ea9284d7e540e350bb000813b9a3008d", size = 4428287, upload-time = "2026-03-13T06:58:42.601Z" }, - { url = "https://files.pythonhosted.org/packages/b2/4e/84e45b25e2e3e903ed3db68d7eafa96dae9a1d1f6d0e7fc85120347a852f/hf_xet-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:163aab46854ccae0ab6a786f8edecbbfbaa38fcaa0184db6feceebf7000c93c0", size = 3665574, upload-time = "2026-03-13T06:58:53.881Z" }, - { url = "https://files.pythonhosted.org/packages/ee/71/c5ac2b9a7ae39c14e91973035286e73911c31980fe44e7b1d03730c00adc/hf_xet-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:09b138422ecbe50fd0c84d4da5ff537d27d487d3607183cd10e3e53f05188e82", size = 3528760, upload-time = "2026-03-13T06:58:52.187Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/fcd2504015eab26358d8f0f232a1aed6b8d363a011adef83fe130bff88f7/hf_xet-1.4.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:949dcf88b484bb9d9276ca83f6599e4aa03d493c08fc168c124ad10b2e6f75d7", size = 3796493, upload-time = "2026-03-13T06:58:39.267Z" }, - { url = "https://files.pythonhosted.org/packages/82/56/19c25105ff81731ca6d55a188b5de2aa99d7a2644c7aa9de1810d5d3b726/hf_xet-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:41659966020d59eb9559c57de2cde8128b706a26a64c60f0531fa2318f409418", size = 3555797, upload-time = "2026-03-13T06:58:37.546Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/8933c073186849b5e06762aa89847991d913d10a95d1603eb7f2c3834086/hf_xet-1.4.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c588e21d80010119458dd5d02a69093f0d115d84e3467efe71ffb2c67c19146", size = 4212127, upload-time = "2026-03-13T06:58:30.539Z" }, - { url = "https://files.pythonhosted.org/packages/eb/01/f89ebba4e369b4ed699dcb60d3152753870996f41c6d22d3d7cac01310e1/hf_xet-1.4.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a296744d771a8621ad1d50c098d7ab975d599800dae6d48528ba3944e5001ba0", size = 3987788, upload-time = "2026-03-13T06:58:29.139Z" }, - { url = "https://files.pythonhosted.org/packages/84/4d/8a53e5ffbc2cc33bbf755382ac1552c6d9af13f623ed125fe67cc3e6772f/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f563f7efe49588b7d0629d18d36f46d1658fe7e08dce3fa3d6526e1c98315e2d", size = 4188315, upload-time = "2026-03-13T06:58:48.017Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b8/b7a1c1b5592254bd67050632ebbc1b42cc48588bf4757cb03c2ef87e704a/hf_xet-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5b2e0132c56d7ee1bf55bdb638c4b62e7106f6ac74f0b786fed499d5548c5570", size = 4428306, upload-time = "2026-03-13T06:58:49.502Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0c/40779e45b20e11c7c5821a94135e0207080d6b3d76e7b78ccb413c6f839b/hf_xet-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:2f45c712c2fa1215713db10df6ac84b49d0e1c393465440e9cb1de73ecf7bbf6", size = 3665826, upload-time = "2026-03-13T06:58:59.88Z" }, - { url = "https://files.pythonhosted.org/packages/51/4c/e2688c8ad1760d7c30f7c429c79f35f825932581bc7c9ec811436d2f21a0/hf_xet-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6d53df40616f7168abfccff100d232e9d460583b9d86fa4912c24845f192f2b8", size = 3529113, upload-time = "2026-03-13T06:58:58.491Z" }, - { url = "https://files.pythonhosted.org/packages/b4/86/b40b83a2ff03ef05c4478d2672b1fc2b9683ff870e2b25f4f3af240f2e7b/hf_xet-1.4.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:71f02d6e4cdd07f344f6844845d78518cc7186bd2bc52d37c3b73dc26a3b0bc5", size = 3800339, upload-time = "2026-03-13T06:58:36.245Z" }, - { url = "https://files.pythonhosted.org/packages/64/2e/af4475c32b4378b0e92a587adb1aa3ec53e3450fd3e5fe0372a874531c00/hf_xet-1.4.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e9b38d876e94d4bdcf650778d6ebbaa791dd28de08db9736c43faff06ede1b5a", size = 3559664, upload-time = "2026-03-13T06:58:34.787Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4c/781267da3188db679e601de18112021a5cb16506fe86b246e22c5401a9c4/hf_xet-1.4.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:77e8c180b7ef12d8a96739a4e1e558847002afe9ea63b6f6358b2271a8bdda1c", size = 4217422, upload-time = "2026-03-13T06:58:27.472Z" }, - { url = "https://files.pythonhosted.org/packages/68/47/d6cf4a39ecf6c7705f887a46f6ef5c8455b44ad9eb0d391aa7e8a2ff7fea/hf_xet-1.4.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c3b3c6a882016b94b6c210957502ff7877802d0dbda8ad142c8595db8b944271", size = 3992847, upload-time = "2026-03-13T06:58:25.989Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ef/e80815061abff54697239803948abc665c6b1d237102c174f4f7a9a5ffc5/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d9a634cc929cfbaf2e1a50c0e532ae8c78fa98618426769480c58501e8c8ac2", size = 4193843, upload-time = "2026-03-13T06:58:44.59Z" }, - { url = "https://files.pythonhosted.org/packages/54/75/07f6aa680575d9646c4167db6407c41340cbe2357f5654c4e72a1b01ca14/hf_xet-1.4.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b0932eb8b10317ea78b7da6bab172b17be03bbcd7809383d8d5abd6a2233e04", size = 4432751, upload-time = "2026-03-13T06:58:46.533Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/193eabd7e7d4b903c4aa983a215509c6114915a5a237525ec562baddb868/hf_xet-1.4.2-cp37-abi3-win_amd64.whl", hash = "sha256:ad185719fb2e8ac26f88c8100562dbf9dbdcc3d9d2add00faa94b5f106aea53f", size = 3671149, upload-time = "2026-03-13T06:58:57.07Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" }, + { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" }, + { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" }, + { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" }, + { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" }, + { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" }, ] [[package]] @@ -2503,31 +2590,38 @@ wheels = [ [[package]] name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, ] [[package]] @@ -2561,7 +2655,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.7.2" +version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -2574,9 +2668,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/15/eafc1c57bf0f8afffb243dcd4c0cceb785e956acc17bba4d9bf2ae21fc9c/huggingface_hub-1.7.2.tar.gz", hash = "sha256:7f7e294e9bbb822e025bdb2ada025fa4344d978175a7f78e824d86e35f7ab43b", size = 724684, upload-time = "2026-03-20T10:36:08.767Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/0f/ed994dbade67a54407c28cab96ef845e0e6d25500be56aca6394f8bfc9dd/huggingface_hub-1.16.1.tar.gz", hash = "sha256:7f1dc4c5ec21aed69be630ad0c3378616be16f3de1a47b141c0e812965d9c832", size = 792534, upload-time = "2026-05-21T18:40:00.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/de/3ad061a05f74728927ded48c90b73521b9a9328c85d841bdefb30e01fb85/huggingface_hub-1.7.2-py3-none-any.whl", hash = "sha256:288f33a0a17b2a73a1359e2a5fd28d1becb2c121748c6173ab8643fb342c850e", size = 618036, upload-time = "2026-03-20T10:36:06.824Z" }, + { url = "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl", hash = "sha256:64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", size = 668176, upload-time = "2026-05-21T18:39:58.596Z" }, ] [[package]] @@ -2616,11 +2710,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" }, ] [[package]] @@ -2715,14 +2809,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "8.6.1" +version = "9.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767, upload-time = "2025-01-20T22:21:30.429Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971, upload-time = "2025-01-20T22:21:29.177Z" }, + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, ] [[package]] @@ -2761,11 +2855,11 @@ wheels = [ [[package]] name = "invoke" -version = "2.2.1" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/227c48c5fe47fa178ccf1fda8f047d16c97ba926567b661e9ce2045c600c/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, + { url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" }, ] [[package]] @@ -2794,7 +2888,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.11.0" +version = "9.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2804,13 +2898,14 @@ dependencies = [ { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, + { name = "psutil" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, + { url = "https://files.pythonhosted.org/packages/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" }, ] [[package]] @@ -2882,26 +2977,26 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.4.0" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/cf/ea4ef2920830dea3f5ab2ea4da6fb67724e6dca80ee2553788c3607243d0/jaraco_functools-4.5.0.tar.gz", hash = "sha256:3bb5665ea4a020cf78a7040e89154c77edadb3ca74f366479669c5999aa70b03", size = 20272, upload-time = "2026-05-15T21:34:10.025Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, + { url = "https://files.pythonhosted.org/packages/96/9a/982e48afcffcd727a9144506720ffd4224b6b7e355c98641866f38b7c043/jaraco_functools-4.5.0-py3-none-any.whl", hash = "sha256:79ce39246eddbde4b3a03b77ea5f0f7878dc669b166a66cf3fa8e266aa3fa2f4", size = 10594, upload-time = "2026-05-15T21:34:08.595Z" }, ] [[package]] name = "jedi" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, ] [[package]] @@ -2927,70 +3022,74 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, ] [[package]] @@ -3211,9 +3310,10 @@ wheels = [ [[package]] name = "kubernetes" -version = "35.0.0" +version = "36.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiohttp" }, { name = "certifi" }, { name = "durationpy" }, { name = "python-dateutil" }, @@ -3224,17 +3324,18 @@ dependencies = [ { name = "urllib3" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/c2/cb08cd4cc2874c4ca6e12cb94f7639176043905e9d12bdda34db9ad9a3a0/kubernetes-36.0.1.tar.gz", hash = "sha256:3eadd6ae1be3b742ae63bd382b139c9fd5171afb6e00771dcefaae2d49001992", size = 2337184, upload-time = "2026-05-26T20:41:33.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, + { url = "https://files.pythonhosted.org/packages/92/6b/62f3d6cd024b1d0cfe8a87189b3562a4bb2dc581279056ccfd8cc233c556/kubernetes-36.0.1-py2.py3-none-any.whl", hash = "sha256:7631d11dd761f18658064a6ee91a36923cec3bef3cd92b99e08a53745b95f7d0", size = 4617214, upload-time = "2026-05-26T20:41:30.361Z" }, ] [[package]] name = "langchain-core" -version = "1.2.21" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, + { name = "langchain-protocol" }, { name = "langsmith" }, { name = "packaging" }, { name = "pydantic" }, @@ -3243,28 +3344,40 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/e4/135ef5bbb5b97bdf15f777b86d7fba2ef8a162723ae96b3c7c1add9891a9/langchain_core-1.2.21.tar.gz", hash = "sha256:1a121d13976dc0908d5a8222262810ea483a4cf2b05006bdba75df5b11b554b3", size = 841063, upload-time = "2026-03-23T18:01:01.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/ae/f7591e57d4c6b64b521f9832fc471070902718c59bf135401f3b90a3f7ef/langchain_core-1.2.21-py3-none-any.whl", hash = "sha256:486cb405e2ecb0c407cb5fb5379ed0f919eb4b8a868b60cc8c3c15a3dfb560a7", size = 505756, upload-time = "2026-03-23T18:01:00.176Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" }, ] [[package]] name = "langchain-openai" -version = "1.1.10" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/0f/01147f842499338ae3b0dd0a351fb83006d9ed623cf3a999bd68ba5bbe2d/langchain_openai-1.1.10.tar.gz", hash = "sha256:ca6fae7cf19425acc81814efed59c7d205ec9a1f284fd1d08aae9bda85d6501b", size = 1059755, upload-time = "2026-02-17T18:03:44.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/1b/c506c7f41156d3a6b4582b4c487f480001b8741deecc6e2d4931fdf4cf2c/langchain_openai-1.2.2.tar.gz", hash = "sha256:8698ffcee9a086e91ab6d207f0026181a03effcbf86bf9aee1808ee35af69dcc", size = 1147539, upload-time = "2026-05-21T22:08:31.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/17/3785cbcdc81c451179247e4176d2697879cb4f45ab2c59d949ca574e072d/langchain_openai-1.1.10-py3-none-any.whl", hash = "sha256:d91b2c09e9fbc70f7af45345d3aa477744962d41c73a029beb46b4f83b824827", size = 87205, upload-time = "2026-02-17T18:03:43.502Z" }, + { url = "https://files.pythonhosted.org/packages/a6/8e/7406c99afacafc8c2ce0fa4152f9f8b9598c93ceb291959821abd053b982/langchain_openai-1.2.2-py3-none-any.whl", hash = "sha256:7da39a3c70cbafa93853456199e39a264dc70651be79b12ac49b4f6a448bce2d", size = 99631, upload-time = "2026-05-21T22:08:29.527Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" }, ] [[package]] name = "langgraph" -version = "1.1.3" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -3274,53 +3387,53 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/b2/e7db624e8b0ee063ecfbf7acc09467c0836a05914a78e819dfb3744a0fac/langgraph-1.1.3.tar.gz", hash = "sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370", size = 545120, upload-time = "2026-03-18T23:42:58.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/5a/ffc12434ee8aecab830d58b4d204ddea45073eae7639c963310f671a5bf5/langgraph-1.2.2.tar.gz", hash = "sha256:f54a98458976b3ff0774683867df125fb52d8dbedeb2441d0b0656a51331cee5", size = 695730, upload-time = "2026-05-26T18:07:28.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/f7/221cc479e95e03e260496616e5ce6fb50c1ea01472e3a5bc481a9b8a2f83/langgraph-1.1.3-py3-none-any.whl", hash = "sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546", size = 168149, upload-time = "2026-03-18T23:42:56.967Z" }, + { url = "https://files.pythonhosted.org/packages/42/9b/b08d578bba73e25351152dfd3d6d21e81210a5fff1b6f26e56f33197c8f5/langgraph-1.2.2-py3-none-any.whl", hash = "sha256:0a851bf4ba5939c5474a2fd57e6b439b5315283e254e42943bd392c2d71a5e03", size = 236376, upload-time = "2026-05-26T18:07:26.577Z" }, ] [[package]] name = "langgraph-checkpoint" -version = "4.0.1" +version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "ormsgpack" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/44/a8df45d1e8b4637e29789fa8bae1db022c953cc7ac80093cfc52e923547e/langgraph_checkpoint-4.0.1.tar.gz", hash = "sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9", size = 158135, upload-time = "2026-02-27T21:06:16.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, ] [[package]] name = "langgraph-prebuilt" -version = "1.0.8" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/06/dd61a5c2dce009d1b03b1d56f2a85b3127659fdddf5b3be5d8f1d60820fb/langgraph_prebuilt-1.0.8.tar.gz", hash = "sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69", size = 164442, upload-time = "2026-02-19T18:14:39.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/41/ec966424ad3f2ed3996d24079d3342c8cd6c0bd0653c12b2a917a685ec6c/langgraph_prebuilt-1.0.8-py3-none-any.whl", hash = "sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0", size = 35648, upload-time = "2026-02-19T18:14:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.3.12" +version = "0.3.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "orjson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/a1/012f0e0f5c9fd26f92bdc9d244756ad673c428230156ef668e6ec7c18cee/langgraph_sdk-0.3.12.tar.gz", hash = "sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71", size = 194932, upload-time = "2026-03-18T22:15:54.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/af/cdd4d6f3c05b3c1112ed3f12ef830faf15951b21d22cbc622a4becbbe25c/langgraph_sdk-0.3.15.tar.gz", hash = "sha256:29e805003d2c6e296823dd71992610976fd0428cefaa8b3304fd91f2247037de", size = 201924, upload-time = "2026-05-22T16:54:27.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/4d/4f796e86b03878ab20d9b30aaed1ad459eda71a5c5b67f7cfe712f3548f2/langgraph_sdk-0.3.12-py3-none-any.whl", hash = "sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327", size = 95834, upload-time = "2026-03-18T22:15:53.545Z" }, + { url = "https://files.pythonhosted.org/packages/be/a5/0196d9c05749c25bc198e4909d68c998bc3120297e14944921baf2f4c384/langgraph_sdk-0.3.15-py3-none-any.whl", hash = "sha256:3838773acf7456d158165385d49f48f1e856f28b56ccd99ea139a8f27004815d", size = 98166, upload-time = "2026-05-22T16:54:26.013Z" }, ] [[package]] name = "langsmith" -version = "0.7.22" +version = "0.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -3330,12 +3443,13 @@ dependencies = [ { name = "requests" }, { name = "requests-toolbelt" }, { name = "uuid-utils" }, + { name = "websockets" }, { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/2a/2d5e6c67396fd228670af278c4da7bd6db2b8d11deaf6f108490b6d3f561/langsmith-0.7.22.tar.gz", hash = "sha256:35bfe795d648b069958280760564632fd28ebc9921c04f3e209c0db6a6c7dc04", size = 1134923, upload-time = "2026-03-19T22:45:23.492Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/61/d269b8bd3376031de7be6ac2de8ba94fafff67635195d97aa0e842027ac7/langsmith-0.8.6.tar.gz", hash = "sha256:a46fd3403c2de3a9c34f72ebb7b2e45872627671adcc67c6a4c571520b6931cc", size = 4463093, upload-time = "2026-05-27T22:51:52.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/94/1f5d72655ab6534129540843776c40eff757387b88e798d8b3bf7e313fd4/langsmith-0.7.22-py3-none-any.whl", hash = "sha256:6e9d5148314d74e86748cb9d3898632cad0320c9323d95f70f969e5bc078eee4", size = 359927, upload-time = "2026-03-19T22:45:21.603Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c5/28f99eccd79ce89ec93de9a5039a74ddf4740f2d9671b0a06c5d2e200914/langsmith-0.8.6-py3-none-any.whl", hash = "sha256:b304888ea5ec5fe397db24f0bf474b0c8e472fb23ee36a2007e9837f6ff29cc1", size = 399954, upload-time = "2026-05-27T22:51:50.847Z" }, ] [[package]] @@ -3363,94 +3477,94 @@ wheels = [ [[package]] name = "lxml" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" }, - { url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" }, - { url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" }, - { url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" }, - { url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" }, - { url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" }, - { url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" }, - { url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" }, - { url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" }, - { url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" }, - { url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" }, - { url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" }, - { url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" }, - { url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" }, - { url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" }, - { url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" }, - { url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" }, - { url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" }, - { url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" }, - { url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" }, - { url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" }, - { url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" }, - { url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" }, - { url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" }, - { url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" }, - { url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" }, - { url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" }, - { url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" }, - { url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" }, - { url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" }, - { url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" }, - { url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" }, - { url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" }, - { url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" }, - { url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" }, - { url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" }, - { url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" }, - { url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" }, - { url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" }, - { url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" }, - { url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" }, +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/6e/c4add832b6fc1e887125b96f880d7b9b70aae5248718e046b1704bcac4b9/lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7", size = 8570821, upload-time = "2026-05-18T19:17:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/22/00/ff3009c88e65de8011630acf8ab5a09cb2becd2aaf47fba2f3449f6224e9/lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1", size = 4624252, upload-time = "2026-05-18T19:17:47.897Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, + { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, + { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, + { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, + { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, + { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, + { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, + { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, + { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, + { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, + { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, + { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, + { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, + { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, + { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, + { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, + { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, + { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, + { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, ] [[package]] name = "mako" -version = "1.3.10" +version = "1.3.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, ] [[package]] @@ -3479,14 +3593,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -3554,7 +3668,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.8" +version = "3.10.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -3567,55 +3681,55 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, - { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, - { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, - { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, - { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, - { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, - { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, - { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, - { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, - { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, ] [[package]] name = "matplotlib-inline" -version = "0.2.1" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, ] [[package]] @@ -3719,9 +3833,10 @@ wheels = [ [[package]] name = "mlflow" -version = "3.10.1" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "aiohttp" }, { name = "alembic" }, { name = "cryptography" }, { name = "docker" }, @@ -3742,14 +3857,14 @@ dependencies = [ { name = "sqlalchemy" }, { name = "waitress", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/94/a583069259500182c070db798118aee7877d37bd1981e49af5ae9113b100/mlflow-3.10.1.tar.gz", hash = "sha256:609509ccc15eb9c17861748e537cbffa57d2caf488ff3e30efed62951a6977cf", size = 9542009, upload-time = "2026-03-05T11:15:22.677Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/e3/b2148a6d6f38731d3dda49a7e46cf6932a458aa0aa5414b80e6e7251fa1d/mlflow-3.12.0.tar.gz", hash = "sha256:227ee31c6abf7ae3b3c38d4ca87c356e107578740c1efee89da43f2a5b9e3b47", size = 9939137, upload-time = "2026-05-05T10:28:58.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/18/ca682e740b90d5a930981cd375f878a453a713741b5b7d9c0d9516552b5e/mlflow-3.10.1-py3-none-any.whl", hash = "sha256:17bfbd76d4071498d6199c3fc53945e5f50997d14e3e2a6bfd4dc3cb8957f209", size = 10165655, upload-time = "2026-03-05T11:15:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f8/47f28975c1c1b70d351fa19c5aef21cef5ae1e1aca36bd1858798384bdbb/mlflow-3.12.0-py3-none-any.whl", hash = "sha256:e1c28ed4c48557cc52c766f17f1ca5826753ddf241d43f30f99c45f7ea6b3ce0", size = 10625639, upload-time = "2026-05-05T10:28:55.777Z" }, ] [[package]] name = "mlflow-skinny" -version = "3.10.1" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -3769,17 +3884,18 @@ dependencies = [ { name = "pyyaml" }, { name = "requests" }, { name = "sqlparse" }, + { name = "starlette" }, { name = "typing-extensions" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/65/5b2c28e74c167ba8a5afe59399ef44291a0f140487f534db1900f09f59f6/mlflow_skinny-3.10.1.tar.gz", hash = "sha256:3d1c5c30245b6e7065b492b09dd47be7528e0a14c4266b782fe58f9bcd1e0be0", size = 2478631, upload-time = "2026-03-05T10:49:01.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/c0/9cbe24b4abcbadb3a3cdab65bfd552b6b75de64374b477abac89190d25d0/mlflow_skinny-3.12.0.tar.gz", hash = "sha256:74d27066bc9553d281e0c31d25f07deb39dbe99d190e4f7c257703e5c8ee6d10", size = 2723866, upload-time = "2026-05-05T10:28:46.388Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/52/17460157271e70b0d8444d27f8ad730ef7d95fb82fac59dc19f11519b921/mlflow_skinny-3.10.1-py3-none-any.whl", hash = "sha256:df1dd507d8ddadf53bfab2423c76cdcafc235cd1a46921a06d1a6b4dd04b023c", size = 2987098, upload-time = "2026-03-05T10:48:59.566Z" }, + { url = "https://files.pythonhosted.org/packages/95/05/2df60fab37881c490e9364ea697a6c3a78d3b593fde2d9332a75f8cdf1f8/mlflow_skinny-3.12.0-py3-none-any.whl", hash = "sha256:0498f3697abcabcc6204c432ef179840f6a7a34ce123837c98c1913064fda6dd", size = 3261903, upload-time = "2026-05-05T10:28:44.24Z" }, ] [[package]] name = "mlflow-tracing" -version = "3.10.1" +version = "3.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -3791,18 +3907,18 @@ dependencies = [ { name = "protobuf" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/7a/4c3c1b7a52a5956b1af81bdd90892019d5927460d520bd4f52063f423029/mlflow_tracing-3.10.1.tar.gz", hash = "sha256:9e54d63cf776d29bb9e2278d35bf27352b93f7b35c8fe8452e9ba5e2a3c5b78f", size = 1243515, upload-time = "2026-03-05T10:46:29.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/13/d32fe4cca53dde68f09fd38c545ea709e8565fd7c2ffd7c5eff99e504aaf/mlflow_tracing-3.12.0.tar.gz", hash = "sha256:8702a34a1d4f1517ba904d716f5a8fca4675e6526f7d164d02bdaabececa2d80", size = 1352412, upload-time = "2026-05-05T10:28:51.115Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/9a/7ac1db2ed7b5e21c50fadf925a53f0c77452a8a855ee4a119b084c2fa5d3/mlflow_tracing-3.10.1-py3-none-any.whl", hash = "sha256:649c722cc58d54f1f40559023a6bd6f3f08150c3ce3c3bb27972b3e795890f47", size = 1495173, upload-time = "2026-03-05T10:46:27.395Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bf/e22b778addbe19a7a912400c37a197ee9cdebc1641e3b0a3882c30da6ee4/mlflow_tracing-3.12.0-py3-none-any.whl", hash = "sha256:c6072553f47b42505dc7ee62946688a4a0dde8f06b78fbc60e946397b20e1518", size = 1618720, upload-time = "2026-05-05T10:28:48.999Z" }, ] [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/1d/f4da6f02cdffe04d6362210b807146a26044c88d839208aec273bb0d9184/more_itertools-11.1.0.tar.gz", hash = "sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d", size = 145772, upload-time = "2026-05-22T14:14:29.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] [[package]] @@ -3816,16 +3932,16 @@ wheels = [ [[package]] name = "msal" -version = "1.35.1" +version = "1.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/aa/5a646093ac218e4a329391d5a31e5092a89db7d2ef1637a90b82cd0b6f94/msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656", size = 165658, upload-time = "2026-03-04T23:38:51.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/cb/b02b0f748ac668922364ccb3c3bff5b71628a05f5adfec2ba2a5c3031483/msal-1.36.0.tar.gz", hash = "sha256:3f6a4af2b036b476a4215111c4297b4e6e236ed186cd804faefba23e4990978b", size = 174217, upload-time = "2026-04-09T10:20:33.525Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/86/16815fddf056ca998853c6dc525397edf0b43559bb4073a80d2bc7fe8009/msal-1.35.1-py3-none-any.whl", hash = "sha256:8f4e82f34b10c19e326ec69f44dc6b30171f2f7098f3720ea8a9f0c11832caa3", size = 119909, upload-time = "2026-03-04T23:38:50.452Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d3/414d1f0a5f6f4fe5313c2b002c54e78a3332970feb3f5fed14237aa17064/msal-1.36.0-py3-none-any.whl", hash = "sha256:36ecac30e2ff4322d956029aabce3c82301c29f0acb1ad89b94edcabb0e58ec4", size = 121547, upload-time = "2026-04-09T10:20:32.336Z" }, ] [[package]] @@ -3842,42 +3958,42 @@ wheels = [ [[package]] name = "msgspec" -version = "0.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" }, - { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, - { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, - { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" }, - { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" }, - { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" }, - { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" }, - { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" }, - { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" }, - { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" }, - { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" }, - { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" }, - { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" }, - { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" }, - { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" }, - { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" }, - { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" }, - { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" }, - { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" }, - { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" }, - { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" }, +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" }, + { url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" }, + { url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" }, + { url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" }, + { url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, ] [[package]] @@ -4147,6 +4263,7 @@ name = "nvidia-cublas-cu12" version = "12.8.4.1" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, ] @@ -4155,6 +4272,7 @@ name = "nvidia-cuda-cupti-cu12" version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, ] @@ -4164,6 +4282,7 @@ version = "12.8.93" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, ] [[package]] @@ -4171,18 +4290,20 @@ name = "nvidia-cuda-runtime-cu12" version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, ] [[package]] name = "nvidia-cudnn-cu12" -version = "9.10.2.21" +version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/277c51962ee46fa3e5b203ac5f76107c650f781d6891e681e28e6f3e9fe6/nvidia_cudnn_cu12-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:08caaf27fe556aca82a3ee3b5aa49a77e7de0cfcb7ff4e5c29da426387a8267e", size = 656910700, upload-time = "2026-02-03T20:40:25.508Z" }, + { url = "https://files.pythonhosted.org/packages/c5/41/65225d42fba06fb3dd3972485ea258e7dd07a40d6e01c95da6766ad87354/nvidia_cudnn_cu12-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ac6ad90a075bb33a94f2b4cf4622eac13dd4dc65cf6dd9c7572a318516a36625", size = 657906812, upload-time = "2026-02-03T20:44:12.638Z" }, ] [[package]] @@ -4206,6 +4327,7 @@ dependencies = [ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, ] @@ -4215,6 +4337,7 @@ version = "1.13.1.3" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, ] [[package]] @@ -4222,6 +4345,7 @@ name = "nvidia-curand-cu12" version = "10.3.9.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, ] @@ -4235,6 +4359,7 @@ dependencies = [ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, ] @@ -4246,6 +4371,7 @@ dependencies = [ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, ] wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, ] @@ -4254,23 +4380,24 @@ name = "nvidia-cusparselt-cu12" version = "0.7.1" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, ] [[package]] name = "nvidia-cutlass-dsl" -version = "4.4.2" +version = "4.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nvidia-cutlass-dsl-libs-base" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/03/678dab0383db1ddfc449da216220f40404189eb36eeed9d87a4fa4bdb0e6/nvidia_cutlass_dsl-4.4.2-py3-none-any.whl", hash = "sha256:7cfb9ef19062b055b9372c7a627004724e2755e4c8b16c3cc88807d64501a4ae", size = 10167, upload-time = "2026-03-16T02:18:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/f0/15/575d7df4fe2f3406f1cfc68be72aeff2834f8a696daf1cd5bee8017e4507/nvidia_cutlass_dsl-4.5.2-py3-none-any.whl", hash = "sha256:68ed1b63ca74aae87955012da9dfd7fdaae471329d0028b229b841c7192ccf52", size = 10179, upload-time = "2026-05-25T03:38:56.364Z" }, ] [[package]] name = "nvidia-cutlass-dsl-libs-base" -version = "4.4.2" +version = "4.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cuda-python" }, @@ -4278,12 +4405,14 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/7d/0df5e38d11e52cc72095a14d6448bc1c5d0d4b00b069a1189ca417fb225b/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2ec8812eeadcbb6fe20bda2e295ed9c00653f8253b78e33cf0ab65a47b829e73", size = 75473821, upload-time = "2026-03-16T02:27:08.371Z" }, - { url = "https://files.pythonhosted.org/packages/56/98/e264964741d9cc9816625d9600d17a5249fd5cbd8c2d166fb0d0c34dfe5a/nvidia_cutlass_dsl_libs_base-4.4.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:22e37b58f7a6f2f43bba533c4df8a088012122e0b4e9a632eca23937adeafb39", size = 74355593, upload-time = "2026-03-16T02:25:11.762Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c9/2f17950ee2deb4b5f6b82f8155515a21792fe296e81bb638f164d8e2ca9b/nvidia_cutlass_dsl_libs_base-4.4.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b59a052cbfb9a25747d1b6d413615456bea38d1f377da085af07c0d86a4c8b39", size = 75477304, upload-time = "2026-03-16T02:27:35.645Z" }, - { url = "https://files.pythonhosted.org/packages/e1/68/27380038ebd9c8eab4be364e833fea144aef597704f44948921668f7adf4/nvidia_cutlass_dsl_libs_base-4.4.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8e3324a33afa7424e93beae7e54a311e80db82b9e4ed4bba2aeeda1d6c888cd9", size = 74355765, upload-time = "2026-03-16T02:24:16.778Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/0dc7f2e5b5c65106a5bb05e60654f1a79abe92e27e9b00588a73cd26ca1f/nvidia_cutlass_dsl_libs_base-4.4.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:af96c1170569138b3cb965202907fbf5ab95d7c1dcc210952d00cdf9ab7b859a", size = 75472171, upload-time = "2026-03-16T02:28:03.136Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ae/0998f328b28b956d7eb399d16f4ee681ca318b306007264444a623e86c64/nvidia_cutlass_dsl_libs_base-4.4.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:95db0c8d1d56992e2f5c2dcd5b3baab0297bedc0cbcefc1e70b57acd934e7b23", size = 74356280, upload-time = "2026-03-16T02:25:43.789Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ef/e827e3c67d72adbf4e8f680bdf03b1b67723d9e1ae7c3d0a1751f39f69ce/nvidia_cutlass_dsl_libs_base-4.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d2a3c412287e356fbe48fe9f845d6d33cd35dea5e20d7e4f628c20957967cacd", size = 75643473, upload-time = "2026-05-25T03:49:15.857Z" }, + { url = "https://files.pythonhosted.org/packages/97/68/c1247ab848f26c4ab56e562eea0e3f31fc14c9aaf0d883afaa92d8f05592/nvidia_cutlass_dsl_libs_base-4.5.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15ef6a59193667e663934ef4873f8ccad37455e9b7c3c419c3072113b8aedf61", size = 74513226, upload-time = "2026-05-25T03:51:32.496Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f8/b192015e273ff023a35741d6d5e4a93e4819160dee3955fc5d3d53534450/nvidia_cutlass_dsl_libs_base-4.5.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:395bd77cf642aeef311313453e6582f11c9357a4b81fe620ea3daccd1fccab9b", size = 75645002, upload-time = "2026-05-25T03:48:01.887Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/bfe256ac08e5a6dfb11444809e54c76c3a2f05fff38dd173e2e71b95e4d2/nvidia_cutlass_dsl_libs_base-4.5.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e59da7d89e5e4f8514c6530843f910f9d8734d8042dcaa079c9d9c5063eb3514", size = 74514312, upload-time = "2026-05-25T03:50:56.343Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b2/7a5de500bb74915ab8b3875f4952ae07d562f33d06eef9b2569adf4c09ab/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:216eee6aa8107d35569f9451b66b03a3c53167841d1af9b630b966ef8d966e19", size = 75636795, upload-time = "2026-05-25T03:47:31.081Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bc/5f9dd8c05c3e2f435228224f0b0e76e324c1bf0a6dcd3cfb917b5e94bad7/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:12c29f7c1f1f82851092ba3869264dafafb035228c0d9827a8db08b884fb80ca", size = 74511193, upload-time = "2026-05-25T03:52:39.444Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/76a9d1ce5ade3f43ab6f10e361a9c1962d02177deeaf46f2c3684a7ae959/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:5aca392063ffbc7da30442a267928b22d4a2d37f9ea1db32e4487aa31b0fcc33", size = 75644393, upload-time = "2026-05-25T03:47:02.706Z" }, + { url = "https://files.pythonhosted.org/packages/15/84/08d695d2e0fa95891a2e5abd978f359d50125e4d1f056e54697d465fccc3/nvidia_cutlass_dsl_libs_base-4.5.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:abab8a0d2f3f5661533c366df78f973052b86a3b52b868d997a95dce5aa8f17b", size = 74514399, upload-time = "2026-05-25T03:50:20.841Z" }, ] [[package]] @@ -4297,32 +4426,36 @@ wheels = [ [[package]] name = "nvidia-modelopt" -version = "0.42.0" +version = "0.44.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ninja" }, { name = "numpy" }, { name = "nvidia-ml-py" }, + { name = "omegaconf" }, { name = "packaging" }, { name = "pulp" }, { name = "pydantic" }, + { name = "pyyaml" }, { name = "regex" }, { name = "rich" }, { name = "safetensors" }, { name = "scipy" }, + { name = "setuptools" }, { name = "torch" }, { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/61/a4/ad7f8d4ce21e1df1670aaaa05db45bece34a74c9fb44e4e77f668a24adce/nvidia_modelopt-0.42.0-py3-none-any.whl", hash = "sha256:3e8149b4d206b4ae51165f4f6a6d28fc9c2172406c948d5abcd8637b08db5c28", size = 1005332, upload-time = "2026-03-09T20:43:57.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/ab/7e12dd238638624cb9d48904e9205abe16a5b26bdd5f9b91e3357821cf90/nvidia_modelopt-0.44.0-py3-none-any.whl", hash = "sha256:9b54a853dfda161db97a0dfce4d7c24269d1d19966b5e2026094186af897f74d", size = 1604658, upload-time = "2026-05-13T20:47:04.007Z" }, ] [[package]] name = "nvidia-nccl-cu12" -version = "2.27.5" +version = "2.28.9" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, + { url = "https://files.pythonhosted.org/packages/08/c4/120d2dfd92dff2c776d68f361ff8705fdea2ca64e20b612fab0fd3f581ac/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:50a36e01c4a090b9f9c47d92cec54964de6b9fcb3362d0e19b8ffc6323c21b60", size = 296766525, upload-time = "2025-11-18T05:49:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/44dbb46b3d1b0ec61afda8e84837870f2f9ace33c564317d59b70bc19d3e/nvidia_nccl_cu12-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:485776daa8447da5da39681af455aa3b2c2586ddcf4af8772495e7c532c7e5ab", size = 296782137, upload-time = "2025-11-18T05:49:34.248Z" }, ] [[package]] @@ -4331,6 +4464,7 @@ version = "12.8.93" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, ] [[package]] @@ -4338,6 +4472,7 @@ name = "nvidia-nvshmem-cu12" version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, ] @@ -4346,6 +4481,7 @@ name = "nvidia-nvtx-cu12" version = "12.8.90" source = { registry = "https://pypi.org/simple" } wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, ] @@ -4390,7 +4526,7 @@ wheels = [ [[package]] name = "onnx" -version = "1.20.1" +version = "1.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ml-dtypes" }, @@ -4398,24 +4534,29 @@ dependencies = [ { name = "protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/93/942d2a0f6a70538eea042ce0445c8aefd46559ad153469986f29a743c01c/onnx-1.21.0.tar.gz", hash = "sha256:4d8b67d0aaec5864c87633188b91cc520877477ec0254eda122bef8be43cd764", size = 12074608, upload-time = "2026-03-27T21:33:36.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, - { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, - { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, - { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, - { url = "https://files.pythonhosted.org/packages/ea/bb/715fad292b255664f0e603f1b2ef7bf2b386281775f37406beb99fa05957/onnx-1.20.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3", size = 17912296, upload-time = "2026-01-10T01:39:48.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c3/541af12c3d45e159a94ee701100ba9e94b7bd8b7a8ac5ca6838569f894f8/onnx-1.20.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c", size = 17416925, upload-time = "2026-01-10T01:39:50.82Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/d5660a7d2ddf14f531ca66d409239f543bb290277c3f14f4b4b78e32efa3/onnx-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489", size = 17515602, upload-time = "2026-01-10T01:39:54.132Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/47225ab2a92562eff87ba9a1a028e3535d659a7157d7cde659003998b8e3/onnx-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a", size = 16395729, upload-time = "2026-01-10T01:39:57.577Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7d/1bbe626ff6b192c844d3ad34356840cc60fca02e2dea0db95e01645758b1/onnx-1.20.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def", size = 16348968, upload-time = "2026-01-10T01:40:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ae/cb644ec84c25e63575d9d8790fdcc5d1a11d67d3f62f872edb35fa38d158/onnx-1.21.0-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:fc2635400fe39ff37ebc4e75342cc54450eadadf39c540ff132c319bf4960095", size = 17965930, upload-time = "2026-03-27T21:32:48.089Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b6/eeb5903586645ef8a49b4b7892580438741acc3df91d7a5bd0f3a59ea9cb/onnx-1.21.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9003d5206c01fa2ff4b46311566865d8e493e1a6998d4009ec6de39843f1b59b", size = 17531344, upload-time = "2026-03-27T21:32:50.837Z" }, + { url = "https://files.pythonhosted.org/packages/a7/00/4823f06357892d1e60d6f34e7299d2ba4ed2108c487cc394f7ce85a3ff14/onnx-1.21.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9261bd580fb8548c9c37b3c6750387eb8f21ea43c63880d37b2c622e1684285", size = 17613697, upload-time = "2026-03-27T21:32:54.222Z" }, + { url = "https://files.pythonhosted.org/packages/23/1d/391f3c567ae068c8ac4f1d1316bae97c9eb45e702f05975fe0e17ad441f0/onnx-1.21.0-cp312-abi3-win32.whl", hash = "sha256:9ea4e824964082811938a9250451d89c4ec474fe42dd36c038bfa5df31993d1e", size = 16287200, upload-time = "2026-03-27T21:32:57.277Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a6/5eefbe5b40ea96de95a766bd2e0e751f35bdea2d4b951991ec9afaa69531/onnx-1.21.0-cp312-abi3-win_amd64.whl", hash = "sha256:458d91948ad9a7729a347550553b49ab6939f9af2cddf334e2116e45467dc61f", size = 16441045, upload-time = "2026-03-27T21:33:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/63/c4/0ed8dc037a39113d2a4d66e0005e07751c299c46b993f1ad5c2c35664c20/onnx-1.21.0-cp312-abi3-win_arm64.whl", hash = "sha256:ca14bc4842fccc3187eb538f07eabeb25a779b39388b006db4356c07403a7bbb", size = 16403134, upload-time = "2026-03-27T21:33:03.987Z" }, + { url = "https://files.pythonhosted.org/packages/f8/89/0e1a9beb536401e2f45ac88735e123f2735e12fc7b56ff6c11727e097526/onnx-1.21.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:257d1d1deb6a652913698f1e3f33ef1ca0aa69174892fe38946d4572d89dd94f", size = 17975430, upload-time = "2026-03-27T21:33:07.005Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e6dc71a7b3b317265591b20a5f71d0ff5c0d26c24e52283139dc90c66038/onnx-1.21.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cd7cb8f6459311bdb557cbf6c0ccc6d8ace11c304d1bba0a30b4a4688e245f8", size = 17537435, upload-time = "2026-03-27T21:33:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/27affcac63eaf2ef183a44fd1a1354b11da64a6c72fe6f3fdcf5571bcee5/onnx-1.21.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b58a4cfec8d9311b73dc083e4c1fa362069267881144c05139b3eba5dc3a840", size = 17617687, upload-time = "2026-03-27T21:33:12.619Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5c/ac8ed15e941593a3672ce424280b764979026317811f2e8508432bfc3429/onnx-1.21.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1a9baf882562c4cebf79589bebb7cd71a20e30b51158cac3e3bbaf27da6163bd", size = 16449402, upload-time = "2026-03-27T21:33:15.555Z" }, + { url = "https://files.pythonhosted.org/packages/0e/aa/d2231e0dcaad838217afc64c306c8152a080134d2034e247cc973d577674/onnx-1.21.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bba12181566acf49b35875838eba49536a327b2944664b17125577d230c637ad", size = 16408273, upload-time = "2026-03-27T21:33:18.599Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/8905b14694def6ad23edf1011fdd581500384062f8c4c567e114be7aa272/onnx-1.21.0-cp314-cp314t-macosx_12_0_universal2.whl", hash = "sha256:7ee9d8fd6a4874a5fa8b44bbcabea104ce752b20469b88bc50c7dcf9030779ad", size = 17975331, upload-time = "2026-03-27T21:33:21.69Z" }, + { url = "https://files.pythonhosted.org/packages/61/28/f4e401e5199d1b9c8b76c7e7ae1169e050515258e877b58fa8bb49d3bdcc/onnx-1.21.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5489f25fe461e7f32128218251a466cabbeeaf1eaa791c79daebf1a80d5a2cc9", size = 17537430, upload-time = "2026-03-27T21:33:24.547Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/5d13320eb3660d5af360ea3b43aa9c63a70c92a9b4d1ea0d34501a32fcb8/onnx-1.21.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db17fc0fec46180b6acbd1d5d8650a04e5527c02b09381da0b5b888d02a204c8", size = 17617662, upload-time = "2026-03-27T21:33:27.418Z" }, + { url = "https://files.pythonhosted.org/packages/4d/50/3eaa1878338247be021e6423696813d61e77e534dccbd15a703a144e703d/onnx-1.21.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19d9971a3e52a12968ae6c70fd0f86c349536de0b0c33922ecdbe52d1972fe60", size = 16463688, upload-time = "2026-03-27T21:33:30.229Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/38d46b43bbb525e0b6a4c2c4204cc6795d67e45687a2f7403e06d8e7053d/onnx-1.21.0-cp314-cp314t-win_arm64.whl", hash = "sha256:efba467efb316baf2a9452d892c2f982b9b758c778d23e38c7f44fa211b30bb9", size = 16423387, upload-time = "2026-03-27T21:33:33.446Z" }, ] [[package]] name = "onnx-ir" -version = "0.2.0" +version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ml-dtypes" }, @@ -4424,14 +4565,14 @@ dependencies = [ { name = "sympy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/a5/acc43c8fa6edbc584d127fb6bbd13ae9ebfc01b9675c74e0da2de15fa4a6/onnx_ir-0.2.0.tar.gz", hash = "sha256:8bad3906691987290789b26d05e0dbff467029a0b1e411e12e4cae02e43503e4", size = 141693, upload-time = "2026-02-24T02:31:10.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/e6/672fefb2f108d077f58181a7babf4c0f8d1182a30353ffc9c79c63afc5ee/onnx_ir-0.2.1.tar.gz", hash = "sha256:8b8b10a93f43e65962104de6070c43c5dacb0e3cdfefc7c8059dd83c9db64f35", size = 144279, upload-time = "2026-04-20T20:21:47.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/df/a99736bcca6b16e36c687ce4996abcf4ce73c514fddd9e730cfcb6a334f2/onnx_ir-0.2.0-py3-none-any.whl", hash = "sha256:eb14d1399c2442bd1ff702719e70074e9cedfa3af5729416a32752c9e0f82591", size = 164100, upload-time = "2026-02-24T02:31:09.454Z" }, + { url = "https://files.pythonhosted.org/packages/8c/aa/f7a53321c60b9ad9ee184b6018292ed6b5389947592a2c8c09c736bb7f9e/onnx_ir-0.2.1-py3-none-any.whl", hash = "sha256:c7285da889312f91882de2092e298a9eeeefbfc1d1951c49d983992967eb09a7", size = 166792, upload-time = "2026-04-20T20:21:46.357Z" }, ] [[package]] name = "onnxscript" -version = "0.6.2" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ml-dtypes" }, @@ -4441,9 +4582,9 @@ dependencies = [ { name = "packaging" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/2b/538fdeb0e25bed5d7e0f954af5710543e2629499fb74381afc3333f8a8ae/onnxscript-0.6.2.tar.gz", hash = "sha256:abb2e6f464db40c9b8c7fbb3e64cca04cf3f4495e67c4eda5eac17b784191ce3", size = 590865, upload-time = "2026-02-10T22:53:39.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/99/fd948eba63ba65b52265a4cd09a14f96bb9f5b730fcef58876c4358bf406/onnxscript-0.7.0.tar.gz", hash = "sha256:c95ed7b339b02cface56ee27689565c46612e1fc542c562298dddfdad5268dc5", size = 612032, upload-time = "2026-04-20T17:09:19.775Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/56/e6b179397497ab93266b6eb00743403a6a699a29063a423c4a14595d3db9/onnxscript-0.6.2-py3-none-any.whl", hash = "sha256:20e3c3fd1da19b3655549d5455a2df719db47374fe430e01e865ae69127c37b9", size = 689064, upload-time = "2026-02-10T22:53:41.663Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ce/2ed92575cc3be4ea1db5f38f16f20765f9b20b69b14d6c1d9972658a8ee9/onnxscript-0.7.0-py3-none-any.whl", hash = "sha256:5b356907d4501e9919f8599c91d8da967406a37b1fac2b40caa55a49acf242ea", size = 714842, upload-time = "2026-04-20T17:09:22.089Z" }, ] [[package]] @@ -4467,7 +4608,7 @@ wheels = [ [[package]] name = "openai" -version = "2.24.0" +version = "2.38.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -4479,9 +4620,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/12/cfa322c5f5dd8fa21aab9a7a8e979e7a11123800f86ca8d82eb68a83d213/openai-2.38.0.tar.gz", hash = "sha256:798694c6cf74145541fda94325b6f8f72d8e1fd0262cc137c8d728177a6a4ce3", size = 772764, upload-time = "2026-05-21T21:23:42.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/ccff9be562e24207716d04ef9dc931c76aff0c89a7265da43e2104d7fe06/openai-2.38.0-py3-none-any.whl", hash = "sha256:ec6661c57b2dcc47414a767e6e3335c7ed3d19c9696999283a3c82e95c756a3c", size = 1344910, upload-time = "2026-05-21T21:23:39.636Z" }, ] [[package]] @@ -4531,15 +4672,18 @@ langgraph = [ megatron = [ { name = "apex" }, { name = "deep-ep", marker = "sys_platform == 'linux'" }, + { name = "flash-attn-4" }, { name = "megatron-bridge" }, { name = "megatron-core" }, { name = "ml-dtypes", marker = "python_full_version < '3.13'" }, + { name = "ninja" }, { name = "numpy" }, { name = "nvidia-ml-py" }, { name = "nvidia-modelopt", marker = "sys_platform != 'darwin'" }, { name = "nvidia-resiliency-ext" }, { name = "pybind11" }, { name = "quack-kernels" }, + { name = "tilelang", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "torch" }, { name = "transformer-engine" }, { name = "transformer-engine-cu12" }, @@ -4595,6 +4739,7 @@ requires-dist = [ { name = "deep-ep", marker = "sys_platform == 'linux' and extra == 'megatron'", git = "https://github.com/deepseek-ai/DeepEP.git?rev=v1.2.1" }, { name = "duckdb", marker = "extra == 'backend'", specifier = ">=1.0.0" }, { name = "fastapi", marker = "extra == 'tinker'", specifier = ">=0.128.0" }, + { name = "flash-attn-4", marker = "extra == 'megatron'", url = "https://files.pythonhosted.org/packages/24/f7/01ee2576ce41f9884d291ee21861ef194afc0b2b1ce3bd175fc7a6e1b133/flash_attn_4-4.0.0b5-py3-none-any.whl" }, { name = "gql", marker = "extra == 'backend'", specifier = ">=4.0.0" }, { name = "hf-xet", marker = "extra == 'backend'", specifier = ">=1.1.0" }, { name = "huggingface-hub", marker = "extra == 'tinker'" }, @@ -4610,6 +4755,7 @@ requires-dist = [ { name = "nbclient", marker = "extra == 'backend'", specifier = ">=0.10.1" }, { name = "nbmake", marker = "extra == 'backend'", specifier = ">=1.5.5" }, { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "ninja", marker = "extra == 'megatron'", specifier = ">=1.11.1" }, { name = "numpy", marker = "extra == 'megatron'", specifier = "<2" }, { name = "numpy", marker = "extra == 'tinker'", specifier = "<2" }, { name = "nvidia-cudnn-frontend", marker = "sys_platform == 'linux' and extra == 'backend'", specifier = "<1.21" }, @@ -4627,16 +4773,17 @@ requires-dist = [ { name = "pybind11", marker = "extra == 'megatron'", specifier = ">=2.13.6" }, { name = "pydantic", marker = "extra == 'tinker'", specifier = ">=2.12.5" }, { name = "pytest", marker = "extra == 'backend'", specifier = ">=8.4.1" }, - { name = "quack-kernels", marker = "extra == 'megatron'", specifier = "==0.2.5" }, + { name = "quack-kernels", marker = "extra == 'megatron'", specifier = "==0.3.7" }, { name = "seaborn", marker = "extra == 'plotting'", specifier = ">=0.13.2" }, { name = "setproctitle", specifier = ">=1.3.6" }, { name = "setuptools", marker = "extra == 'backend'", specifier = ">=78.1.0" }, { name = "tblib", specifier = ">=3.0.0" }, + { name = "tilelang", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'megatron'", specifier = "==0.1.10" }, { name = "tinker", marker = "extra == 'tinker'", specifier = ">=0.21.0,<0.22" }, { name = "tinker-cookbook", marker = "extra == 'tinker'", specifier = ">=0.4.1,<0.5" }, - { name = "torch", marker = "extra == 'backend'", specifier = "==2.10.0" }, - { name = "torch", marker = "extra == 'megatron'", specifier = "==2.10.0" }, - { name = "torch", marker = "extra == 'tinker'", specifier = "==2.10.0" }, + { name = "torch", marker = "extra == 'backend'", specifier = ">=2.11.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torch", marker = "extra == 'megatron'", specifier = ">=2.11.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torch", marker = "extra == 'tinker'", specifier = ">=2.11.0", index = "https://download.pytorch.org/whl/cu128" }, { name = "torchao", marker = "extra == 'backend'", specifier = "==0.16.0" }, { name = "transformer-engine", marker = "extra == 'megatron'", specifier = "==2.11.0" }, { name = "transformer-engine-cu12", marker = "extra == 'megatron'", specifier = "==2.11.0" }, @@ -4674,15 +4821,44 @@ dev = [ [[package]] name = "opentelemetry-api" -version = "1.33.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, - { name = "importlib-metadata" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/8d/1f5a45fbcb9a7d87809d460f09dc3399e3fbd31d7f3e14888345e9d29951/opentelemetry_api-1.33.1.tar.gz", hash = "sha256:1c6055fc0a2d3f23a50c7e17e16ef75ad489345fd3df1f8b8af7c0bbf8a109e8", size = 65002, upload-time = "2025-05-16T18:52:41.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/44/4c45a34def3506122ae61ad684139f0bbc4e00c39555d4f7e20e0e001c8a/opentelemetry_api-1.33.1-py3-none-any.whl", hash = "sha256:4db83ebcf7ea93e64637ec6ee6fabee45c5cbe4abd9cf3da95c43828ddb50b83", size = 65771, upload-time = "2025-05-16T18:52:17.419Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" }, ] [[package]] @@ -4699,82 +4875,82 @@ wheels = [ [[package]] name = "opentelemetry-sdk" -version = "1.33.1" +version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/12/909b98a7d9b110cce4b28d49b2e311797cffdce180371f35eba13a72dd00/opentelemetry_sdk-1.33.1.tar.gz", hash = "sha256:85b9fcf7c3d23506fbc9692fd210b8b025a1920535feec50bd54ce203d57a531", size = 161885, upload-time = "2025-05-16T18:52:52.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/8e/ae2d0742041e0bd7fe0d2dcc5e7cce51dcf7d3961a26072d5b43cc8fa2a7/opentelemetry_sdk-1.33.1-py3-none-any.whl", hash = "sha256:19ea73d9a01be29cacaa5d6c8ce0adc0b7f7b4d58cc52f923e4413609f670112", size = 118950, upload-time = "2025-05-16T18:52:37.297Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.54b1" +version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "deprecated" }, { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/2c/d7990fc1ffc82889d466e7cd680788ace44a26789809924813b164344393/opentelemetry_semantic_conventions-0.54b1.tar.gz", hash = "sha256:d1cecedae15d19bdaafca1e56b29a66aa286f50b5d08f036a145c7f3e9ef9cee", size = 118642, upload-time = "2025-05-16T18:52:53.962Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/80/08b1698c52ff76d96ba440bf15edc2f4bc0a279868778928e947c1004bdd/opentelemetry_semantic_conventions-0.54b1-py3-none-any.whl", hash = "sha256:29dab644a7e435b58d3a3918b58c333c92686236b30f7891d5e51f02933ca60d", size = 194938, upload-time = "2025-05-16T18:52:38.796Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, ] [[package]] name = "orjson" -version = "3.11.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, - { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, - { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, - { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, - { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, ] [[package]] @@ -4818,11 +4994,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -4874,7 +5050,7 @@ wheels = [ [[package]] name = "paramiko" -version = "4.0.0" +version = "5.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt" }, @@ -4882,18 +5058,18 @@ dependencies = [ { name = "invoke" }, { name = "pynacl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743, upload-time = "2025-08-04T01:02:03.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/62/93/dcc25d52f49022ae6175d15e6bd751f1acc99b98bc61fc55e5155a7be2e7/paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79", size = 1548586, upload-time = "2026-05-09T18:28:52.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932, upload-time = "2025-08-04T01:02:02.029Z" }, + { url = "https://files.pythonhosted.org/packages/82/5b/eadf6d45de38d30ab603f49393b6cd2cbe7e233af8cf90197e32782b68a9/paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c", size = 208919, upload-time = "2026-05-09T18:28:50.295Z" }, ] [[package]] name = "parso" -version = "0.8.6" +version = "0.8.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, ] [[package]] @@ -4907,11 +5083,11 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -4929,7 +5105,7 @@ wheels = [ [[package]] name = "peft" -version = "0.18.1" +version = "0.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -4943,9 +5119,9 @@ dependencies = [ { name = "tqdm" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/48/147b3ea999560b40a34fd78724c7777aa9d18409c2250bdcaf9c4f2db7fc/peft-0.18.1.tar.gz", hash = "sha256:2dd0d6bfce936d1850e48aaddbd250941c5c02fc8ef3237cd8fd5aac35e0bae2", size = 635030, upload-time = "2026-01-09T13:08:01.136Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/cf/037f1e3d5186496c05513a6754639e2dab3038a05f384284d49a9bd06a2d/peft-0.19.1.tar.gz", hash = "sha256:0d97542fe96dcdaa20d3b81c06f26f988618f416a73544ab23c3618ccb674a40", size = 763738, upload-time = "2026-04-16T15:46:45.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/14/b4e3f574acf349ae6f61f9c000a77f97a3b315b4bb6ad03791e79ae4a568/peft-0.18.1-py3-none-any.whl", hash = "sha256:0bf06847a3551e3019fc58c440cffc9a6b73e6e2962c95b52e224f77bbdb50f1", size = 556960, upload-time = "2026-01-09T13:07:55.865Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" }, ] [[package]] @@ -5014,89 +5190,89 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] [[package]] name = "pip" -version = "26.0.1" +version = "26.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/83/0d7d4e9efe3344b8e2fe25d93be44f64b65364d3c8d7bc6dc90198d5422e/pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8", size = 1812747, upload-time = "2026-02-05T02:20:18.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/48/cb9b7a682f6fe01a4221e1728941dd4ac3cd9090a17db3779d6ff490b602/pip-26.1.1.tar.gz", hash = "sha256:d36762751d156a4ee895de8af39aa0abeeeb577f93a2eca6ab62467bbf0f8a78", size = 1840400, upload-time = "2026-05-04T19:02:21.248Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b", size = 1787723, upload-time = "2026-02-05T02:20:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl", hash = "sha256:99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb", size = 1812777, upload-time = "2026-05-04T19:02:18.9Z" }, ] [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -5110,30 +5286,30 @@ wheels = [ [[package]] name = "polars" -version = "1.39.3" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "polars-runtime-32" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/84/af/5fd97632f49ffe46b887b9931e19ec38ae1e3d9198be86dccd465dc6f1b3/polars-1.41.1.tar.gz", hash = "sha256:4a8df19475a68c3b4a65466b2683fc3a9a76053a591cde1748d84b690aff9338", size = 737807, upload-time = "2026-05-27T19:54:41.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/68/ef/cdd8bf7e46e94c4cb8f7c092c9c2c731a734a2dc3076516a85e457845b92/polars-1.41.1-py3-none-any.whl", hash = "sha256:b758df44b0d5dc3f19b2d81eaa3c617d53196226163d41e7ccd240ab494274da", size = 833213, upload-time = "2026-05-27T19:53:28.752Z" }, ] [[package]] name = "polars-runtime-32" -version = "1.39.3" +version = "1.41.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/96/cd099e525716cc8549096e3cc1c1f13b9d97d00588d40781f42a09243b4c/polars_runtime_32-1.41.1.tar.gz", hash = "sha256:84cb75c70bf48fd27a9c2c83b9ade7eadd647eee6c3df3ed2dcc7dccfd5ad56e", size = 2989104, upload-time = "2026-05-27T19:54:43.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" }, - { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" }, - { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" }, - { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" }, - { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" }, - { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" }, + { url = "https://files.pythonhosted.org/packages/65/be/d3777241935a5ba3a54b1bc89e81f9a640d13c30f38b37f8e677a5683288/polars_runtime_32-1.41.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:3791802e0665ab66e72cdacf94966fd409f408acd7d16c1a31ecc74ea06aa6e8", size = 52210540, upload-time = "2026-05-27T19:53:31.669Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/846140d1fbdada68b467116c65845935eb82f5ac92884573b0906ae8fcb2/polars_runtime_32-1.41.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:df0de10d152ebd2fb3cccd0a2a26db68138440bc44164e831a7c9a50f73adf8b", size = 46504153, upload-time = "2026-05-27T19:53:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a7/873f38f71e20e747ec4d4aea8b5c510e279b3447efc5bac725a954923f8e/polars_runtime_32-1.41.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b544fcf95219857d698f5b61583309dcc5469443fbcb688356d5913169cf37df", size = 50393809, upload-time = "2026-05-27T19:53:37.64Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6b/6e9f6818e2b8258be5127a3455a6e09d99770f14098e9d5bfd85c2b3aa71/polars_runtime_32-1.41.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d279ad9036293592396988a46046d73cc340a3bd51fa82fa6993822632ed11f9", size = 56368917, upload-time = "2026-05-27T19:53:40.857Z" }, + { url = "https://files.pythonhosted.org/packages/00/ec/33fd93f4d6f251c3a4125668d8e4b6fc25b7abdb3cd13aecb4cda2252d56/polars_runtime_32-1.41.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d25e6e99a85488943b6377194e1dd6391281027d355e5da3f607705434b9756f", size = 50564909, upload-time = "2026-05-27T19:53:44.096Z" }, + { url = "https://files.pythonhosted.org/packages/11/5f/0c893aacc1aa4f78b15c0eeab44fce69487ff659fb2145e894bd3f5df451/polars_runtime_32-1.41.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1fec5be999825a1956392c682d0c04426b4fc40c4e16bc166807f36b5056d10e", size = 54289924, upload-time = "2026-05-27T19:53:47.067Z" }, + { url = "https://files.pythonhosted.org/packages/b0/98/996c48e2f94b8c81d6ac9e513fdd75a612015c40a367ab885ef7d053f08a/polars_runtime_32-1.41.1-cp310-abi3-win_amd64.whl", hash = "sha256:dfb9eff25fca1b67d6381895313d05a3510f3c0cdba0bd0cf24cba9071aa190b", size = 51946331, upload-time = "2026-05-27T19:53:50.066Z" }, + { url = "https://files.pythonhosted.org/packages/ac/67/c053610d3609263d4d4412390ced8db8e45322c1358ec6ef5359457a6ae5/polars_runtime_32-1.41.1-cp310-abi3-win_arm64.whl", hash = "sha256:348f4fe9ebacf904b71ecc7c293314292117a78c6464aa0b5781db7ffc425c56", size = 45957732, upload-time = "2026-05-27T19:53:52.816Z" }, ] [[package]] @@ -5163,26 +5339,26 @@ wheels = [ [[package]] name = "prek" -version = "0.3.8" +version = "0.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/ee/03e8180e3fda9de25b6480bd15cc2bde40d573868d50648b0e527b35562f/prek-0.3.8.tar.gz", hash = "sha256:434a214256516f187a3ab15f869d950243be66b94ad47987ee4281b69643a2d9", size = 400224, upload-time = "2026-03-23T08:23:35.981Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/3b/a0ae60bbd4c4735f20aeddfbd3c50fb669cd8e99c078a3ed75a6a4a5c6d7/prek-0.4.3.tar.gz", hash = "sha256:e486307ea649e7300b3535fac52fe0ba0b80aebe23143b662659d16e6a7c8b47", size = 461800, upload-time = "2026-05-27T03:18:58.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/84/40d2ddf362d12c4cd4a25a8c89a862edf87cdfbf1422aa41aac8e315d409/prek-0.3.8-py3-none-linux_armv6l.whl", hash = "sha256:6fb646ada60658fa6dd7771b2e0fb097f005151be222f869dada3eb26d79ed33", size = 5226646, upload-time = "2026-03-23T08:23:18.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/52/7308a033fa43b7e8e188797bd2b3b017c0f0adda70fa7af575b1f43ea888/prek-0.3.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3d7fdadb15efc19c09953c7a33cf2061a70f367d1e1957358d3ad5cc49d0616", size = 5620104, upload-time = "2026-03-23T08:23:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b1/f106ac000a91511a9cd80169868daf2f5b693480ef5232cec5517a38a512/prek-0.3.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:72728c3295e79ca443f8c1ec037d2a5b914ec73a358f69cf1bc1964511876bf8", size = 5199867, upload-time = "2026-03-23T08:23:38.066Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e9/970713f4b019f69de9844e1bab37b8ddb67558e410916f4eb5869a696165/prek-0.3.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:48efc28f2f53b5b8087efca9daaed91572d62df97d5f24a1c7a087fecb5017de", size = 5441801, upload-time = "2026-03-23T08:23:32.617Z" }, - { url = "https://files.pythonhosted.org/packages/12/a4/7ef44032b181753e19452ec3b09abb3a32607cf6b0a0508f0604becaaf2b/prek-0.3.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6ca9d63bacbc448a5c18e955c78d3ac5176c3a17c3baacdd949b1a623e08a36", size = 5155107, upload-time = "2026-03-23T08:23:31.021Z" }, - { url = "https://files.pythonhosted.org/packages/bd/77/4d9c8985dbba84149760785dfe07093ea1e29d710257dfb7c89615e2234c/prek-0.3.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1000f7029696b4fe712fb1fefd4c55b9c4de72b65509c8e50296370a06f9dc3f", size = 5566541, upload-time = "2026-03-23T08:23:45.694Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1a/81e6769ac1f7f8346d09ce2ab0b47cf06466acd9ff72e87e5d1f0d98cd32/prek-0.3.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ff0bed0e2c1286522987d982168a86cbbd0d069d840506a46c9fda983515517", size = 6552991, upload-time = "2026-03-23T08:23:21.958Z" }, - { url = "https://files.pythonhosted.org/packages/6f/fa/ce2df0dd2dc75a9437a52463239d0782998943d7b04e191fb89b83016c34/prek-0.3.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb087ac0ffda3ac65bbbae9a38326a7fd27ee007bb4a94323ce1eb539d8bbec", size = 5832972, upload-time = "2026-03-23T08:23:20.258Z" }, - { url = "https://files.pythonhosted.org/packages/18/6b/9d4269df9073216d296244595a21c253b6475dfc9076c0bd2906be7a436c/prek-0.3.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2e1e5e206ff7b31bd079cce525daddc96cd6bc544d20dc128921ad92f7a4c85d", size = 5448371, upload-time = "2026-03-23T08:23:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/60/1d/1e4d8a78abefa5b9d086e5a9f1638a74b5e540eec8a648d9946707701f29/prek-0.3.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dcea3fe23832a4481bccb7c45f55650cb233be7c805602e788bb7dba60f2d861", size = 5270546, upload-time = "2026-03-23T08:23:24.231Z" }, - { url = "https://files.pythonhosted.org/packages/77/07/34f36551a6319ae36e272bea63a42f59d41d2d47ab0d5fb00eb7b4e88e87/prek-0.3.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:4d25e647e9682f6818ab5c31e7a4b842993c14782a6ffcd128d22b784e0d677f", size = 5124032, upload-time = "2026-03-23T08:23:26.368Z" }, - { url = "https://files.pythonhosted.org/packages/e3/01/6d544009bb655e709993411796af77339f439526db4f3b3509c583ad8eb9/prek-0.3.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:de528b82935e33074815acff3c7c86026754d1212136295bc88fe9c43b4231d5", size = 5432245, upload-time = "2026-03-23T08:23:47.877Z" }, - { url = "https://files.pythonhosted.org/packages/54/96/1237ee269e9bfa283ffadbcba1f401f48a47aed2b2563eb1002740d6079d/prek-0.3.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6d660f1c25a126e6d9f682fe61449441226514f412a4469f5d71f8f8cad56db2", size = 5950550, upload-time = "2026-03-23T08:23:43.8Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6b/a574411459049bc691047c9912f375deda10c44a707b6ce98df2b658f0b3/prek-0.3.8-py3-none-win32.whl", hash = "sha256:b0c291c577615d9f8450421dff0b32bfd77a6b0d223ee4115a1f820cb636fdf1", size = 4949501, upload-time = "2026-03-23T08:23:16.338Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b4/46b59fe49f635acd9f6530778ce577f9d8b49452835726a5311ffc902c67/prek-0.3.8-py3-none-win_amd64.whl", hash = "sha256:bc147fdbdd4ec33fc7a987b893ecb69b1413ac100d95c9889a70f3fd58c73d06", size = 5346551, upload-time = "2026-03-23T08:23:34.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/05/9cca1708bb8c65264124eb4b04251e0f65ce5bfc707080bb6b492d5a0df7/prek-0.3.8-py3-none-win_arm64.whl", hash = "sha256:a2614647aeafa817a5802ccb9561e92eedc20dcf840639a1b00826e2c2442515", size = 5190872, upload-time = "2026-03-23T08:23:29.463Z" }, + { url = "https://files.pythonhosted.org/packages/df/be/980a0512f7eec3469dd40574f4e35d9ce7b67b358fea58888d13a0625b0d/prek-0.4.3-py3-none-linux_armv6l.whl", hash = "sha256:c67109de8d9766c2afd6e7e64feb9e1a0d3eceb3b4123280c28344660c1a97cd", size = 5541730, upload-time = "2026-05-27T03:19:09.119Z" }, + { url = "https://files.pythonhosted.org/packages/ef/55/937d707cc01d311e5c856c7019bc7db2c5e1835728396bb1ea32a7ecfdfd/prek-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b43a85f5ddf7827a75491e79ca068a49c5e4efde8dbac844ecb89622a78458e4", size = 5906762, upload-time = "2026-05-27T03:19:21.651Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6a/9a99ac481eb148dba55652df88b029ab6c1f90384bd51996026cdab2dafb/prek-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e99ee90a7b6e84dabef891ff7521eb59dae38953467bdb482f004ea522d3a64c", size = 5461541, upload-time = "2026-05-27T03:18:55.984Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8d/9056b02a100cc18b101fc05ecc82635889f5f8cb1cce5d70b027e517a6d9/prek-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f514ec0d95cd4578d74d4601058bd259f5baf91c937f2aaae942d4b070b8077f", size = 5720501, upload-time = "2026-05-27T03:18:52.424Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ea/efbe4523e53022d94272ddfdd3a198ace7de004dd8830a69318085a10393/prek-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03a4ac3c3023a76faa52ad7775720599b10241930be8902c471085b22572b4b0", size = 5452412, upload-time = "2026-05-27T03:19:17.801Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/8a48b2c6a5117110d688c2d8ca2526264ad9f0d3baed4587038ee85e4c2d/prek-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40173425ab82bf0a7267d672b3e3aae9dd425eaee3a3641c6a5f040da3ff95e4", size = 5849515, upload-time = "2026-05-27T03:19:05.545Z" }, + { url = "https://files.pythonhosted.org/packages/ec/66/ccce7a1b6c6b610a22b54092d523ea7d35709e42864dace3734c05dd5f98/prek-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b8d99ee3277f8f3a3453a953120ee5c6c52f7ad89e459a25425cf62135f47b1", size = 6743978, upload-time = "2026-05-27T03:19:07.445Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f8/7a441d780c42e858ad677c82bb54eb3f01b424b710a8db5b9a8782305326/prek-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08595fe96d24c1fe13486b00d55ce73a7b37040a16e82365942606594c67a6b", size = 6108774, upload-time = "2026-05-27T03:19:03.565Z" }, + { url = "https://files.pythonhosted.org/packages/bf/38/fbb1afe14c7536109c68a1d9ca602f152f1929972d006c517c3b92140192/prek-0.4.3-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8607d636ef9232675507d97d252e1dcca5628bff79cb069fa945fff09d7bbb43", size = 5723165, upload-time = "2026-05-27T03:18:59.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b8/edafebce2bbd85f9e9de2781c225d690eb2b9897a06b224f5c24658fe398/prek-0.4.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:89484765304a779780f83489eb3aed5de5366f47fce7713fa5a917ebc281baa0", size = 5560557, upload-time = "2026-05-27T03:18:49.59Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2e/a85a40458ac50c452cae2ddd2eed0b70107fd2b4074d7a5003088ac508f1/prek-0.4.3-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:2d2b0c12e3d1c6d90646f9faa2d4c66f9861f3c6e577d7dbd25e733ed095ac56", size = 5417874, upload-time = "2026-05-27T03:19:01.681Z" }, + { url = "https://files.pythonhosted.org/packages/4c/be/106fb026646e1da65da6d2a5f3cfbda817e68a72429645351b7033c0b2b5/prek-0.4.3-py3-none-musllinux_1_1_i686.whl", hash = "sha256:ca6802eaf191acb6166e9e013dd277ea193ba27c1dca896ab7debf6dca758b6d", size = 5710013, upload-time = "2026-05-27T03:18:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/edfff5f7d9b6c9e5860dfe05c9488e1b96de990b652db2e379d45af8ad2e/prek-0.4.3-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a46862d81078d2c8caa286c392f965ed72fb72eb1fed171910ba54fe8d546ed0", size = 6230160, upload-time = "2026-05-27T03:19:15.58Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/70adf26d5da0b7a66d8e284a661feddd5e8c69784b82084f40485fa321e4/prek-0.4.3-py3-none-win32.whl", hash = "sha256:f78e343584cfff106fc3c361109b87949ad8028dc5aa667e0fccd26db8170d7d", size = 5226844, upload-time = "2026-05-27T03:19:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/9f21aca8ccee6978db831dbf36c2e17461692c75dd291c9b3d170e39a82a/prek-0.4.3-py3-none-win_amd64.whl", hash = "sha256:798d04437d30d6b4e6c1d520fe6ca800c340c9246f0dc8900d8b365df54b71b6", size = 5616068, upload-time = "2026-05-27T03:19:19.653Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ef/cad8f9c66bcc199e22d1ad82a50032067a4c8b4182306d3472ff99f64aa3/prek-0.4.3-py3-none-win_arm64.whl", hash = "sha256:70d9da5fc14ef41565ff7ba9f476fb53166bf719a954339b2e9f42ed494a2f71", size = 5448057, upload-time = "2026-05-27T03:19:11.118Z" }, ] [[package]] @@ -5199,11 +5375,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.24.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, ] [[package]] @@ -5220,98 +5396,108 @@ wheels = [ [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] name = "proto-plus" -version = "1.27.1" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/56/e647b0c675392d2da368da7b6f158f7368b18542fd6f7d7400a2f39de000/proto_plus-1.28.0.tar.gz", hash = "sha256:38e5696342835b08fc116f30a25665b29531cda9d5d5643e9b81fc312385abd9", size = 57221, upload-time = "2026-05-07T08:04:50.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/7c/20/b122d4626976acb81132036d2ad1bb35a1a8775fceb837ec30964622516a/proto_plus-1.28.0-py3-none-any.whl", hash = "sha256:a630604310899e73c59ec302e5765c058d412b2f090b9c79c8822589f14955b8", size = 50410, upload-time = "2026-05-07T08:03:31.962Z" }, ] [[package]] @@ -5359,43 +5545,43 @@ wheels = [ [[package]] name = "psycopg2-binary" -version = "2.9.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, - { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, - { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, - { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, - { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, - { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, - { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, - { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, - { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, ] [[package]] @@ -5409,11 +5595,11 @@ wheels = [ [[package]] name = "pulp" -version = "3.3.0" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/1c/d880b739b841a8aa81143091c9bdda5e72e226a660aa13178cb312d4b27f/pulp-3.3.0.tar.gz", hash = "sha256:7eb99b9ce7beeb8bbb7ea9d1c919f02f003ab7867e0d1e322f2f2c26dd31c8ba", size = 16301847, upload-time = "2025-09-18T08:14:57.552Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/70/69be07a67621ad804d6cf347965eb4e0d7786a97330d99c31d735aaa6c5a/pulp-3.3.2.tar.gz", hash = "sha256:d0904700c207ac11e25e3b1213b70eae1d6fb25faa719d75f3f15054901258c0", size = 16305346, upload-time = "2026-05-25T09:41:26.207Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/6c/64cafaceea3f99927e84b38a362ec6a8f24f33061c90bda77dfe1cd4c3c6/pulp-3.3.0-py3-none-any.whl", hash = "sha256:dd6ad2d63f196d1254eddf9dcff5cd224912c1f046120cb7c143c5b0eda63fae", size = 16387700, upload-time = "2025-09-18T08:14:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/d674f1dde91c71e2ac19e5e5cf1ee6d5845e3aefecd9c39ed9c4b0c9a696/pulp-3.3.2-py3-none-any.whl", hash = "sha256:631b166f72086971a9597f7a0233ababa99bb8d50a01cd543f7758be5a9f86c0", size = 16391742, upload-time = "2026-05-25T09:41:22.2Z" }, ] [[package]] @@ -5500,11 +5686,11 @@ wheels = [ [[package]] name = "pybind11" -version = "3.0.2" +version = "3.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/98/9118a0659646f1628c592ef9bb48e0056efa6bf27c951fd12a178e0136fb/pybind11-3.0.2.tar.gz", hash = "sha256:432f01aeb68e361a3a7fc7575c2c7f497595bf640f747acd909ff238dd766e06", size = 577131, upload-time = "2026-02-17T04:46:52.556Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/f0/35145a3c3baffeef55d4b8324caa33abaa8fa56ab345ecd4b2211d09163e/pybind11-3.0.4.tar.gz", hash = "sha256:3286b59c8a774b9ee650169302dd5a4eedc30a8617905a0560dd8ee44775130c", size = 589533, upload-time = "2026-04-19T03:08:15.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/c5/e98d9c51f3d5300d5e40ad9037dd6b3b60736fd02ab68dcc98c96be7592d/pybind11-3.0.2-py3-none-any.whl", hash = "sha256:f8a6500548919cc33bcd220d5f984688326f574fa97f1107f2f4fdb4c6fb019f", size = 310158, upload-time = "2026-02-17T04:46:49.91Z" }, + { url = "https://files.pythonhosted.org/packages/b3/06/c3a23c9a0263b136c519f033a58d4641e73065fefc7754e9667ec206d992/pybind11-3.0.4-py3-none-any.whl", hash = "sha256:961720ee652da51d531b7b2451a6bd2bc042b0106e6d9baa48ecb7d58034ce63", size = 314166, upload-time = "2026-04-19T03:08:14.091Z" }, ] [[package]] @@ -5620,7 +5806,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -5628,9 +5814,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [package.optional-dependencies] @@ -5640,73 +5826,77 @@ email = [ [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, ] [[package]] @@ -5724,21 +5914,21 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.13.1" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] [[package]] name = "pydo" -version = "0.28.0" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -5747,27 +5937,27 @@ dependencies = [ { name = "msrest" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d6/a04ccb832d9bd2fc92a4c59d5df2b2920c239cdbeba134f191741679f4c9/pydo-0.28.0.tar.gz", hash = "sha256:853bd83cb9f12fb489d5fb05f21e86e158e4d562de54764b00f6cd7695173822", size = 2286659, upload-time = "2026-03-03T14:47:55.224Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/dc/6d5c162cfdacce6406b7c7cf36ac3cb8574852ebb4aaaa95888e0ab04f91/pydo-0.34.0.tar.gz", hash = "sha256:21788f218b53af2af722c5d9e54ecc382b149aa4cc590c33245abcc6c7a0e41b", size = 2702240, upload-time = "2026-05-06T13:05:08.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/41/d0253924ad2c6f38f92c702dd76c6374c0a18b0f1a43afacac1173bc8747/pydo-0.28.0-py3-none-any.whl", hash = "sha256:3b4d88270f4d3748041deeed02ead885cce6fb0fe22e935f4098a6f5242a9421", size = 2325125, upload-time = "2026-03-03T14:47:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c6/d80379afb6702c600af4d5d10b0585089b0a8ca5b49818a9e427982c2f7d/pydo-0.34.0-py3-none-any.whl", hash = "sha256:72d055890bd546b709d5661b56394b276e9ce65cf317c19616abe0085398cf3a", size = 2769505, upload-time = "2026-05-06T13:05:06.943Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyjwt" -version = "2.12.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] @@ -5842,16 +6032,16 @@ wheels = [ [[package]] name = "pyreadline3" -version = "3.5.4" +version = "3.5.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, ] [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -5860,22 +6050,22 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] name = "pytest-asyncio" -version = "1.3.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, ] [[package]] @@ -5914,15 +6104,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/90/bcce6b46823c9bec1757c964dc37ed332579be512e17a30e9698095dcae4/python_discovery-1.2.0.tar.gz", hash = "sha256:7d33e350704818b09e3da2bd419d37e21e7c30db6e0977bb438916e06b41b5b1", size = 58055, upload-time = "2026-03-19T01:43:08.248Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/3c/2005227cb951df502412de2fa781f800663cccbef8d90ec6f1b371ac2c0d/python_discovery-1.2.0-py3-none-any.whl", hash = "sha256:1e108f1bbe2ed0ef089823d28805d5ad32be8e734b86a5f212bf89b71c266e4a", size = 31524, upload-time = "2026-03-19T01:43:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" }, ] [[package]] @@ -5936,11 +6126,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.29" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" }, ] [[package]] @@ -5974,11 +6164,11 @@ wheels = [ [[package]] name = "pytz" -version = "2026.1.post1" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, ] [[package]] @@ -6097,7 +6287,7 @@ wheels = [ [[package]] name = "quack-kernels" -version = "0.2.5" +version = "0.3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "apache-tvm-ffi" }, @@ -6105,9 +6295,9 @@ dependencies = [ { name = "torch" }, { name = "torch-c-dlpack-ext" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/de/472a20a625495e31c33a99a30867c1d58335a1afa02dc30019f667702d1d/quack_kernels-0.2.5.tar.gz", hash = "sha256:06241a5962c09b4a2c27d4d21208e31790836fecde4373c6e9d874fdd88b5590", size = 152256, upload-time = "2026-01-31T09:07:09.998Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/11/6b1664d0e85f91f4549403d4ca6c9248857080f571397da7cb7570338dcd/quack_kernels-0.3.7.tar.gz", hash = "sha256:1c35a3f6f8c06b38cdf6a68d95fbb52e2b75cd261d0f01abcb7cec5d1bd80ca1", size = 193338, upload-time = "2026-03-27T19:55:55.544Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/7a/1a6d9997f979ce6985210a1783766b6c9b85bf6c21dcb990728526ca4d41/quack_kernels-0.2.5-py3-none-any.whl", hash = "sha256:5f7c246c8cb55c560f7601c952d60bddb4ba3e5c741220703a0c781a0aac3aa2", size = 156759, upload-time = "2026-01-31T09:07:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5f/892059ed4849db5ccddb83ae01ffa33adec607e5a483c4fe05576645a4b5/quack_kernels-0.3.7-py3-none-any.whl", hash = "sha256:5931707e24fe0b87139fadd53ecf5d7156e01d3fb8cbfe7e3f6a67b52dd83127", size = 199836, upload-time = "2026-03-27T19:55:54.387Z" }, ] [[package]] @@ -6141,95 +6331,95 @@ wheels = [ [[package]] name = "regex" -version = "2026.2.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, - { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, - { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, - { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, - { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +version = "2026.5.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" }, + { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" }, + { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" }, + { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" }, + { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" }, + { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" }, + { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" }, + { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" }, + { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" }, + { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" }, + { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" }, + { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" }, + { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" }, + { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" }, + { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" }, + { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" }, + { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" }, ] [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -6237,9 +6427,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -6269,29 +6459,29 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "rich-toolkit" -version = "0.19.7" +version = "0.19.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/02/32217f3657ae91a0ea7cf1d74ade78f44352f830d00c468f753ddb3d4980/rich_toolkit-0.19.10.tar.gz", hash = "sha256:dc2e8c515ef9fbb4894e62bd41a2d2960dd7c2f505b5084894604d5ccfee3f09", size = 198167, upload-time = "2026-05-21T10:11:42.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" }, + { url = "https://files.pythonhosted.org/packages/35/84/a005adcb4d1e6846ba3d62768090c3b943e3f6d8dc5c47af64f33584c4a7/rich_toolkit-0.19.10-py3-none-any.whl", hash = "sha256:93a41f67a09aefe90379f1729495c2fee9ccbcc8cfda48e2ca2ae54a995e32b1", size = 33907, upload-time = "2026-05-21T10:11:43.578Z" }, ] [[package]] @@ -6364,83 +6554,112 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +version = "2026.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, + { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, + { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, + { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, + { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, + { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, + { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, + { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, + { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, + { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, + { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, + { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, + { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, + { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, + { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, + { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, + { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, + { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6f/19c1918a4b590d8de87e712e4abe4b3875771eff60216fb6153cf6665c68/rpds_py-2026.5.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1f2c391c3059798093b65df23aca2cac150460ae9c630d99dec83d703d9485b9", size = 349756, upload-time = "2026-05-28T12:00:20.217Z" }, + { url = "https://files.pythonhosted.org/packages/e5/60/a06fe7da34eca79dacbf958a2ba0c6eea85bc2b29de20080bf40f72f66fa/rpds_py-2026.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:413b424f7c4ee65ab5e5be91f5731be0f8b41a1ee2b12dfe810d716312e95a78", size = 343831, upload-time = "2026-05-28T12:00:21.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ec/b2333b97b90e2a6ef6ca8ad386ee284968e74bcfe113b3f1a8d9036429a9/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c595a1d9255dce0599e13130d1440ab2506654f2b50294226ee06402f8fef63", size = 375127, upload-time = "2026-05-28T12:00:23.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/7f/e00aae54067f2b488c4637961d5f58204d470795fc791085fa3f15060d2e/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c27c5f6102eac8c03e7595a00827a53b271ba40a53b59ff8709170e0855ea4a", size = 379034, upload-time = "2026-05-28T12:00:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/be/cc/423999bbb8ae8dc93c77fc1d5e984ade5eb89d237d3bb884ccfa72ae2890/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c7fcf61d44cacecaf3aea542b0e053db77972a4573e7ceda16fb2b399161195", size = 490823, upload-time = "2026-05-28T12:00:26.676Z" }, + { url = "https://files.pythonhosted.org/packages/0f/aa/c671bf660f12e68d3c52ff86c7066ed1372df5a0f4f2ff584e419b8207e7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c817a189d4ee14290420e5ff051e4dd6baa13f3edf84685071dee07a6d538ee", size = 388144, upload-time = "2026-05-28T12:00:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/19/c8/d63bb75b68afe77b229e3021c6031bcaf01da5db5b0e69d0d10f9ba679a7/rpds_py-2026.5.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21846aac0ed2e0589f38c12dc44e77bb64e494b771eadbcf169cba00566ba7ba", size = 371959, upload-time = "2026-05-28T12:00:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/c51122014d8274ff37dc606d60049c3db7d83da02b5b282511e5a906a9a6/rpds_py-2026.5.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b317c87a13f769a4e787819bd508aaa5d69aa09b0880de9af6d3a8a54571cdec", size = 383558, upload-time = "2026-05-28T12:00:31.764Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f9/2790cb99c136a5363acdeacf5c27c56f3de0d4118a1f48fca83404c99c89/rpds_py-2026.5.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ce87129d9f2c14fa6c4a8601fb80eb4488c80d38a20cd13758ef11123e14995d", size = 402789, upload-time = "2026-05-28T12:00:33.247Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1b/e4fb584f8c75d35c38150ff6a332cda949e6f97acba1f4fd123b14ab56fe/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9cdddb6c1207d284d94fd1530adf57fbd797fe7c4b8704ba85f49414f2557e7d", size = 551405, upload-time = "2026-05-28T12:00:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f7/a6731b4216cb3793ea1af5391da240f5683dacc0d13e034fe5fc3503f240/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4e237e139f94d3c036fd28eb9f564c99055476ff4ff05cd42be55ce349b5aa02", size = 616975, upload-time = "2026-05-28T12:00:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/2e051a81d95d8e63f4b35a1c463a87e8766bc3d083c067c5dfb6bf220747/rpds_py-2026.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ed0954b524873214369184a9c82b0eaa45a3fbb9a798cd95b17e0d98499e7ea0", size = 578701, upload-time = "2026-05-28T12:00:37.82Z" }, + { url = "https://files.pythonhosted.org/packages/65/56/b5f6fdb2083e32bca8a8993d89e70db114b4756c9e2c38421328126689d2/rpds_py-2026.5.1-cp314-cp314-win32.whl", hash = "sha256:2d88621d6a7d4dfa633d21abe90f280bb205274e16b1d1e61c6ad4640b2453b7", size = 209806, upload-time = "2026-05-28T12:00:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/fb/80/65a5aa96c155e611d1ed844e4e1f57f3e36b021f396d9f8585d756e6b90d/rpds_py-2026.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:cef8ac28d26f4dda3533060c20fbf80a325458fa9fd23ea72a73cdfa8e978838", size = 225985, upload-time = "2026-05-28T12:00:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/27/7c/ad185212e87b05f196daef92bc5f3caf07298eb47c295b5585c3dd3093ac/rpds_py-2026.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:eaaea962c68cdc68d4a533ba985ab8e9484277910bbfaa2ab3ef7732667bfed8", size = 221219, upload-time = "2026-05-28T12:00:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/e14ae18759020334646b031e708ab4158d653a938822bfb7b95ef2e93aa3/rpds_py-2026.5.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:21942f52dbbd5f8758bf021213d28bd45c39e873e65e2407faf5f1846f5761ad", size = 352148, upload-time = "2026-05-28T12:00:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/31/9b/5f4a1e2f960bca3ac5d052b139dd31eed97b259f9d909173821760d542e8/rpds_py-2026.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f414556f6e3958300ff941e40c9f97e3dc9774ddd1b3434c475d73dd354bbed3", size = 345196, upload-time = "2026-05-28T12:00:46.14Z" }, + { url = "https://files.pythonhosted.org/packages/1a/71/1d9574d6a2fa20ab60eaa55c7467f5aa20cbc770f341a05f09c0876f59e2/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1013a8625c74043210190b246f5b1551e09757c1f356c6e4160ef96c5bc081", size = 374981, upload-time = "2026-05-28T12:00:47.531Z" }, + { url = "https://files.pythonhosted.org/packages/0c/9a/37e99f4915a80aa71670263c1267f7ae0af95f53a3f61e6c3bdc016d4515/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cc68e231a77a5f0d774ae278a1f8e55c0456501820847c1e4efb3829f3441df6", size = 379961, upload-time = "2026-05-28T12:00:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ff/6e73f74b89d2e0715e0fc86b7dde893f9a61ae2f9b256ff3bdfe41ac4e94/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9baffb505aff33acc69b422a19f77806680f3c8632227d79f48de8a810d1c2c5", size = 495965, upload-time = "2026-05-28T12:00:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e0/425faba25f59d74d4638b267f7c7a80e8649d2ef4db10a19b0c4a71e6e6f/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8d2f912928d426e8cfa396f7f3f8d29a59e6689c86dcca3c420730c1096322b", size = 389526, upload-time = "2026-05-28T12:00:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/c6/76/7a41960e3fddae47fab43a28684d5da981401dffd88253de0944148654cb/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90f628283be835db980c941767d41c9a27b5239e54ba0a9c1335247e82406964", size = 376190, upload-time = "2026-05-28T12:00:54.215Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/5f38dc70824fc6951b51d35377e577a3a3a4c81a6769cc5a2de25ebe0ad1/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:1ebb2f0ab7e16132995a72de805170e0203df0c3dd22e1ef1cd1fdd90bd7a131", size = 383921, upload-time = "2026-05-28T12:00:55.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/1a/d60a38caa1505f4b9483c3fbbde12c94e1079154f4f401a6da96f7e77621/rpds_py-2026.5.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3df3d16ded76f1f8c9cdebd0e1ea55fdf4c23b812de189814da7cf229c22a81", size = 404766, upload-time = "2026-05-28T12:00:57.518Z" }, + { url = "https://files.pythonhosted.org/packages/87/ff/602fd3f174d6425f0bce05ad0dfbec0e96b38d0f7d08a79af5aa20083885/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9af8905b8f854990e40d5206aa5ac58d9b0fe0b7f351ff2bb086c20f6c8c6a47", size = 551343, upload-time = "2026-05-28T12:00:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c1/1be13327acdbead3eca1fde03b6a34dbb011f1e864e217f0d32cc1779a7f/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:036a36a87fb1cd3b214d11c4b3c4f7d2ddad933625dca1c900b56a057c07740a", size = 618502, upload-time = "2026-05-28T12:01:00.656Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d7/afb49b49d7f2be8b7ba1a9f0977fa5168003437b93086726f066544e8351/rpds_py-2026.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ae3853454fe9ef283a03c96c2d835d39e84b14643a9d62c82ef0fb87d702ca", size = 581916, upload-time = "2026-05-28T12:01:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/dbef8c1f8a10f07beb62b5f054e20099fd9924b3ec001b8f0b6ac7813a85/rpds_py-2026.5.1-cp314-cp314t-win32.whl", hash = "sha256:6c3d771a46ec18b12af06ce36243a9a80b07a5d0515236332d90863ca8bb326a", size = 207855, upload-time = "2026-05-28T12:01:03.821Z" }, + { url = "https://files.pythonhosted.org/packages/2a/72/bfa4e61ab8e7dc1c8adf397e05e6cbdd4239357bd72b248d3de662f23915/rpds_py-2026.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c93c629be4636cf54337bd5f06c104d55e42ced54d681f6fe21ae510a65116f6", size = 225422, upload-time = "2026-05-28T12:01:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/7b5da92b640f67b6717ccafc83cdd06bfa7ff2395c3685c68922bb54d703/rpds_py-2026.5.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:3574b55c604b8f75dacb007136508bbc0db406e626301778096a133327e7f2fb", size = 349576, upload-time = "2026-05-28T12:01:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/2aafd7ad355a1bd48ca76e2262b74b15e6432b5a1efe150efd4d779cd55d/rpds_py-2026.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:94068eb3ae6d43f5a786b7db96a406a34e6d5c24489feef32fd6e8946ea7b291", size = 343640, upload-time = "2026-05-28T12:01:08.441Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7d/6c9523c1abbe840a1b7fba3c516d48e1d3487cc80fea4366c4071cf56784/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a5b10e8ce894825f380a8f1b6444cf73c294dfea62afbb2d13e3a9e630cec1", size = 375322, upload-time = "2026-05-28T12:01:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5d/0b7b03fb1dc509321f01de3149784ab773e34c8573022029af8076afcb9c/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc09f82e63d4bcd58149572f857a431bae851dc747e313c3b5bdf7abb907fda8", size = 379066, upload-time = "2026-05-28T12:01:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e2/8ef6012999ebf1cb1c22f876d9ce5e63d960fd4631d2af3202d3f480aa25/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e10464d17df3b582745c25cec695cb9558bca2cb6ddb631aee1787fc72c767b2", size = 494586, upload-time = "2026-05-28T12:01:13.051Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/1eeb029bec67582c226b7809172207cd005073af4ebd906e65ff494f4983/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba05adbf15d994c38ec0b7ab32e858e5110c21e9009a00a86545fd220f84e038", size = 388415, upload-time = "2026-05-28T12:01:14.631Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/ffbe10711c4d766c1cab0557d6906c074f795814863c67b351355d29354a/rpds_py-2026.5.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77c004fdc7b891967106f78ddfd7b076bfe6813c6139c6fff6aed3bcaa960b26", size = 372427, upload-time = "2026-05-28T12:01:16.153Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3a/30ba4a6ad457e5b070c18d742a33fb77d8d922b565cc881f8a5313d63bfe/rpds_py-2026.5.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:83bcf894486c9d78dd290d3c0124ff6dd8875d3025e2090a8ec49fcc37c55fdd", size = 383615, upload-time = "2026-05-28T12:01:17.809Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/62e242b53ce39c0814bd24e1a6e6eba6c92be716277745f317f9540a2e7b/rpds_py-2026.5.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3df104083952a0e0c6f10de33e440eabe98fb6317d23e1a58c68f6df08d01b9", size = 402786, upload-time = "2026-05-28T12:01:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/38/c1/a770b9c186928a1ed0f7e6d7ae50e7f3950ed23e3f9e366dbc8e38cb55de/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:980450826cf22e133c57e0835070bdd0dd3f73b9b708c3ce223def2cb9469e14", size = 551583, upload-time = "2026-05-28T12:01:21.013Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/68e8579b95375b70d2a963103c42e705856cdb98569258bd807f4423891c/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:205dde846f24332ab0c1188699a043b8d165b79bb84529ce272c45048ff6be01", size = 616941, upload-time = "2026-05-28T12:01:22.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/a1/a6135aed5730ff03ab957182259987ac11e55fb392a28dc6f0592048a280/rpds_py-2026.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3966b82dd563176396df030f3dd52a6e54cb69b718e95e78bd555ed3d1e0185d", size = 578349, upload-time = "2026-05-28T12:01:24.118Z" }, + { url = "https://files.pythonhosted.org/packages/09/6e/f24201a76a84e6c49d0bdfdfcb735210e21701e9b21c5bfc0ba497dd62f6/rpds_py-2026.5.1-cp315-cp315-win32.whl", hash = "sha256:7818f8d0a415be74d2be3590b0a1c1f463a642f4d0217e7d10602dceef5b79aa", size = 209922, upload-time = "2026-05-28T12:01:25.522Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e4/966bc240bb0485fc265278f6de44d05834bf0b3618886e0b22e33d54c49a/rpds_py-2026.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:b3cc20c0d800af78fd0fac68086e28c1856cec51ea528bb81ea851aa40d39325", size = 226003, upload-time = "2026-05-28T12:01:27.062Z" }, + { url = "https://files.pythonhosted.org/packages/5c/5c/a15a59269cd5e74472734516c73795c15eccfc841b3d4b0228c3f53f19d0/rpds_py-2026.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:3609e9939a8a76cd904cf98a3f1f13b5dc7e150adeaee89e0ea09652ea213e16", size = 221245, upload-time = "2026-05-28T12:01:28.51Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/135ce03804e179a71ceb13be095deda4a279bc88f7a6b8fa161c5ad44e12/rpds_py-2026.5.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:5d333a7127d4b307601ac37792bee01bb95c867cbfacf21b6375b804d6bbd723", size = 352015, upload-time = "2026-05-28T12:01:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/f1f6d2652eb9d848f6eb369d8db83a2da6249bb49ad2c2a48f45d54538d3/rpds_py-2026.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:b5f077b44a4f7808520f66dae234988d867deb9aed9be5da057ce9ba831b2a41", size = 345016, upload-time = "2026-05-28T12:01:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/88/66/b74182775691ea2290c99e52ac8d5db844e56fbec90ce421f107658c8314/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d8f9b7b78c9538fc9e04e82ec0e888ff0c3cffcfad152c77e57cd09351a98a", size = 374775, upload-time = "2026-05-28T12:01:33.136Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8f/15e5a61d9f0a43902d36561d4f07cae6ae9f4716be825159fd72717f33af/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e3a8ae58895ac107ed934a6bf51e5846f95c53b9b940c2c6d310838fd5846358", size = 380270, upload-time = "2026-05-28T12:01:34.574Z" }, + { url = "https://files.pythonhosted.org/packages/02/c3/f859b12763a80540cdf2af0f15b19904cf756a71d7bdd3f82ff3e5b1bbf9/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0957cf3c2b8632ec7aaebffebea8005b353cc2a237b6e2ae3c2cac0820704cfb", size = 495285, upload-time = "2026-05-28T12:01:36.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/ff27c2ac8411d30b03b1829fd88cae8dad1a4d0da48dd25e57c4038042e6/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c396c1304de421050b3681ea70f371874b54d41b0151e96109758144c231e30b", size = 389581, upload-time = "2026-05-28T12:01:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/6e/67/fe92ee32a6cc05c77228a2f8b1762e7124f386ec20ff83d0757b762d58d0/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad1bff7f666b9598e573815affd666aac6a13a585dde336f843e33350c7fadc", size = 376041, upload-time = "2026-05-28T12:01:39.307Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/b4d6685c27aba55bd82f25b278be8237038117d05f9659a6213ad3408130/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:656a042550878f12d45752452d47094b7cfe5ad1e9d7b87b5a22ad3ae5ff8015", size = 383946, upload-time = "2026-05-28T12:01:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/bd/79/2c1d832a53c8e0f8e98fc970ec257b950fecd4f62be2ab7182b500a0cbc8/rpds_py-2026.5.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c4bd4f70294737b5206a3e8e30ccadbf8a60301831c8ea23eec5dbeea1ecfa", size = 405526, upload-time = "2026-05-28T12:01:43.032Z" }, + { url = "https://files.pythonhosted.org/packages/78/c4/c98117b03c6a8581ab2c2dfccfe9a5ad82bd8128a3c28b46a6ad2d97c393/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:43bca78665423cabae77146f2fe7ce55272b6c8d55d82cca83effd42c7e13972", size = 551165, upload-time = "2026-05-28T12:01:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/bc479ca069200af730881b1bd525e3114b2b391a351509fcb1b772f28086/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:42d0f20e85e549c870749d0e247f0c10d318a45b7e9676d575d2dcb04a1b2e66", size = 618778, upload-time = "2026-05-28T12:01:46.337Z" }, + { url = "https://files.pythonhosted.org/packages/77/65/38ab2f90df44c2febfb63cc10ced40763d9b4bc94d173e734528663fe7f5/rpds_py-2026.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b1be5c35683684d5331b93600c210e8367c254683d8a6df6bd21bd2da3a334fb", size = 581839, upload-time = "2026-05-28T12:01:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/ce1f605fe036aadd460e5822e578c6c7ec3a860936cca37d6e0f299daa77/rpds_py-2026.5.1-cp315-cp315t-win32.whl", hash = "sha256:75808f6c38ce7749bb68cc2770161aae5045e6c6f6781a9782e74b93304399df", size = 207866, upload-time = "2026-05-28T12:01:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" }, ] [[package]] @@ -6466,32 +6685,32 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, - { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, - { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, - { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, - { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, - { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, - { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, - { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, +version = "0.15.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, + { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, + { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, + { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, ] [[package]] name = "runpod" -version = "1.8.1" +version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp", extra = ["speedups"] }, @@ -6506,6 +6725,7 @@ dependencies = [ { name = "inquirerpy" }, { name = "paramiko" }, { name = "prettytable" }, + { name = "psutil" }, { name = "py-cpuinfo" }, { name = "requests" }, { name = "tomli" }, @@ -6514,21 +6734,21 @@ dependencies = [ { name = "urllib3" }, { name = "watchdog" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/d4/79710eb65047e4a43c9639e4633c3a5333bf36cc33cb4d15f18518872157/runpod-1.8.1.tar.gz", hash = "sha256:3d31aa0e175a91e7c962fe1de51e2872a06e78f5d7e37ff7520ba23db1485413", size = 284691, upload-time = "2025-11-19T22:54:08.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/ff/03855d2b9de4466719f8e5ddebf27aba6812dd4337be34dbf9d67ffe87b6/runpod-1.9.0.tar.gz", hash = "sha256:77ba9b45c4b926d88f80988e804d3866b66979f33d07605d616e3f5539028764", size = 570322, upload-time = "2026-04-09T00:39:55.301Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5c/ce583cfbba69f4f989658c7e984b1175d4e1f5f19132d9554a5ff7031647/runpod-1.8.1-py3-none-any.whl", hash = "sha256:2cc36ce80c02b7b6f54216154345e5064bfa510718acfc684cd9f56ac506d518", size = 157526, upload-time = "2025-11-19T22:54:06.968Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f9/cecaedaef9124e05d3b0e0c16ea717f2eae1df95297fa874d32c937087d5/runpod-1.9.0-py3-none-any.whl", hash = "sha256:941b4a54611f9ad7b71609d0d4d10aa735422305f4c8a1574b79e8fe8e27b0f9", size = 328012, upload-time = "2026-04-09T00:39:53.552Z" }, ] [[package]] name = "s3transfer" -version = "0.16.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/1f/12417f7f493fc45e1f9fd5d4a9b6c125cf8d2cf3f8ddbdfab3e76406e9d6/s3transfer-0.18.0.tar.gz", hash = "sha256:3760b8b7ec1315da54048b2d626276732bee4300d054d492d4e1d43e20d4ecbd", size = 160560, upload-time = "2026-05-28T19:39:09.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a58fc997655386daa2e25784e30c288aa3e3819e401f77029ee4899fb55a/s3transfer-0.18.0-py3-none-any.whl", hash = "sha256:239c13b09e65ad0346e1be7348b8a202dcad44ac7ea7c6eb858fc881dce739b6", size = 88572, upload-time = "2026-05-28T19:39:07.999Z" }, ] [[package]] @@ -6744,15 +6964,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.55.0" +version = "2.61.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/b8/285293dc60fc198fffc3fcdbc7c6d4e646e0f74e61461c355d40faa64ceb/sentry_sdk-2.55.0.tar.gz", hash = "sha256:3774c4d8820720ca4101548131b9c162f4c9426eb7f4d24aca453012a7470f69", size = 424505, upload-time = "2026-03-17T14:15:51.707Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/4d/3c66e6045bd2071256b6b6fdcb0cc02b86ce54b2acc2ceac79af8e0efbb5/sentry_sdk-2.61.0.tar.gz", hash = "sha256:1ca9b4bb777eb5be67004edab7eb894f21c6301f1d05ed64966719ad5d1764ce", size = 458510, upload-time = "2026-05-28T09:40:28.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/66/20465097782d7e1e742d846407ea7262d338c6e876ddddad38ca8907b38f/sentry_sdk-2.55.0-py2.py3-none-any.whl", hash = "sha256:97026981cb15699394474a196b88503a393cbc58d182ece0d3abe12b9bd978d4", size = 449284, upload-time = "2026-03-17T14:15:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/5a/9794736d5802689c1a48862e6afe6b7f3e86cc37c15d4a84bc0143877dc1/sentry_sdk-2.61.0-py3-none-any.whl", hash = "sha256:ec4d30273909cb1d198e03208b16ee70e2bc5d90a16fd9f1fb2fc6a72e1f03dc", size = 483111, upload-time = "2026-05-28T09:40:27.027Z" }, ] [[package]] @@ -6815,11 +7035,11 @@ wheels = [ [[package]] name = "setuptools" -version = "79.0.1" +version = "81.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/71/b6365e6325b3290e14957b2c3a804a529968c77a049b2ed40c095f749707/setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88", size = 1367909, upload-time = "2025-04-23T22:20:59.241Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/6d/b4752b044bf94cb802d88a888dc7d288baaf77d7910b7dedda74b5ceea0c/setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51", size = 1256281, upload-time = "2025-04-23T22:20:56.768Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] @@ -6904,7 +7124,7 @@ wheels = [ [[package]] name = "skops" -version = "0.13.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -6913,9 +7133,9 @@ dependencies = [ { name = "scikit-learn" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/0c/5ec987633e077dd0076178ea6ade2d6e57780b34afea0b497fb507d7a1ed/skops-0.13.0.tar.gz", hash = "sha256:66949fd3c95cbb5c80270fbe40293c0fe1e46cb4a921860e42584dd9c20ebeb1", size = 581312, upload-time = "2025-08-06T09:48:14.916Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/9f/46448c4e41a4c5ee4bdb74b3758af48e5ff0faeffe40f4e301bfc7594894/skops-0.14.0.tar.gz", hash = "sha256:6c8c0e047f691a3a582c3258943eecafcbfd79c8c7eef66260f3703e363254f0", size = 608084, upload-time = "2026-04-20T18:23:55.336Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/e8/6a2b2030f0689f894432b9c2f0357f2f3286b2a00474827e04b8fe9eea13/skops-0.13.0-py3-none-any.whl", hash = "sha256:55e2cccb18c86f5916e4cfe5acf55ed7b0eecddf08a151906414c092fa5926dc", size = 131200, upload-time = "2025-08-06T09:48:13.356Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0e/3ae19fa941522cd98e119762e7181d371c8dba0b2d72bfaf9522692e329c/skops-0.14.0-py3-none-any.whl", hash = "sha256:60a5db78a9db46ccee2139a0ba13ab5afb1c96f4749b382e75a371291bbe3e36", size = 132198, upload-time = "2026-04-20T18:23:54.018Z" }, ] [[package]] @@ -7115,48 +7335,43 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.48" +version = "2.0.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/b0/a9d19b43f38f878b1278bca5b00b909f7540d41494396dd2561f9ad0956d/sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb", size = 2159807, upload-time = "2026-05-24T19:27:53.086Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/191dd58a248fd2cfd4780fa82c375c505e4ad98c8b522fa69ec492130d77/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89", size = 3343358, upload-time = "2026-05-24T20:09:29.279Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600", size = 3357994, upload-time = "2026-05-24T20:17:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/35/a6/a0e283f5494f92b0d77e319ff77e437b1ffe4a051ba67c81d53234825475/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e", size = 3289399, upload-time = "2026-05-24T20:09:32.239Z" }, + { url = "https://files.pythonhosted.org/packages/b7/96/1b07325ba71752d6a028b77d07bed1483ad545f794e8b1dc89b3ba3b3c68/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615", size = 3321216, upload-time = "2026-05-24T20:17:15.581Z" }, + { url = "https://files.pythonhosted.org/packages/ed/8e/bad6ed253e8a99edfc99af02f7173ec48a1d3ed1b9b35a1b8bc1700900cc/sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a", size = 2119194, upload-time = "2026-05-24T19:50:04.943Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/314a6690dda4b9cfc571eab1a63cf6fe6e1470aa3759ccda6aa016ee0f5a/sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7", size = 2146186, upload-time = "2026-05-24T19:50:06.74Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" }, + { url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" }, + { url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" }, + { url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" }, + { url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" }, + { url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" }, + { url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" }, ] [[package]] @@ -7297,54 +7512,75 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/e5/5f3cb2159769d0f4324c0e9e87f9de3c4b1cd45848a96b2eb3566ad5ca77/tiktoken-0.13.0.tar.gz", hash = "sha256:c9435714c3a84c2319499de9a300c0e604449dd0799ff246458b3bb6a7f433c1", size = 38986, upload-time = "2026-05-15T04:51:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/8e/144bde4e01df66b34bb865557c7cd754ed08b036217ebd79c9db5e9048a9/tiktoken-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:32ac870a806cfb260a02d0cb70426aef02e038297f8ad50df5040bb5af360791", size = 1034888, upload-time = "2026-05-15T04:50:31.579Z" }, + { url = "https://files.pythonhosted.org/packages/36/18/d4ac9d20956cdebca04841316660ed584c2fecdc2b81722a28bc7ad3b1e4/tiktoken-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d9980f11429ed2d737c463bb1fb78cf330caa026adf002f714aced7849a687b", size = 982970, upload-time = "2026-05-15T04:50:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/ed/6bb8d05b9f731f749fee5c6f5ca63e981143c826a5985877330507bd13b7/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3f277ebea5edd7b8bf03c6f9431e1d67d517530115572b2dc1d465326e8f88c7", size = 1115741, upload-time = "2026-05-15T04:50:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/34/de/2ca96b07a82d972b74fe4b46de055b79c904e45c7eab699354a0bfa697dc/tiktoken-0.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a116178fa7e1b4065bff05214360373a65cac22f965be7b3f73d00a0dbfe7649", size = 1136523, upload-time = "2026-05-15T04:50:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/9dafec002c2d4424378563cf4cf5c7fb93631d2a55013c8b87554ee4012c/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c397ddda233208345b01bd30f2fca79ff730e55731d0108a603f9bc57f6af3b", size = 1181954, upload-time = "2026-05-15T04:50:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d0/1f8578c45b2f24759b46f0b50d31878c63c73e6bf0f2227e10ec5c5408dc/tiktoken-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95097e4f89b06403976e498abf61a0ee73a7497e73fb599cb211d8197a054d91", size = 1240069, upload-time = "2026-05-15T04:50:38.221Z" }, + { url = "https://files.pythonhosted.org/packages/aa/90/28d7f154888610aa9237e541986beb62b479df29d193a5a0617dbb1514d0/tiktoken-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8f2d16e7a7c783ad81f36e457d046d1f1c8af70b22aec8a13238efe531977c41", size = 874748, upload-time = "2026-05-15T04:50:39.587Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/b096c859c2a47c11731bf2f5885f4028b809dfe2396582883eed9cae372f/tiktoken-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5df5d1507bd245f1ccad4a074698240021239e455eb0bb4ced4e3d7181872154", size = 1034228, upload-time = "2026-05-15T04:50:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/53/61/c68e123b6d753e3fc2751e9b18e732c9d8bf1e1926762e736eee935d931c/tiktoken-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fe806a50664e83a6ffd56cbd1e4f5dcc6cd32a3e7538f70dc38b1a271384545", size = 982978, upload-time = "2026-05-15T04:50:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8b/96cc178cc584e65d363134500f297790b06cd48cdeb1e8fcf7bbe60f4715/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:125bc05005e747f993a83dc67934249932d6e4209854452cd4c0b1d53fba3ba2", size = 1116355, upload-time = "2026-05-15T04:50:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/bab735d2c72ea55404b295d02d092644eb5f7cc6205e34d35eb9abfb9ab2/tiktoken-0.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5e6358911cab4adee6712da27d65573496a4f68cf8a2b5fca6a4ad10fc5748cf", size = 1135772, upload-time = "2026-05-15T04:50:44.782Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/6de04ebdf904edfaad87788011b3735087a0c9ea671b9027e1e4e965e8c8/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:975cbd78d085d75d26b59660e262736dcaed1e35f8f142cd6291025c01d25486", size = 1182415, upload-time = "2026-05-15T04:50:46.422Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/470a05f3b1caf038f44880e334d47ab674e0c80d514c66b375d14d5afa10/tiktoken-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ab9bc99fa020a4c283424590ecd7f3afd70c1c281cb3fa3192a6c3af9f9615", size = 1239879, upload-time = "2026-05-15T04:50:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/42/a6/c1936d16055436cb32e6c6128d68629622e00f4768562f55653752d34768/tiktoken-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:6b1615f0ff71953d19729ceb18865429c185b0a23c5353f1bbca34a394bf60f7", size = 874829, upload-time = "2026-05-15T04:50:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/acb5992c3772b5a36284f742cfb7a5895aa4471d1848ac31464ad50d7fdf/tiktoken-0.13.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6eb4a5bfbc6426938026b1a334e898ac53541360d62d8c689870160cc80abd67", size = 1033600, upload-time = "2026-05-15T04:50:50.4Z" }, + { url = "https://files.pythonhosted.org/packages/14/e9/742e9aec30f59b9f161f7ff7cd072e02ea836c9e1c0854a8076dfcd40d5c/tiktoken-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:43cee3e5400573b2046fbf092cc7a5bc30164f9e4c95ce20714da929df48737a", size = 982516, upload-time = "2026-05-15T04:50:52.03Z" }, + { url = "https://files.pythonhosted.org/packages/72/74/ca1541b053e7648254d2e4b42a253e1bb4359f2c91a0a8d49228c794e1a0/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7de52e3f566d19b3b11bd37eea552c6c305ad74081f736882bd44d148ed4c48d", size = 1115518, upload-time = "2026-05-15T04:50:53.543Z" }, + { url = "https://files.pythonhosted.org/packages/46/e3/93825eaf5a4a504795b787e5d5dea07fbeb3dabf97aa7b450be8bde59c89/tiktoken-0.13.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:51384448aa508e4df84c0f7c1dc3211c7f7b8096325660ee5fc82f3e11b381ce", size = 1136867, upload-time = "2026-05-15T04:50:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/8c/46/002b68de6827091d5ae90b048f326e8aad8d953520950e5ce1508879414f/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e28157350f7ebf35008dd8e9e0fdb621f976e4230c881099c85e8cf07eaa50e2", size = 1181826, upload-time = "2026-05-15T04:50:56.296Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/d393e3185a276505182f7abd93fe714f3c444a2be9180798fa052347504e/tiktoken-0.13.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:165cf1820ea4a354985c2490a5205d4cc74661c934aca79dd0368232fff94e0f", size = 1239489, upload-time = "2026-05-15T04:50:57.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/bc07d1f1635d4897a202acc0ae11c2886eaa7325c359ba4741b47bf8e225/tiktoken-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6c43a675ca14f6f2749ba7f12075d37456015a24b859f2517b9beb4ef30807ec", size = 873820, upload-time = "2026-05-15T04:50:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/8c/93/0dd6adca026a616c3a92974566b43381eea4b475ce1f36c062b8271a9ac5/tiktoken-0.13.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaaaef47c2406277181d2086484c317bf7fc433e2d5d03ff94f56b0dcec87471", size = 1034977, upload-time = "2026-05-15T04:51:00.957Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5ec6e6bc5b30bed6d93f7f2162d8f6b32437b3ba27cb527cfe004f6109c9/tiktoken-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca8b310bd93b3772cb1b7922d915446864860f562bdfe4825c63a0aed3fb28cd", size = 983635, upload-time = "2026-05-15T04:51:02.629Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/c8ae9aff00d625c50659b4513e707a0462c4bf5d4d6cc1b802103225c02e/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32e0c12305105002c047b3bb1070b0dd9a73b0cb3b2856a8972b810e7a4f5881", size = 1116036, upload-time = "2026-05-15T04:51:04.082Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ac/6a5dddd1d0a6018ecb389bd0353e6b4a515eb4d2286611bd0ace1937b9e1/tiktoken-0.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ba5fd62507a932d1241346179e3b39bc7bf7408f03c272652d93b3bedf5db24", size = 1135544, upload-time = "2026-05-15T04:51:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b8/585032b4384b2f7dcdaddcb52865c83a701a420d09e3c2b4a2be1c450c57/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d108bc2d470fc53c8ecd24f2c0fd2b5f98c33e87cdb6aa2e9b8c5dced703d273", size = 1182217, upload-time = "2026-05-15T04:51:06.517Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b6/993ff1ded3958215fd341a847b8e5ffeb5de473f435296870d314fc91ac4/tiktoken-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb99cb5127449f58d0a2d5f5ccfb390d8dbdfd919c221246caaee29d8725ed51", size = 1239404, upload-time = "2026-05-15T04:51:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3d/fef7e06e3b33e7538db0ced734cf9fe23b6832d2ac4990c119c377aec55e/tiktoken-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:115c4f26ffa11caac8b54eea35c2ad38c612c20a48d35dd15d70a02ac6f51f58", size = 918686, upload-time = "2026-05-15T04:51:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c1/82/a7fc44582bc32ab00de988a2299bf77c077f59068b233109e34b7d6ca7e6/tiktoken-0.13.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:472527e9132952f2fbf77cd290658bacf003d4d5a3fabc18e5fbd407cbae4d9b", size = 1034454, upload-time = "2026-05-15T04:51:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/37/d0/24d8a890c14f432a05cea669c17bebeaa99f96a7c79523b590f564246411/tiktoken-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e2f67d27c9626cdd25fe33d9313c5cdb3d8d82da646b68d6eb8e7e9c20e6448", size = 982976, upload-time = "2026-05-15T04:51:11.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/2ab43f62788a9266187a9bfc1d3af99ad83e5eaa25fbef168a69cd5ad14f/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2b920b35805cd64585a37c3dc7ce65fba4d2d36016be01e1d7942482ca29093a", size = 1115526, upload-time = "2026-05-15T04:51:12.608Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/1494321ed323ce7a14d88e3cd6cb9058625977df1c6961ddc492bd10a9f3/tiktoken-0.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:493af3aa28a4aaf2e3d2600a2ee717252c9bf5ab38fff94eb5a02db5ab77e5ad", size = 1136466, upload-time = "2026-05-15T04:51:13.926Z" }, + { url = "https://files.pythonhosted.org/packages/96/d9/dfd086aa2d918c563a140720e0ce296cada1634efd2783d5cf51e05f984e/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6644c9c2b5cf3916f5a3641d7d12fdb3f006a7b3d9ff6acdaec44e29ab1ff91e", size = 1181863, upload-time = "2026-05-15T04:51:15.025Z" }, + { url = "https://files.pythonhosted.org/packages/2f/68/a18b4f307086954fdae32714cb4f85562e34f9d34ab206e61f1816aa6018/tiktoken-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cb65b60b9408563676d874a3a4ee573370066f0dc4e29d84e82e989c6517424", size = 1239218, upload-time = "2026-05-15T04:51:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/16/5b/f2aa703a4fc5d2dff73460a7d46cc2f3f44aa0f3dd8eeb20d2a0ecf68862/tiktoken-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:85b78cc3a2c3d48723ca751fa981f1fedccd54194ca0471b957364353a898b07", size = 918110, upload-time = "2026-05-15T04:51:17.237Z" }, +] + +[[package]] +name = "tilelang" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "apache-tvm-ffi", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "cloudpickle", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "ml-dtypes", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "numpy", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "psutil", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "torch", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "torch-c-dlpack-ext", marker = "python_full_version < '3.14' and platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "tqdm", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "z3-solver", marker = "platform_machine != 's390x' and sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/5c/07146b4527656102e48d21c2599aa80477e83ea3f149ac0df3b15a247bd4/tilelang-0.1.10.tar.gz", hash = "sha256:d8813e668fcf75843bc2d68c633c352b419c1e292895a6038a4aadd943e56c2b", size = 93184128, upload-time = "2026-05-25T03:58:57.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/66/ab4301dc38ca9f09832df2936c73388c611c198dc938634acb6ce80dfa74/tilelang-0.1.10-cp38-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85180d1a96defeecdf52d5d075a31c3fc551d8485981e6b636762a9cd7eb02fe", size = 49768455, upload-time = "2026-05-25T03:56:17.081Z" }, ] [[package]] name = "timm" -version = "1.0.26" +version = "1.0.27" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -7353,9 +7589,9 @@ dependencies = [ { name = "torch" }, { name = "torchvision" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/1e/e924b3b2326a856aaf68586f9c52a5fc81ef45715eca408393b68c597e0e/timm-1.0.26.tar.gz", hash = "sha256:f66f082f2f381cf68431c22714c8b70f723837fa2a185b155961eab90f2d5b10", size = 2419859, upload-time = "2026-03-23T18:12:10.272Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/54/ece85b0eef3700c90db8271a43669b05a0ebbe2edb1962329c34374a297e/timm-1.0.27.tar.gz", hash = "sha256:315dfe63186ca9fb7ff941268941231fd5be259f2b4bb4afa28560ae1015cb9a", size = 2439861, upload-time = "2026-05-08T19:38:36.844Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/e9/bebf3d50e3fc847378988235f87c37ad3ac26d386041ab915d15e92025cd/timm-1.0.26-py3-none-any.whl", hash = "sha256:985c330de5ccc3a2aa0224eb7272e6a336084702390bb7e3801f3c91603d3683", size = 2568766, upload-time = "2026-03-23T18:12:08.062Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2e/26bab7686ff4aed48f8f5f6c23e2aa37b7a37ddd9effe3aa61e908fd518f/timm-1.0.27-py3-none-any.whl", hash = "sha256:5ff07c9ddf53cbada88eab1c93ff175c64cab683b5a2fddf863bcee985926f89", size = 2589280, upload-time = "2026-05-08T19:38:35.034Z" }, ] [[package]] @@ -7439,47 +7675,47 @@ wheels = [ [[package]] name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] @@ -7493,71 +7729,49 @@ wheels = [ [[package]] name = "tomlkit" -version = "0.14.0" +version = "0.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/db/03eaf4331631ef6b27d6e3c9b68c54dc6f0d63d87201fed600cc409307fd/tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3", size = 161875, upload-time = "2026-05-10T07:38:22.245Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, + { url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" }, ] [[package]] name = "torch" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } +version = "2.11.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } dependencies = [ - { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, { name = "filelock" }, { name = "fsspec" }, { name = "jinja2" }, { name = "networkx" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "sys_platform == 'linux'" }, { name = "setuptools" }, { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, - { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/abada41517ce0011775f0f4eacc79659bc9bc6c361e6bfe6f7052a6b9363/torch-2.10.0-3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:98c01b8bb5e3240426dcde1446eed6f40c778091c8544767ef1168fc663a05a6", size = 915622781, upload-time = "2026-03-11T14:17:11.354Z" }, - { url = "https://files.pythonhosted.org/packages/ab/c6/4dfe238342ffdcec5aef1c96c457548762d33c40b45a1ab7033bb26d2ff2/torch-2.10.0-3-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:80b1b5bfe38eb0e9f5ff09f206dcac0a87aadd084230d4a36eea5ec5232c115b", size = 915627275, upload-time = "2026-03-11T14:16:11.325Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f0/72bf18847f58f877a6a8acf60614b14935e2f156d942483af1ffc081aea0/torch-2.10.0-3-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:46b3574d93a2a8134b3f5475cfb98e2eb46771794c57015f6ad1fb795ec25e49", size = 915523474, upload-time = "2026-03-11T14:17:44.422Z" }, - { url = "https://files.pythonhosted.org/packages/f4/39/590742415c3030551944edc2ddc273ea1fdfe8ffb2780992e824f1ebee98/torch-2.10.0-3-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b1d5e2aba4eb7f8e87fbe04f86442887f9167a35f092afe4c237dfcaaef6e328", size = 915632474, upload-time = "2026-03-11T14:15:13.666Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8e/34949484f764dde5b222b7fe3fede43e4a6f0da9d7f8c370bb617d629ee2/torch-2.10.0-3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0228d20b06701c05a8f978357f657817a4a63984b0c90745def81c18aedfa591", size = 915523882, upload-time = "2026-03-11T14:14:46.311Z" }, - { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, - { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, - { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, - { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, - { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, - { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, - { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, - { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, - { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, - { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, - { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, - { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, - { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9c8f38efee365cb9d334de8a83ce52fc7e5fc9e5a7b0853285efa1b69e00b0f2", upload-time = "2026-04-27T17:41:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d252cf975fb18c94a85336323ad425f473df56dab35a44b00399bd70c7a3b997", upload-time = "2026-04-27T17:42:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:7c78215c3af4f62e63f2b2e360f1722fc719b0853c7ac22666483d9810613a4c", upload-time = "2026-04-27T17:43:49Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7db3580106bba044da5b8950f3fb8fe5f31999eaab3f6a3aa2ac5d202c3684d2", upload-time = "2026-04-27T17:45:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:db964b33c55035a72ab3e2162287af8f1cc276039c65d015740cc88c26dcedf7", upload-time = "2026-04-27T17:46:18Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:6f367e62fd81b75cdf23ca4b75ced834d2db2cf98d1588ac935bde345de9de23", upload-time = "2026-04-27T17:48:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd1cf1005c5fe419194ee294b7b584ba5ad0f2fb1778b3fe5a7b9c3f4617ddbc", upload-time = "2026-04-27T17:50:01Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:74b628dbc71603977b09f4e140792c6e997081a35ef3421555f3f6e201b81210", upload-time = "2026-04-27T17:50:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:c2a5984deba8e001d166bf9cb83b8351f63a28b009e1a2fa0e4bbf08c90b259b", upload-time = "2026-04-27T17:52:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:baa52f7b8a53cab16587b10f1c27d1000ca033f97236878b685b75d5a1b92408", upload-time = "2026-04-27T17:54:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d389a850677f0d24dafae1573644034428d8d3b9c80b51d55ba62fed7e6c8777", upload-time = "2026-04-27T17:55:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-win_amd64.whl", hash = "sha256:d6c21797ff75271b4fbdd905e2d703be4ecea5ea5bbdde4d1c201e9c71bc411d", upload-time = "2026-04-27T17:56:46Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:06849e9311dbb0617c97557d9c26c99a9e1c4f2ac9cb8e9b6d9b420d522acb91", upload-time = "2026-04-27T17:58:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:169a9987e1f84f0c5eee07544b3a34827a163ac9180e23abf0c3548f1335762c", upload-time = "2026-04-27T17:59:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-win_amd64.whl", hash = "sha256:d86c125d720c2c368c53bd1a4ef062916d91fa965c10448c74c78b5d039faf2d", upload-time = "2026-04-27T18:01:14Z" }, ] [[package]] @@ -7594,7 +7808,7 @@ wheels = [ [[package]] name = "torchvision" -version = "0.25.0" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -7602,43 +7816,43 @@ dependencies = [ { name = "torch" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, - { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, - { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, - { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, - { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, - { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, - { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, - { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, - { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, - { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, - { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, - { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" }, + { url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0c/722e989f9cf026e97ef7cb24a9bb1859e099f72d247ae35388fb89729f73/torchvision-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c037709072ca9b19750c0cbe9e8bb6f91c9a1be1befa26df33e281deccbd8c7", size = 4021073, upload-time = "2026-05-13T14:57:00.848Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ae/36547812e6e047c1d80bcacd1b17a340612b08a6e876e0aabf3d0b9228b0/torchvision-0.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:41d6dae73e1af09fa82ded597ae57f2a2314285acde54b25890a8f8e51b999d7", size = 1758826, upload-time = "2026-05-13T14:57:05.262Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/32c4ea842738728a14e3df8c576c62dedcf5ae5cb6a5c984c6429ebe7524/torchvision-0.27.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:70f071c6f74b60d5fe8851636d8d4cd5f4fa29d57fd9348a87a6f17b990b95ba", size = 7789501, upload-time = "2026-05-13T14:56:57.786Z" }, + { url = "https://files.pythonhosted.org/packages/f6/24/4d0d48684251bd0673f87d633d5d88ab00227983b00591156eed2f86c8d5/torchvision-0.27.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aaafa6962c9d91f42503de1957d6fa349907d028c06f335bd95da7a5bc57147d", size = 7579868, upload-time = "2026-05-13T14:56:41.618Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/e6edd051d2ba25adf23b120fa97f458dff888d098c51e84724f17d2d1470/torchvision-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:aee384a2782c89517c4ab9061d2720ba59fd2ffe5ef89d0a149cc2d43abdf521", size = 4092700, upload-time = "2026-05-13T14:57:09.729Z" }, + { url = "https://files.pythonhosted.org/packages/fa/23/95dfa40431360f42ca949bf861434bed51164adfa8fb9801e05bf3194f50/torchvision-0.27.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:c5121f1b9ab09a7f73e837871deb8321551f7eaeb19d87aa00de9191968eae44", size = 1845008, upload-time = "2026-05-13T14:57:03.768Z" }, + { url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/01b6461117a6a94b5af3f8ee166bb0f045056f3cf187750c110dabfdfffa/torchvision-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a49e55055a39a8506fe7e59850522cab004efb2c3839f6057658889c1d69c815", size = 4141602, upload-time = "2026-05-13T14:56:53.449Z" }, + { url = "https://files.pythonhosted.org/packages/92/22/c0633677b3b3f3e69554a21ac087bf705f829c40cd5e3783507b8c006681/torchvision-0.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c1fac0fc2a7adf29481fc1938a0e7845c57ba1147a986784109c4d98f434ea8c", size = 1758818, upload-time = "2026-05-13T14:56:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/48/e8/55f9d9667b56dae470e69e31beac9b00d458ea393feec1aae95cc4f3f1c9/torchvision-0.27.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cbf89764fc76f3f17fbf80c12d5a89c691e91cb9d82c38412aaf0568655ffb19", size = 7789667, upload-time = "2026-05-13T14:56:48.858Z" }, + { url = "https://files.pythonhosted.org/packages/00/bc/6f8681daf3bbc4c315bb0005110f99d28e3ecd675bf9c8f2c0d393fbac7a/torchvision-0.27.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:91f61b9865423037c327eb56afa207cc72de874e458c361840db9dcf5ce0c0eb", size = 7579848, upload-time = "2026-05-13T14:56:38.209Z" }, + { url = "https://files.pythonhosted.org/packages/19/6c/8d8020e6bd1e46c53e487c9c4e9457a07f2ee28931028fb5d71e2da40adc/torchvision-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:5bb82fc3c55daf1788621e504310b0a286f1069627a8742f692aebb075ef25a7", size = 4119284, upload-time = "2026-05-13T14:56:46.625Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7e/e78c48662a8d551606efdbe11c6b9c1d6d2391b92cd0e4591b9e6a2412b8/torchvision-0.27.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2c4099a15150143b9b034730b404a56d572efe0b79489b4c765d929cb4eac7f3", size = 1758828, upload-time = "2026-05-13T14:56:52.293Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/d03ee9f9ee7bf11a8c7c776fb8e7fd6102f59c013791a2a4e5175bd6cba7/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b4c6bb0a670dcba017b3643e21902c9b8a1cc1c127d602f1488fa29ec3c6e865", size = 7790618, upload-time = "2026-05-13T14:56:44.721Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/4002336a74742be70728603ec1769feb2b55e0d19c532c9ec9f92008de76/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1c2db4bde82bc48ebff73436a6adf34d4f809448268a70d9a1285f5c8f92313d", size = 7580217, upload-time = "2026-05-13T14:56:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/ed/cb/4dd4783eb3565f526ba6e64b6f6ca26c00eacc924cdfe60455db9d91b84b/torchvision-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:72bf547e58ddb948689734eed6f4b6a2031f979dba4fb08e3690688b392e929f", size = 4226392, upload-time = "2026-05-13T14:56:40.235Z" }, ] [[package]] name = "tornado" -version = "6.5.5" +version = "6.5.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/57/6d7303a77ae439d9189108f76c0c4fd89ee5e2cc8387bffb55232565c4ed/tornado-6.5.6.tar.gz", hash = "sha256:9a365179fe8ff6b8766f602c0f67c185d778193e9bdd828b19f0b6ed7764177d", size = 518139, upload-time = "2026-05-27T15:35:54.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, - { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, - { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, - { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, - { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0d/b4f481e18c5a51864e6d12b9a05ecf72919696680b747c958c3fc1f4fbae/tornado-6.5.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:65fcfaafb079435c2c19dc9e07c0f1cf0fa9051759ed0a7d0a3ba7ea7f64919c", size = 447737, upload-time = "2026-05-27T15:35:38.122Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9c/5430c39fcab1144d35860f457b15e9c08b4bc7ac86764354204e983d6183/tornado-6.5.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:38bc01b4acacded2de63ae78023548e41ebe6fbed3ec05a796d7ae3ad893887e", size = 445899, upload-time = "2026-05-27T15:35:40.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/79/fa7e14a2f939c807a8d30619b4eb604eab219601b78792516ebe22d40cf9/tornado-6.5.6-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b942e6a137fda31ff54bf8e6e2c8d1c37f1f50583f3ed53fb840b53b9601d104", size = 448964, upload-time = "2026-05-27T15:35:42.106Z" }, + { url = "https://files.pythonhosted.org/packages/a7/71/bd67d5f5199f937dafe03a49a37989f60f600ff6fef34c79412a829d97bd/tornado-6.5.6-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8666946e70171b8c3f1fc9b7876fac492e84822c4c7f3746f4e8f8bc9ac92a79", size = 449935, upload-time = "2026-05-27T15:35:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/c24388c9cf5b3c3a513b56a158af9f23092c9a2810d789e294310797df21/tornado-6.5.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c34cfab7ad6d104f052f55de06d39bbafc5885cfeb4da688803308dbcfa90b7", size = 449767, upload-time = "2026-05-27T15:35:45.793Z" }, + { url = "https://files.pythonhosted.org/packages/a5/eb/6a07ad550c3f7b37244bd0becdf293ec3d3e961783d8b720a97df50de1b2/tornado-6.5.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:385f35e4e22fb52551dfcda4cdc8c30c61c2c001aef5ddad99cdfe116952efd3", size = 449174, upload-time = "2026-05-27T15:35:47.485Z" }, + { url = "https://files.pythonhosted.org/packages/bb/84/3469e098dccdb6763130e06aacd786bb4363fca7b590a55c101ddf34ed30/tornado-6.5.6-cp39-abi3-win32.whl", hash = "sha256:db475f1b67b2809b10bb16264829087724ca8d24fe4ed47f7b8675cae453ef86", size = 450230, upload-time = "2026-05-27T15:35:49.322Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3c/273a04e0b9dd9016f1685cca0c1c8795a71ac88a34a8c889a0b443483226/tornado-6.5.6-cp39-abi3-win_amd64.whl", hash = "sha256:6739bf1e8eb09230f1280ddbd3236f0309db70f2c551a8dbc40f62babdf82f79", size = 450667, upload-time = "2026-05-27T15:35:51.194Z" }, + { url = "https://files.pythonhosted.org/packages/02/98/0cffe22a224f60c5fb1e3aa0b76f9da2e1ca78b0e9545e3d077c68ce60a7/tornado-6.5.6-cp39-abi3-win_arm64.whl", hash = "sha256:2543597b24a695d72338a9a77818362d72387c03ae173f1f169eadc5c91466ac", size = 449690, upload-time = "2026-05-27T15:35:52.902Z" }, ] [[package]] @@ -7667,11 +7881,11 @@ wheels = [ [[package]] name = "traitlets" -version = "5.14.3" +version = "5.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, ] [[package]] @@ -7698,7 +7912,7 @@ wheels = [ [[package]] name = "transformer-engine-torch" -version = "0.5.18" +version = "2.11.0" source = { git = "https://github.com/NVIDIA/TransformerEngine.git?subdirectory=transformer_engine%2Fpytorch&rev=v2.11#c188b533cc3721ca9c6bbfd26148f5cf60108c25" } dependencies = [ { name = "einops" }, @@ -7749,12 +7963,12 @@ wheels = [ [[package]] name = "triton-windows" -version = "3.6.0.post26" +version = "3.7.0.post26" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/1e/8d9814e67ba3f20094cf3c69e7815a491f20beb86469a647550ba86728b0/triton_windows-3.6.0.post26-cp312-cp312-win_amd64.whl", hash = "sha256:189d8c57911aa9d2ff983a715e5c967b325f576307db60924cab22b501a36515", size = 47402104, upload-time = "2026-03-10T02:51:40.997Z" }, - { url = "https://files.pythonhosted.org/packages/2e/69/7579a5da5d8c5372711bb4b99a185ad35eae6c7549d85b9f75171e06832b/triton_windows-3.6.0.post26-cp313-cp313-win_amd64.whl", hash = "sha256:033f3d50c6a0e4539a3ccfa042304dbf76bf79155f382f9c09d010323d5a9a32", size = 47402101, upload-time = "2026-03-10T02:51:47.669Z" }, - { url = "https://files.pythonhosted.org/packages/7e/27/3d272c154c00c044e0f113d053650d6faf56258789dcdc1e5e3e4a91d86d/triton_windows-3.6.0.post26-cp314-cp314-win_amd64.whl", hash = "sha256:c8029386813d6df4ec700bbff050c1e308fd24d66be3d8848274fc5d97bad396", size = 48580269, upload-time = "2026-03-10T02:51:54.665Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/e6e2eb0d23916788d6b3436b519c351d1eb281b5d3dcbd9c3b4fc67e9f46/triton_windows-3.7.0.post26-cp312-cp312-win_amd64.whl", hash = "sha256:7fde7436c1147b9446627747ec291bde6fec9a5063cad38c43bd3fa57d954eca", size = 49645796, upload-time = "2026-05-14T14:13:25.496Z" }, + { url = "https://files.pythonhosted.org/packages/11/a3/137b8ad1e9bfa750a2f4cab380d7631c9f92c81c14c149161191af1fceda/triton_windows-3.7.0.post26-cp313-cp313-win_amd64.whl", hash = "sha256:74f82c6b7c6a26d6c5bbdf93ce9204e972af34cc1d3b23173108cc0c2126be58", size = 49645531, upload-time = "2026-05-14T14:14:56.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/76/c7e894886a65cb768bac009db491d2178a05cfaf428b3742c8049064766a/triton_windows-3.7.0.post26-cp314-cp314-win_amd64.whl", hash = "sha256:a644f216fea924737e676de98fd2448c0d9bfb39f045f98ce384c0d8d7603b7c", size = 50882809, upload-time = "2026-05-14T14:16:27.971Z" }, ] [[package]] @@ -7773,11 +7987,11 @@ wheels = [ [[package]] name = "trove-classifiers" -version = "2026.1.14.14" +version = "2026.5.22.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/b6/1c41aa221b157b624ea1a72e975404ef228724d249011ee411ac211a615e/trove_classifiers-2026.5.22.10.tar.gz", hash = "sha256:5477e9974e91904fb2cfa4a7581ab6e2f30c2c38d847fd00ed866080748101d5", size = 17061, upload-time = "2026-05-22T10:17:28.99Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/c9/02/9a14d3048ffa4f45b7c60956a9b22688dd925d6de50f6baf7e55f3664942/trove_classifiers-2026.5.22.10-py3-none-any.whl", hash = "sha256:01fe864225726e03efb843827ecabfe319fc4dee8dd66d65b8996cb09be46e2c", size = 14225, upload-time = "2026-05-22T10:17:27.569Z" }, ] [[package]] @@ -7806,53 +8020,53 @@ wheels = [ [[package]] name = "typeguard" -version = "4.5.1" +version = "4.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/e8/66e25efcc18542d58706ce4e50415710593721aae26e794ab1dec34fb66f/typeguard-4.5.1.tar.gz", hash = "sha256:f6f8ecbbc819c9bc749983cc67c02391e16a9b43b8b27f15dc70ed7c4a007274", size = 80121, upload-time = "2026-02-19T16:09:03.392Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/dfba5c4633cafc4c701f237d2ba63b416805047fd6d96aab4cfc40969f98/typeguard-4.5.2.tar.gz", hash = "sha256:5a16dcac23502039299c97c8941651bc33d7ea8cc4b2f7d6bbb1b528f6eea423", size = 80240, upload-time = "2026-05-14T12:59:40.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl", hash = "sha256:44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40", size = 36745, upload-time = "2026-02-19T16:09:01.6Z" }, + { url = "https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl", hash = "sha256:fcf9de18bd945cdb4c7b996e12b4c51ce83f92f191314a6d7cf1739586ec98cf", size = 36748, upload-time = "2026-05-14T12:59:39.473Z" }, ] [[package]] name = "typer" -version = "0.23.1" +version = "0.26.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, - { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/15/f5fc7be23b7196bc065b282d9589a372392fb10860c80f9c1dd7eb008662/typer-0.26.3.tar.gz", hash = "sha256:3e2b9352f535e5303ef27806dadc2c8647687bdca5c902f03fec3fb88f46a46a", size = 198326, upload-time = "2026-05-28T20:30:50.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, + { url = "https://files.pythonhosted.org/packages/cd/cc/c6c5dea061e2740355bfeef22ac6a41751bd2f3903e83921295569bdcec4/typer-0.26.3-py3-none-any.whl", hash = "sha256:e70549ec5a403ca8a0bf0802ddd9f3c6ff7a14ccbb859b01b697baa943636f33", size = 122338, upload-time = "2026-05-28T20:30:49.816Z" }, ] [[package]] name = "typer-slim" -version = "0.23.1" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/22/b9c47b8655937b6877d40791b937931702ba9c5f9d28753199266aa96f50/typer_slim-0.23.1.tar.gz", hash = "sha256:dfe92a6317030ee2380f65bf92e540d7c77fefcc689e10d585b4925b45b5e06a", size = 4762, upload-time = "2026-02-13T10:04:26.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/8a/5764b851659345f34787f1b6eb30b9d308bbd6c294825cbe38b6b869c97a/typer_slim-0.23.1-py3-none-any.whl", hash = "sha256:8146d5df1eb89f628191c4c604c8464fa841885d0733c58e6e700ff0228adac5", size = 3397, upload-time = "2026-02-13T10:04:27.132Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, ] [[package]] name = "types-paramiko" -version = "4.0.0.20260322" +version = "4.0.0.20260518" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/cc/b83f1c085cc2c4d85f4ba2f799d1b18840b768d7b1a7dfb7d5cc5470fbc9/types_paramiko-4.0.0.20260322.tar.gz", hash = "sha256:dfcb13d8cf52499a198ced552b78fa685369a376b143abfb90cd49f465e383a0", size = 29040, upload-time = "2026-03-22T04:08:47.815Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/b6/4cdb11fb6006be6309844dc5f88b6efef3f5ea5352ade42a1e9308570e28/types_paramiko-4.0.0.20260518.tar.gz", hash = "sha256:286f6830945cba63797eedf375ed87138d93198121253afe66c5d6dbcf91318d", size = 29193, upload-time = "2026-05-18T06:06:36.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/92/10415430e8035fe0155582757d9829784202bb198ed84fe9afa5e59d5263/types_paramiko-4.0.0.20260322-py3-none-any.whl", hash = "sha256:c585bcf81b5d2fc722279763d50eca8095777ee949af8706900e9f8411af979b", size = 38809, upload-time = "2026-03-22T04:08:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/44/a2/1a54b77758c9c175526bd0448de353f0563e71ba1ddc8bd4ac0c835deafd/types_paramiko-4.0.0.20260518-py3-none-any.whl", hash = "sha256:0ffaf1a6eb796833a49653cba4c7be13af51c8269d75234972d6239763dda270", size = 38791, upload-time = "2026-05-18T06:06:35.771Z" }, ] [[package]] @@ -7878,25 +8092,25 @@ wheels = [ [[package]] name = "tyro" -version = "1.0.10" +version = "1.0.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, { name = "typeguard" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/c1/0a5850badd3f18373d6a0366091638674cec6780b558c1c5b846adea938b/tyro-1.0.10.tar.gz", hash = "sha256:2822eacac963a4922bf7eafe3b156a1f0f7fe8e34148202987581224f25565c2", size = 481084, upload-time = "2026-03-18T08:24:17.307Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/d6/7126f9e7de139632134d59b5d1972e93c610ee2cb13829e8f4f48f6613cb/tyro-1.0.13.tar.gz", hash = "sha256:731a90c9836b77fffe7c3fa0477ef2d3b6fa91252ddc0bb4d32dadd4fcc143d4", size = 489479, upload-time = "2026-04-14T18:21:52.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/be/a0b4c9fa64999a2e337cbefcdedd2e101e8dd88a84e4fa497bd0e4531dc1/tyro-1.0.10-py3-none-any.whl", hash = "sha256:8de87a3a40c8a91f10831f8f0638cd0eed00f0e4de9cd3d561e967f407477210", size = 183433, upload-time = "2026-03-18T08:24:16.012Z" }, + { url = "https://files.pythonhosted.org/packages/93/4f/c43a0a8f0c66fd40a1d6cc47332a5a1d1043e9b331f7070ea701b91a7598/tyro-1.0.13-py3-none-any.whl", hash = "sha256:a0bdb8462c551dd84fc00a76916ce4d37e879c84eefaf34e2165312407cc6c09", size = 185221, upload-time = "2026-04-14T18:21:54.328Z" }, ] [[package]] name = "tzdata" -version = "2025.3" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] @@ -7979,11 +8193,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] @@ -8000,50 +8214,106 @@ wheels = [ [[package]] name = "uuid-utils" -version = "0.14.1" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/01/a1/822ceef22d1c139cffebe4b1b660cfaa10253d5c770aa2598dc8e9497593/uuid_utils-0.16.0.tar.gz", hash = "sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7", size = 42596, upload-time = "2026-05-19T07:44:23.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/4c/b4cf43a5d22bcdb91727acdf54be0d78e83e595b73c5a9a8a4291875f059/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:727fae3f0682191ec9c8ce1cd0f71e81b471a2e26b7c5fd66712fc0f11640aa0", size = 562183, upload-time = "2026-05-19T07:45:02.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fb/4b0d1c4b5e9f8679ca41b9cdbce5749e1d5db3d3d42a07060d6ce61ac583/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66a9c8cedf7695c28e700f6a66bde0809c3b2e0d8a70968be7bfd47c908952e5", size = 289018, upload-time = "2026-05-19T07:44:07.726Z" }, + { url = "https://files.pythonhosted.org/packages/de/43/2dc6c7401c8fab86e46b0b33ada6dcfde949b2fd48877ba6f880862be80e/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9152bff801ec2ccf630df06d67389090a2c612dea87fbf9a887ab4b222929f6f", size = 326171, upload-time = "2026-05-19T07:45:25.186Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/48f11fb91f36453611ca148bc441436f279870b1ec6b576dc5167fb6e680/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06fc7db470c37e5c1ab3fd2cd159697d6f8b279d7d23b5b96bd418b115f8caa9", size = 332222, upload-time = "2026-05-19T07:45:09.036Z" }, + { url = "https://files.pythonhosted.org/packages/30/cb/b2b49528521e4a097f129e8bf7850a26f00af46afba778832cf3458a5c00/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e1a1f57fe3631e164dad27b24aa81267810e20575f705af3b0fa734f3a21247", size = 444801, upload-time = "2026-05-19T07:45:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b3/a28d9c6f7c701dfe01c8020b30e33899a28eb9e4d056b07e7388f50ebf67/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ee392fe59808a731b7b6bf4d453fb6e833774921331cceae5f254d1e9c5b97d", size = 325594, upload-time = "2026-05-19T07:44:44.682Z" }, + { url = "https://files.pythonhosted.org/packages/cf/65/e1ff41dc44966e396ead86e104ba21b35ddb07ff7a64bb55013074ee77fe/uuid_utils-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b2e981b1258db444df4cf4bf4c79673570d081d48d35f22d0f86471e0ad795c5", size = 349312, upload-time = "2026-05-19T07:45:15.582Z" }, + { url = "https://files.pythonhosted.org/packages/ed/57/fb19b7951f66a46e03bd1943a61ee9d59c83e994e56e8c97d79aff1f0e47/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbb92feb4db08cd76e27b4d3b1a82bfde708447317150c614eb9f761a43b387e", size = 502115, upload-time = "2026-05-19T07:43:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8e/9a129c469b7b77afb62da5c6b7e92591073b845bd0c3108c0d0aa65389fb/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c3c5afaaa68b1d6393d653e9fc93a2fde9da1681da01f74b4593f41d31fb5f1", size = 607433, upload-time = "2026-05-19T07:44:11.675Z" }, + { url = "https://files.pythonhosted.org/packages/4a/56/2ef71fad168cc3d894f7094fa458086c093635d7835381c91470b19c9ad3/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:38126b353527c5f001e4b24db9e62351eb768d0367febcd68100a4b39a035109", size = 566076, upload-time = "2026-05-19T07:44:35.453Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/68e60ea053ca30f35df877b96001331398140d5c4983561affa1350331b1/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41a67e546d9adf11c4e4cb5c8e81f000f8b1f000c17912ced089b499855719a5", size = 530645, upload-time = "2026-05-19T07:45:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/42/19/b521f7d73094fca4c0c44002f4a42bfcbcf0b770fdc3c4b9a596dda25734/uuid_utils-0.16.0-cp312-cp312-win32.whl", hash = "sha256:52d2cc8c12a3466cd1727883e0746d8bad5dddd670369eb553ba17fdc3b565ca", size = 168887, upload-time = "2026-05-19T07:45:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/87/1f/4126c3ccbc2d98a613664e55f6ab6d7bd4b98424a04486e4fcc76549af15/uuid_utils-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97625e5edfda8b118160ce1e88756f92b1635775f836c168be7bf10928d97fa", size = 174607, upload-time = "2026-05-19T07:43:52.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/62/b83ccc8446ae39dcc0bda2cb3b525b6af6a2036383afe1d1d5fe7b234c2c/uuid_utils-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:baf79c8050eb784b252dd34807df73f61130fe8676b61231baccab62530f20ec", size = 173021, upload-time = "2026-05-19T07:45:10.204Z" }, + { url = "https://files.pythonhosted.org/packages/60/9b/74c1f47a9b4f138a254e51528e5ffaeba6bf99ecead9f0c4b6fccccfbfcb/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4", size = 563166, upload-time = "2026-05-19T07:44:10.494Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1c/009e37b70f1f0ff17e7103a36bafde33d503d9ea7fe739761aa3e3c9fde6/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0681d1bdb7956e0c6d581e7601dabcfb2b08c25d2a65189f4e9b102c94f5ff46", size = 289529, upload-time = "2026-05-19T07:43:54.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5e/e0323d54321166639eb2be5e8a464f5cb0fc04d72d91f3e78944bb6a1da8/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0", size = 326328, upload-time = "2026-05-19T07:45:31.901Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/046f6cb958467c3bf4a163a8a53b178b64a62e21ed8ad5b2c1dacb3a2cfc/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b", size = 332322, upload-time = "2026-05-19T07:43:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/01914e3949744db7acd0006885e5542fbebb6e39114857d007d29b3265c2/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e", size = 445787, upload-time = "2026-05-19T07:45:36.102Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/f6908f41279f205d70c8a0d5dcb25dd6802741d7f88e3f0123453c3584d3/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0", size = 324678, upload-time = "2026-05-19T07:45:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/11/4a/bf841ba90f829c7779d82155e0f4b88ef6726ccc25507d064d50ac2cd329/uuid_utils-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:95b7f480010ea98a29ee809857a98aa923008c68129af1b39244adccff7377fb", size = 349704, upload-time = "2026-05-19T07:44:47.172Z" }, + { url = "https://files.pythonhosted.org/packages/e6/31/3b5c60172b8c57bf4ca485484b8e4edef550ca324f9287f1183be97422e2/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306", size = 502456, upload-time = "2026-05-19T07:45:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/88/bf/3da8d497af80fd51d8bf85551c77ede67f07825924ec5987bf9b6031014a/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b", size = 607727, upload-time = "2026-05-19T07:44:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/7c8cf03ec15cd6f40e4cbab81b2b4a625461327f68c7971e54723280ec3e/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f235ac5827d74ac630cc87f29278cdaa5d2f273613a6e05bbd96df7aa4170776", size = 566204, upload-time = "2026-05-19T07:44:51.225Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5f/af955feae69cce7fd2121ca3f790ff4b85ad2e17b2149546f50753e1a047/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0", size = 529986, upload-time = "2026-05-19T07:45:57.85Z" }, + { url = "https://files.pythonhosted.org/packages/10/cf/3fec757e51bef10eb41ae8075f5442c60e85ff456b42d16a3063f5dc6c80/uuid_utils-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10", size = 98683, upload-time = "2026-05-19T07:44:16.369Z" }, + { url = "https://files.pythonhosted.org/packages/40/a7/cd1adbea7ef882a70db064c00cd93b12e11027b4cdd7ffd79e95c35fc3e3/uuid_utils-0.16.0-cp313-cp313-win32.whl", hash = "sha256:924a8de04460e4cf65998ad0b6568084f7c51740ebd3254d07a0bcde35a84af6", size = 168822, upload-time = "2026-05-19T07:44:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/74/99/617ceb9e3a95b23837012740979baf71afad723b70daf34862da3f7c17a1/uuid_utils-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:5279bc7ab3c6683f1c67314695bee14d869015acbbc677bdb0015190fe753d16", size = 174967, upload-time = "2026-05-19T07:44:56.022Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d8/148ae707bfc36d482e39db679c86b81bdce264d4feb9df5d40a03b7687e3/uuid_utils-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:61a9c4c26ad12ac66fa4bfd0fdb8494724fe7a5b98a9fcd43e78e2b388663dbb", size = 173142, upload-time = "2026-05-19T07:43:50.171Z" }, + { url = "https://files.pythonhosted.org/packages/21/05/ca6d60705e71fdeaa3431dad94e279a8213c5573cb2925e1aabf3dc0330a/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0", size = 564408, upload-time = "2026-05-19T07:44:38.351Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8c/b9a0462c38535c1662acb1025768e2d626bee5ce9e1790bad6b5381162ea/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f1614572fd9345cdc3dde3f40c237345719fabca1aa87d2d87b321d523cfa34d", size = 289923, upload-time = "2026-05-19T07:45:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/a53afeef1a56051551a0f5a801e4bce411dd73c6a8c99bad16902651256d/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a", size = 325762, upload-time = "2026-05-19T07:45:18.261Z" }, + { url = "https://files.pythonhosted.org/packages/72/ca/4462a4f36365d7ee72d41e05e6bcfe127e861b073ab37c25b2c8a518317c/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965", size = 332359, upload-time = "2026-05-19T07:45:34.886Z" }, + { url = "https://files.pythonhosted.org/packages/c5/67/9d3373fa7c5a746fdecc64e30caf915c29eb632203508d87676f9243ed03/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71", size = 445483, upload-time = "2026-05-19T07:44:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/57/08/ce01aa6d897fc7f875844fe58cad0a542c8ebf089d9242b654b56260ecb8/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98", size = 326281, upload-time = "2026-05-19T07:44:59.677Z" }, + { url = "https://files.pythonhosted.org/packages/76/ef/2c719b2c26bb5b5e5061a1435c11ad2bd33ac3cd6d4cd0c7c3ac1d3396ed/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:caac9c8b1d50e8fbddc76e93bfefbef472978eb45adbfdb6289d578816992953", size = 350809, upload-time = "2026-05-19T07:45:28.076Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9b/c1ed447328b32229cca38ac4c62d309eab006e5e9c4020e2056a175bc607/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c", size = 502088, upload-time = "2026-05-19T07:44:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e0/8442f4efe7bde72f0b4ae5f675d0c7fbe209ad0b54718b8ddf43c46c6fae/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada", size = 607631, upload-time = "2026-05-19T07:44:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/9a9fa261edf4c972f28ae83421377e3ab8dbd0bd7db58fd316e782d09a3b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1b0dcedf9266bf34a54d5cbe78648eaa627e02352f2a6923ed647530aea2f661", size = 567618, upload-time = "2026-05-19T07:43:58.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/1bcfdb9d539bd42736dd6076470a42fbb5db23f79712c0a06aa0a3752f7b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71", size = 530971, upload-time = "2026-05-19T07:45:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/24/0c/18945f417d6bb4d0dd2b7652fe36c58c4e83bcf593b9b326b83aa40b853a/uuid_utils-0.16.0-cp313-cp313t-win32.whl", hash = "sha256:7f8cf49c05d58523a0f977cb7f11afc05791a0fa164d7303b8365a34750638e7", size = 169369, upload-time = "2026-05-19T07:44:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/c0eb0c3fab2ed80d706369b750029143b53126809b77b36bcbb77da66bab/uuid_utils-0.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e99f9a8b2420b228faba23a637e96efaf5c6a678b2e225870f24431c82707f50", size = 175384, upload-time = "2026-05-19T07:45:56.623Z" }, + { url = "https://files.pythonhosted.org/packages/b7/77/50ac87b6e18b1c686f700aa38c9471a990683c6a955f71ac1a6677ed8145/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6853b627983aa1b4fd95aa52d9e87136eb94a7b3b7de0fbb1db8a498d457eeec", size = 564108, upload-time = "2026-05-19T07:43:55.609Z" }, + { url = "https://files.pythonhosted.org/packages/83/16/65046676de246bb5334d9f58aa96d2feb9fc347fda3556aaff7da1c2fc7a/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f44b65ae0c329843817d9c90e36a7a3c677b413bf407c99e67db874dac49dad3", size = 289967, upload-time = "2026-05-19T07:45:38.886Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/54fa988606a15dfd2028e925d8eb9c3ee6edbf1eb7692a67b37282880b56/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de8a365795a76f347f5622621c2bee543cffa0c70949f3ee093bdefc9d926dcc", size = 325835, upload-time = "2026-05-19T07:44:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1b/50622f967ceacea1f89fd065d9bfd395b51acb02cfb0a4ddc8fa9ff0c983/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:426a8c9af90242d879706ccf29da56f0b0712e7739fb0bbe16baacabc75596e2", size = 332607, upload-time = "2026-05-19T07:43:42.42Z" }, + { url = "https://files.pythonhosted.org/packages/12/f5/4059706be6617e2787e375ea52994ce3c3fa3920b7d4a9c8ebf7895681a5/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833bc4b3c3fc24be541f67b01b4a75b6b9942a9b7137395b4eb35435948bd6da", size = 444287, upload-time = "2026-05-19T07:43:37.106Z" }, + { url = "https://files.pythonhosted.org/packages/65/d5/f44b2710563da687a368f0ce4dcbd462dfb6708bcd46439d831991d595c7/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb5252d7c00d586077f10e169d6e6d0b0d0f806d8a085073f0d19b4737aef4e", size = 324949, upload-time = "2026-05-19T07:45:33.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a7/a69e859e37d26c5603f0bc0ae481860f691224f140e5a832f325b804770d/uuid_utils-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b3377ce388fd7bf8d231ec9d1d4f58c8e87888ddea93581f60ed6f878a4f722", size = 349651, upload-time = "2026-05-19T07:43:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/db/73/4139cd3ca7b81ea283c1c8769373e9b2008241c0744a8ffb25f0a1b31325/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:12b6310beb38adc173ec5dc89e98812fd7e3d98f87f3ef01d2ea6ecb5d87994f", size = 502326, upload-time = "2026-05-19T07:45:40.292Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8c/858101583fbad1b3fa04da88b1f7170836aa0f00b4cb712063325c44466d/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a49b5a75497643479c919e2e537a4a36224ac3aaa0fada61b75d87024021ac3e", size = 607689, upload-time = "2026-05-19T07:44:48.355Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/8f3d54a4763dd91ebd0f3d7b0c2ec434e4e0b1fc667b03a44d611a465ec6/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:63bfdf00be51b6b3b79275d6767d034ea5c7a0caa067a35d72861284100cb60a", size = 566214, upload-time = "2026-05-19T07:44:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/4c9a8d9baaa243c7902d84dbba4d51b1ab51c379c66d3fd6368ff6933ecf/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7525bc59ac4579c32317d2493dd42cf134b9bb50cd0bc6a41dd9f77e4740dde6", size = 529989, upload-time = "2026-05-19T07:44:43.141Z" }, + { url = "https://files.pythonhosted.org/packages/6d/13/d32cea997f880cedde415730ce0e872ebfd7a040155ae0bbda70eccd208e/uuid_utils-0.16.0-cp314-cp314-win32.whl", hash = "sha256:fbcac6e6710aa2e4bfbb81762758e01470dc56d5048ba4253acc77c9833568ff", size = 169146, upload-time = "2026-05-19T07:45:46.655Z" }, + { url = "https://files.pythonhosted.org/packages/1c/19/9fc55172d8fe59e1f27a14d598b427fa508a7ebb35fa7b7b99c24fa0ef13/uuid_utils-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:d23fcaf37368a1647319187ef6f8b741bf079f033065899bc2d00a44b0a1214a", size = 175364, upload-time = "2026-05-19T07:45:55.335Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/fcd9226b715c5aa0638fcdd6deaf0de6c6c3c451c692cd76bfca810c6512/uuid_utils-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:ea3265f8e2b452a4870f3298cb1d183dc4e36a3682cbb264dbe46af31267e706", size = 173268, upload-time = "2026-05-19T07:44:31.19Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/97ec9af95e58b8187f2934008ffab26e1604d149e34fe01c388b0543a24f/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:99f8420c3ed59f89a086782ac197e257f4b1debb4545dffa90cf5db23f96c892", size = 564464, upload-time = "2026-05-19T07:44:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6d/e4082f407484ac28923c0bf8e861e71d277118d8b7542d0a350340e45350/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:259bab73c241743d684dcc3507feb76f484d720545e4e4805582aeff8e19700b", size = 290087, upload-time = "2026-05-19T07:44:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/43/c5c5f273c0ff889f20f10344784f9197dd00eb81ccc294330d4b949fea7e/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:897e8ef0dc5e4ac0b17cf9cae84bb41e560d806280ec5b93db7475b504022105", size = 325532, upload-time = "2026-05-19T07:43:47.508Z" }, + { url = "https://files.pythonhosted.org/packages/13/7f/669aa899ab5378374d28a28231e6978f739921a1af394c7ebd6cc86e2639/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5af79cde16a7600dfccb7d431aec0afd3088ff170b6a09887bf3f7ab3cc7c81", size = 332209, upload-time = "2026-05-19T07:43:51.528Z" }, + { url = "https://files.pythonhosted.org/packages/2b/57/a2a32406d79a222794ef98a19254fd9a81a029a0f32d7740fba9873bff1f/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bece1a6f677ca36047442c465d8166643eed9818b9e43e0bf42d3cf73e92dcff", size = 445507, upload-time = "2026-05-19T07:44:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/85459a35bfa7d73e79acbc4eab1cf6aa6e4d9d022c3260ed9dea539c7f0b/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3444498e7b099499c8a607d7771377020fa55f7274e46f54106af19f752d7", size = 326154, upload-time = "2026-05-19T07:45:23.587Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/e965efdbb503ed14d6e57aec1a22b98326ed24cc2fb48e750c4d192267a0/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:542098f6cb6874aebeff98715f3ab7646fbe0f2ffb24509ca372828c68c4ed0e", size = 350905, upload-time = "2026-05-19T07:44:36.957Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/4321867888a783d03b7c053c0b68ca45d03974d86fcebf44d4ec268db397/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7207b25fe534bcf4d57e0110f90670e61c1c38b6f4598ba855af69ab428fc118", size = 502098, upload-time = "2026-05-19T07:44:17.696Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/914a47bf42479bff0ce3e1fa1cbe3585354708edc928e27687cf91de9c26/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:16dc5c6e439f75b0456114e955983e2156c1f38887733e54d54205d3005223e4", size = 607032, upload-time = "2026-05-19T07:44:22.151Z" }, + { url = "https://files.pythonhosted.org/packages/85/4c/2abacd6badba61a047eaa39c8347656229d12843bd9bbe4906daa6dc752c/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6d3ee32c57898d8415242b08d5dd086bc4f7bcbbb3fc102ef257f3d793eb294", size = 567664, upload-time = "2026-05-19T07:45:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/53/1f/9d1a09521276424da19dc0d74456aed3311170fec181b28fa6acba45d963/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7555f120a2282d1901c9a632c2398a614101af4fe3f7c8114aa0f1d8c1978855", size = 530996, upload-time = "2026-05-19T07:45:44.229Z" }, + { url = "https://files.pythonhosted.org/packages/b4/22/14dbedb6b61f492d5524077fd10bbfb137583b0f0aafa6cd870ccb43f39a/uuid_utils-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:756575d082ea4cb7d2f923d5b640c0efe7c82573aab49220c4e09b62d13737ff", size = 169358, upload-time = "2026-05-19T07:45:05.146Z" }, + { url = "https://files.pythonhosted.org/packages/25/f4/a636806c98401a1108f2456e9cc3fa39a618145bfb1d0860c57203159cfe/uuid_utils-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:aa50261a83991dbb570a00573741455bd8f3249444f7329e5bdcd494799d1504", size = 174813, upload-time = "2026-05-19T07:45:59.579Z" }, + { url = "https://files.pythonhosted.org/packages/75/12/3823742459d87a100deb24bb6b41692aa961b267abd130fa7739cdf7d409/uuid_utils-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1", size = 171733, upload-time = "2026-05-19T07:45:29.283Z" }, ] [[package]] name = "uv" -version = "0.11.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/02/69a3b06fd8a91f95b79e95e14f5ccdd4df0f124c381aefe9d1e2784d5a65/uv-0.11.11.tar.gz", hash = "sha256:2ba46a912a1775957c579a1a42c8c8b480418502326b72427b1cad972c8f659f", size = 4112827, upload-time = "2026-05-06T20:04:47.982Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/54/39d3c58de992767834120fe3735b85cc60dd00a69b377c3d947ca6f172a1/uv-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:4977a1193e5dc9c2934b9f97d6cf787382f80deae17646640ee583cfc61486c0", size = 23537936, upload-time = "2026-05-06T20:04:58.626Z" }, - { url = "https://files.pythonhosted.org/packages/de/c9/d2d7ca30abf4c2d5ae0d9360a1e154115af176308ef1ecdc8bf7af724cf8/uv-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:92817f276758e41b4160fcb6d457ebd9f228f0473efe3808891164f326fdea38", size = 23068282, upload-time = "2026-05-06T20:05:01.466Z" }, - { url = "https://files.pythonhosted.org/packages/fa/37/f64decba47d7afaace3f238aa4a416dca947bd0a1a9b534c3a0f179e1016/uv-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6eec6ad051e6e5d922cd547b9f7b09a7f821597ae01900a6f01b0a01317e5fd0", size = 21671522, upload-time = "2026-05-06T20:05:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/93/a6/c129878d7c2a66ffdaa12dc253d3135c5e10fc5b5e15812791e188c6dbec/uv-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1d227bb53b701e533f0aa074dd145a6fa31492dc7d6d57a6e72a700b9a4a1991", size = 23283200, upload-time = "2026-05-06T20:04:39.879Z" }, - { url = "https://files.pythonhosted.org/packages/8f/c2/cff1f9ab7eda3d863e9866fca0e14df37c0fd734b66ebb77d751258b2fae/uv-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:05ee9f18701692fcb22db98085c041a3be7a35b88c710dea4487c293f42a4b95", size = 23081561, upload-time = "2026-05-06T20:05:07.149Z" }, - { url = "https://files.pythonhosted.org/packages/ca/44/ebd02ca8fae5961d1bcbcee11019dd170dd0d42517afad753281335700cc/uv-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0632af539d6a1ee00f58da9e7db32fd99e12187aa67426cb90d871154ab5debb", size = 23105780, upload-time = "2026-05-06T20:04:50.107Z" }, - { url = "https://files.pythonhosted.org/packages/86/f7/0741abcd70591a65f85fc4e8fecd3fb3fb4bdfe50042cccf016714955fd9/uv-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb3f2715551d2fc9ef44b6cf0918fcc556cd99e9bf6caa1d8a870a4657d2b180", size = 24542681, upload-time = "2026-05-06T20:04:53.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/42/46e7e35f1f39e39d4bf0f712479768cf8d33eb7f35b67fceaea43e975dfd/uv-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c86bd6460579857d7e359bdbfe6f688076c654481ae933151d1449f9ea672fb6", size = 25459284, upload-time = "2026-05-06T20:04:34.168Z" }, - { url = "https://files.pythonhosted.org/packages/e8/fc/efdb16e1a6c619b021259ac8d8e4b6afd97efb446054ea28761eb2e1a177/uv-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f69f4df007c7506db8d7f77ccabd466a886ac21e9b04a479dd0cd22e26d2262", size = 24560769, upload-time = "2026-05-06T20:04:42.648Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f8/a5d5bac297b1379719050788c6b852c6b3eefcb1e82d8465ed22c10cede7/uv-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5b9f31dab557b5ee4257d8c6ba2608a63c7278537cb0cd102cf6fc518e3fb5c", size = 24639659, upload-time = "2026-05-06T20:04:31.491Z" }, - { url = "https://files.pythonhosted.org/packages/ee/d5/f3be167a43192062f1409fd6b857a612665d331174293b4ffc73218872e1/uv-0.11.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8e8faf2e5b3517155fd18e509b19b21135247d43b7fb9a8d61a44a53118d5ab7", size = 23388445, upload-time = "2026-05-06T20:04:25.199Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cd/ef1f573ee8edd2beab9fcd2449121483829621b3b57f7ba3f35c56ef373b/uv-0.11.11-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:3f8c9a1bea743a3fe39e956455686f4d0dd25ef58e8d70dc11a45381fd7c50e5", size = 24114301, upload-time = "2026-05-06T20:04:28.586Z" }, - { url = "https://files.pythonhosted.org/packages/9d/be/9181158465719e875a6995c10af24e00cdefba3fe6c9c8cbb02d34b2ade7/uv-0.11.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f68dc7b62050a26ac6b1491398aebbbf0fa5485627e73b1d626666a097dbab07", size = 24155126, upload-time = "2026-05-06T20:04:55.98Z" }, - { url = "https://files.pythonhosted.org/packages/71/9c/bb306f9964870847f02a931d1fff896726f8bafcf9ce917122ac1bfef14c/uv-0.11.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:29ddb0d9b24a30ff4360b94e3cb704e82cd5fda86dc224032251f33ab5ceb79e", size = 23824684, upload-time = "2026-05-06T20:05:10.305Z" }, - { url = "https://files.pythonhosted.org/packages/56/48/434a1cf4798ca200e0dcb36411ba38013edb6d3e1aeb4cd85e8a2d7db9ca/uv-0.11.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:505a31f2c30fa9e83b1853cab06c5b92e66341c914c6f20f3878903aa09a6f34", size = 24862560, upload-time = "2026-05-06T20:04:37.287Z" }, - { url = "https://files.pythonhosted.org/packages/63/3a/997cddf82917f084d486e1c268c7e94836190fd928c93aa3fb92caee9a7f/uv-0.11.11-py3-none-win32.whl", hash = "sha256:c1e0e3e18cc94680642eac3c3f19f2635c17dd058edcb41b78cbdc459f574eb4", size = 22573619, upload-time = "2026-05-06T20:04:45.35Z" }, - { url = "https://files.pythonhosted.org/packages/30/5f/db34b840f8d86833ef810de8150fc9ce01a03c779393e08eadbcc4c010d5/uv-0.11.11-py3-none-win_amd64.whl", hash = "sha256:36412b13f6287304789abdf40122d268cee548fce3573e07d148a29370181421", size = 25170135, upload-time = "2026-05-06T20:05:13.001Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3e/f3ba2557b437ec5b1fde1e0d5248b723432dc90f09b0050f52695596fd2e/uv-0.11.11-py3-none-win_arm64.whl", hash = "sha256:011f42faf5d267a6681ea77e3f236f275cb4490efeecb9599de74dc7ad7df8f6", size = 23597162, upload-time = "2026-05-06T20:05:16.095Z" }, +version = "0.11.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/8e/ec34c19d0f254fcbcc5c1ce8c7f06e47e0f69a7e1a0269c1d59cb0b0f279/uv-0.11.17.tar.gz", hash = "sha256:1d1be74deec997db1dda05a7e67541c904d65cbfd72e455d3c0a2a1e4bf2cddf", size = 4203607, upload-time = "2026-05-28T20:39:47.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/2e/e6d42f9d39009eee976f1e5dfd31d3d1943e6e593ad7b191cf11e9744a36/uv-0.11.17-py3-none-linux_armv6l.whl", hash = "sha256:8426bfe315564d414cbc5ba5467595dc6348965e19acec742914f47da3ff269f", size = 23551216, upload-time = "2026-05-28T20:39:05.395Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ee/d72bcc60f3585653a4b768425854d737d98d65c1765547d25c2999547ea9/uv-0.11.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6d1a033cc68cabb4141d6c1e3b66ffc6e970b98ba42e210f33270251e0bd8697", size = 22997377, upload-time = "2026-05-28T20:39:25.21Z" }, + { url = "https://files.pythonhosted.org/packages/58/34/1bc69798d9ae998fbc42c61b02883f2ba00d04bdd858e589604d01846287/uv-0.11.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58c07ffc272c847d29cd98ca5082fa4304a645f87c718ec900e3cca9026bd096", size = 21630197, upload-time = "2026-05-28T20:39:28.935Z" }, + { url = "https://files.pythonhosted.org/packages/6b/93/1be48ec6a8933d9a77d0ce5240ed63f68869f68517ccf5d62268ed03f3e8/uv-0.11.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:036d6e2940afe8b79637530b01b9241d8cfd174b07f1179a1ebbd42409c38ca3", size = 23414940, upload-time = "2026-05-28T20:39:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/00/31/b7488ff49d80090ea9d05d67a4d381a1b4479502e9853e654caa1c1c678e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:283186700c3e65a4644a73a917232da7d3e4a94d25ea0377a44f5b263fa49577", size = 23096330, upload-time = "2026-05-28T20:39:01.284Z" }, + { url = "https://files.pythonhosted.org/packages/fe/95/42b6137c5de06278d229c7eef2f314df2a738cd799795bbb44dace21bd6e/uv-0.11.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2e44dfbfc7778d0d90edc6738f237c91e5e37e4e3cfe94c8a312cec56a41485", size = 23101906, upload-time = "2026-05-28T20:39:17.149Z" }, + { url = "https://files.pythonhosted.org/packages/17/7c/0ca03b2d19965db6d5dfe0c8cf96a3d0b424503c8cbc3cd2ffdc5869a15d/uv-0.11.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a817eeb3026f27a53d3f4b7855a5105f6787dd192140e201eda4d2b9a11b72e", size = 24444409, upload-time = "2026-05-28T20:39:59.218Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fb/179f55a3b19d47c30ec1f41b9b964da74dfa7053ff310a70a9c4d8cb998d/uv-0.11.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf8f5ad959583dcd2c4ae445c754a97c05700246ff89259f3fd285c9c20f4c00", size = 25540153, upload-time = "2026-05-28T20:39:09.535Z" }, + { url = "https://files.pythonhosted.org/packages/f7/29/592f42012765c43ae45c112110e214bca7b0cfc08c4c1b52e1dfa47dedd5/uv-0.11.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce16892a45134d20165c1ceababe06f3e9ce6a58902db1eff812c8c93626823f", size = 24665906, upload-time = "2026-05-28T20:39:41.254Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/b75808766f895248553c6370968509cd4f726e6943e310a8f7a171036ad0/uv-0.11.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da839e5a491c9a701d7d327a199cafc76ac27a03ac84fd2a8d4bf32c3af2448", size = 24863325, upload-time = "2026-05-28T20:39:51.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6a/6f27ee69e97f480104bb8ec335f04c2a12add98edfcc4844a68e9538b6e2/uv-0.11.17-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ec004b3c9bf9cb7756067ad1bd0bf64eb843e6fa2edbfbb3135ee152c14cea91", size = 23521674, upload-time = "2026-05-28T20:38:55.869Z" }, + { url = "https://files.pythonhosted.org/packages/df/11/1344aca7c710f794750f74de0e552a54ab24193ecc01fa3b3ae22ff822a1/uv-0.11.17-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:659227cac719b618cc91e02be9e274ad5bd72d74fa278123e6373537e9f28216", size = 24224725, upload-time = "2026-05-28T20:39:32.945Z" }, + { url = "https://files.pythonhosted.org/packages/ad/44/7b11550c1453ea13b81e549c83523e6ab6ed3231d09b2fd6b9eb19acceaf/uv-0.11.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e301d844eed9401f0f0351de12c55f1306ca05372acb0f28d35717c8ba663a22", size = 24301643, upload-time = "2026-05-28T20:39:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/1a/36/8f683bc60547b8f93d0e752a8574d13fad776999cb978482b360c053ca22/uv-0.11.17-py3-none-musllinux_1_1_i686.whl", hash = "sha256:f0bf483c0d9fa14283992d56061b498b9d3d4adebd285af8744dc33f64dadfba", size = 23786049, upload-time = "2026-05-28T20:39:20.999Z" }, + { url = "https://files.pythonhosted.org/packages/10/dc/7a495db39c2970de4fa375c337dbd617b16780911f88f0511f8fe7f6747c/uv-0.11.17-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2ccd5487a4a192bc832ea04c867a26883757db8fdfe88bed85d8129c82f9e505", size = 25049786, upload-time = "2026-05-28T20:40:03.292Z" }, + { url = "https://files.pythonhosted.org/packages/37/dd/74eff72d749eaf7e19f489878e21a368a7fef58d26ea0c63ec044ecd78b1/uv-0.11.17-py3-none-win32.whl", hash = "sha256:12b701fa32c5be3691759a73956e4462f30fa7b0dfa52ec66cb305bbb6ea4129", size = 22479213, upload-time = "2026-05-28T20:39:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/8af4a92b99a8a4823297c26df727fe957267e03e1196e3caa803c3f6ccb2/uv-0.11.17-py3-none-win_amd64.whl", hash = "sha256:44ec1fe3af839f87370dcf0400c0cab917cc1ce697d563e860fc7d9ed72655e7", size = 25083161, upload-time = "2026-05-28T20:40:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/00/76/a689077832d585d29d87f9cd0d65eca1af58abd29a4eab004d0a8a858b9c/uv-0.11.17-py3-none-win_arm64.whl", hash = "sha256:37c915bfcf86f99c1c5be7c9ed21e0d80624067ba47bc8916a3cb0530bc94d27", size = 23544936, upload-time = "2026-05-28T20:39:37.137Z" }, ] [[package]] @@ -8104,7 +8374,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.2.0" +version = "21.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -8112,9 +8382,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/f0/b47ecf438211a25a97f8f0e4b23c22bc2496ebfea18dd6ec16210f09cc36/virtualenv-21.4.1.tar.gz", hash = "sha256:2ca543c713b72840ceffd94e9bdedfbd09a661defa1f7f69e5429ad4059442e2", size = 7613344, upload-time = "2026-05-28T04:12:49.905Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, + { url = "https://files.pythonhosted.org/packages/ff/dc/ac4f3a987a87e1a18556896f257c4e15c95ed157b7975347ec6b313b75ce/virtualenv-21.4.1-py3-none-any.whl", hash = "sha256:caf4ff72d1b4039057f41d8e8466e859513d67c0400d9c6b62c02c9d1ebc3e12", size = 7594078, upload-time = "2026-05-28T04:12:47.686Z" }, ] [[package]] @@ -8181,72 +8451,88 @@ wheels = [ [[package]] name = "watchfiles" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, ] [[package]] @@ -8263,16 +8549,16 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, ] [[package]] name = "weave" -version = "0.52.35" +version = "0.52.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -8280,17 +8566,19 @@ dependencies = [ { name = "diskcache-weave" }, { name = "gql", extra = ["httpx"] }, { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, { name = "packaging" }, { name = "polyfile-weave" }, { name = "pydantic" }, { name = "sentry-sdk" }, { name = "tenacity" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, - { name = "wandb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/c1/be5614320c016c27c9c58765cc380451e799aadb5e04a47995e38c27d100/weave-0.52.35.tar.gz", hash = "sha256:ba30a303a0b00fd428137833b3913fcfda5d0ce4723da2cd600eb48baa972678", size = 770064, upload-time = "2026-03-19T20:04:20.863Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/7c/f0c54919dc390beaf33086e15abdc1b8499c6273c2035d73703ed8a0b9d6/weave-0.52.41.tar.gz", hash = "sha256:59159952f9c7c65d78dd4f7a96bfc13accb2f3d93cb43583af6c6d05c5036b4d", size = 937328, upload-time = "2026-05-19T22:03:03.124Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/51/1011621f5c1efe326613231c317f54cf31ff07a220e8425c7a749809cdf1/weave-0.52.35-py3-none-any.whl", hash = "sha256:b3ed2209da6017cd580a71c24a8c0d967f32bd16f736ed32b04823a7330d4709", size = 954994, upload-time = "2026-03-19T20:04:18.961Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c9/5ccf0962f20189399edb7461afac225501a30a1567592baea899e5300ca5/weave-0.52.41-py3-none-any.whl", hash = "sha256:3f5611b2eee399a48ce26c1df4c3238418dd5a4cee1d0e8b86a3b7db8a7d931c", size = 1150794, upload-time = "2026-05-19T22:03:00.675Z" }, ] [[package]] @@ -8349,14 +8637,14 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.7" +version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/43/76ded108b296a49f52de6bac5192ca1c4be84e886f9b5c9ba8427d9694fd/werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351", size = 875700, upload-time = "2026-03-24T01:08:07.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295, upload-time = "2026-03-24T01:08:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] @@ -8379,66 +8667,66 @@ wheels = [ [[package]] name = "wrapt" -version = "2.1.2" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, - { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, - { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, - { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, - { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, - { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, - { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, - { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, - { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, - { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, - { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, - { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, - { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, - { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, - { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, - { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, - { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, - { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, - { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, - { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, - { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, - { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, - { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0", size = 81021, upload-time = "2026-05-22T14:48:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8", size = 81692, upload-time = "2026-05-22T14:48:01.49Z" }, + { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" }, + { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" }, + { url = "https://files.pythonhosted.org/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052", size = 77857, upload-time = "2026-05-22T14:48:11.782Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5", size = 80825, upload-time = "2026-05-22T14:48:13.046Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579", size = 79087, upload-time = "2026-05-22T14:48:14.323Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, + { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, + { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, + { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" }, + { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, ] [[package]] @@ -8455,8 +8743,8 @@ name = "xformers" version = "0.0.35" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "torch" }, + { name = "numpy", marker = "platform_machine != 's390x'" }, + { name = "torch", marker = "platform_machine != 's390x'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/5a/6e27734bd793adc44d0b8d294e67cfacf4ec590572c1aef51d683fc7a791/xformers-0.0.35.tar.gz", hash = "sha256:f7fc183a58e4bf0e2ae339a18fb1b1d4a37854c0f2545b4f360fef001646ab76", size = 4258182, upload-time = "2026-02-20T20:33:05.417Z" } wheels = [ @@ -8466,198 +8754,215 @@ wheels = [ [[package]] name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" }, + { url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" }, + { url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" }, + { url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" }, + { url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" }, ] [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] + +[[package]] +name = "z3-solver" +version = "4.15.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/8e/0c8f17309549d2e5cde9a3ccefa6365437f1e7bafe71878eaf9478e47b18/z3_solver-4.15.4.0.tar.gz", hash = "sha256:928c29b58c4eb62106da51c1914f6a4a55d0441f8f48a81b9da07950434a8946", size = 5018600, upload-time = "2025-10-29T18:12:03.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/c9/bb51a96af0091324c81b803f16c49f719f9f6ea0b0bb52200f5c97ec4892/z3_solver-4.15.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e103a6f203f505b8b8b8e5c931cc407c95b61556512d4921c1ddc0b3f41b08e", size = 29268352, upload-time = "2025-10-29T18:11:53.032Z" }, ] [[package]] name = "zipp" -version = "3.23.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, ] [[package]] diff --git a/vllm_runtime/src/art_vllm_runtime/patches.py b/vllm_runtime/src/art_vllm_runtime/patches.py index f4f58eac9..53e098cd2 100644 --- a/vllm_runtime/src/art_vllm_runtime/patches.py +++ b/vllm_runtime/src/art_vllm_runtime/patches.py @@ -514,11 +514,19 @@ def issue_routing_d2h_copy( ) capturer_cls._scatter_to_host = scatter_to_host # type: ignore[method-assign] capturer_cls.get_routed_experts = get_routed_experts # type: ignore[method-assign] - routed_experts_capturer.issue_routing_d2h_copy = issue_routing_d2h_copy - try: - from vllm.v1.worker import gpu_model_runner + from vllm.v1.worker import gpu_model_runner - gpu_model_runner.issue_routing_d2h_copy = issue_routing_d2h_copy - except Exception: - pass + gpu_model_runner_issue_routing_d2h_copy = getattr( + gpu_model_runner, "issue_routing_d2h_copy", None + ) + if gpu_model_runner_issue_routing_d2h_copy is not original_issue_routing_d2h_copy: + raise RuntimeError( + "ART routed-expert prefix-cache patch expected " + "vllm.v1.worker.gpu_model_runner.issue_routing_d2h_copy to reference " + "vllm.model_executor.layers.fused_moe.routed_experts_capturer." + "issue_routing_d2h_copy. vLLM internals changed; update the patch." + ) + + routed_experts_capturer.issue_routing_d2h_copy = issue_routing_d2h_copy + gpu_model_runner.issue_routing_d2h_copy = issue_routing_d2h_copy setattr(routed_experts_capturer, "_art_prefix_route_sidecar_patched", True)