uv_torch/
backend.rs

1//! `uv-torch` is a library for determining the appropriate PyTorch index based on the operating
2//! system and CUDA driver version.
3//!
4//! This library is derived from `light-the-torch` by Philipp Meier, which is available under the
5//! following BSD-3 Clause license:
6//!
7//! ```text
8//! BSD 3-Clause License
9//!
10//! Copyright (c) 2020, Philip Meier
11//! All rights reserved.
12//!
13//! Redistribution and use in source and binary forms, with or without
14//! modification, are permitted provided that the following conditions are met:
15//!
16//! 1. Redistributions of source code must retain the above copyright notice, this
17//!    list of conditions and the following disclaimer.
18//!
19//! 2. Redistributions in binary form must reproduce the above copyright notice,
20//!    this list of conditions and the following disclaimer in the documentation
21//!    and/or other materials provided with the distribution.
22//!
23//! 3. Neither the name of the copyright holder nor the names of its
24//!    contributors may be used to endorse or promote products derived from
25//!    this software without specific prior written permission.
26//!
27//! THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28//! AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29//! IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
30//! DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
31//! FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
32//! DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
33//! SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
34//! CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
35//! OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36//! OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37//! ```
38
39use std::borrow::Cow;
40use std::str::FromStr;
41use std::sync::LazyLock;
42
43use either::Either;
44use url::Url;
45
46use uv_distribution_types::IndexUrl;
47use uv_normalize::PackageName;
48use uv_pep440::Version;
49use uv_platform_tags::Os;
50use uv_static::EnvVars;
51
52use crate::{Accelerator, AcceleratorError, AmdGpuArchitecture};
53
54/// The strategy to use when determining the appropriate PyTorch index.
55#[derive(Debug, Copy, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
56#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
57#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
58#[serde(rename_all = "kebab-case")]
59pub enum TorchMode {
60    /// Select the appropriate PyTorch index based on the operating system and CUDA driver version.
61    Auto,
62    /// Use the CPU-only PyTorch index.
63    Cpu,
64    /// Use the PyTorch index for CUDA 13.0.
65    Cu130,
66    /// Use the PyTorch index for CUDA 12.9.
67    Cu129,
68    /// Use the PyTorch index for CUDA 12.8.
69    Cu128,
70    /// Use the PyTorch index for CUDA 12.6.
71    Cu126,
72    /// Use the PyTorch index for CUDA 12.5.
73    Cu125,
74    /// Use the PyTorch index for CUDA 12.4.
75    Cu124,
76    /// Use the PyTorch index for CUDA 12.3.
77    Cu123,
78    /// Use the PyTorch index for CUDA 12.2.
79    Cu122,
80    /// Use the PyTorch index for CUDA 12.1.
81    Cu121,
82    /// Use the PyTorch index for CUDA 12.0.
83    Cu120,
84    /// Use the PyTorch index for CUDA 11.8.
85    Cu118,
86    /// Use the PyTorch index for CUDA 11.7.
87    Cu117,
88    /// Use the PyTorch index for CUDA 11.6.
89    Cu116,
90    /// Use the PyTorch index for CUDA 11.5.
91    Cu115,
92    /// Use the PyTorch index for CUDA 11.4.
93    Cu114,
94    /// Use the PyTorch index for CUDA 11.3.
95    Cu113,
96    /// Use the PyTorch index for CUDA 11.2.
97    Cu112,
98    /// Use the PyTorch index for CUDA 11.1.
99    Cu111,
100    /// Use the PyTorch index for CUDA 11.0.
101    Cu110,
102    /// Use the PyTorch index for CUDA 10.2.
103    Cu102,
104    /// Use the PyTorch index for CUDA 10.1.
105    Cu101,
106    /// Use the PyTorch index for CUDA 10.0.
107    Cu100,
108    /// Use the PyTorch index for CUDA 9.2.
109    Cu92,
110    /// Use the PyTorch index for CUDA 9.1.
111    Cu91,
112    /// Use the PyTorch index for CUDA 9.0.
113    Cu90,
114    /// Use the PyTorch index for CUDA 8.0.
115    Cu80,
116    /// Use the PyTorch index for ROCm 6.4.
117    #[serde(rename = "rocm6.4")]
118    #[cfg_attr(feature = "clap", clap(name = "rocm6.4"))]
119    Rocm64,
120    /// Use the PyTorch index for ROCm 6.3.
121    #[serde(rename = "rocm6.3")]
122    #[cfg_attr(feature = "clap", clap(name = "rocm6.3"))]
123    Rocm63,
124    /// Use the PyTorch index for ROCm 6.2.4.
125    #[serde(rename = "rocm6.2.4")]
126    #[cfg_attr(feature = "clap", clap(name = "rocm6.2.4"))]
127    Rocm624,
128    /// Use the PyTorch index for ROCm 6.2.
129    #[serde(rename = "rocm6.2")]
130    #[cfg_attr(feature = "clap", clap(name = "rocm6.2"))]
131    Rocm62,
132    /// Use the PyTorch index for ROCm 6.1.
133    #[serde(rename = "rocm6.1")]
134    #[cfg_attr(feature = "clap", clap(name = "rocm6.1"))]
135    Rocm61,
136    /// Use the PyTorch index for ROCm 6.0.
137    #[serde(rename = "rocm6.0")]
138    #[cfg_attr(feature = "clap", clap(name = "rocm6.0"))]
139    Rocm60,
140    /// Use the PyTorch index for ROCm 5.7.
141    #[serde(rename = "rocm5.7")]
142    #[cfg_attr(feature = "clap", clap(name = "rocm5.7"))]
143    Rocm57,
144    /// Use the PyTorch index for ROCm 5.6.
145    #[serde(rename = "rocm5.6")]
146    #[cfg_attr(feature = "clap", clap(name = "rocm5.6"))]
147    Rocm56,
148    /// Use the PyTorch index for ROCm 5.5.
149    #[serde(rename = "rocm5.5")]
150    #[cfg_attr(feature = "clap", clap(name = "rocm5.5"))]
151    Rocm55,
152    /// Use the PyTorch index for ROCm 5.4.2.
153    #[serde(rename = "rocm5.4.2")]
154    #[cfg_attr(feature = "clap", clap(name = "rocm5.4.2"))]
155    Rocm542,
156    /// Use the PyTorch index for ROCm 5.4.
157    #[serde(rename = "rocm5.4")]
158    #[cfg_attr(feature = "clap", clap(name = "rocm5.4"))]
159    Rocm54,
160    /// Use the PyTorch index for ROCm 5.3.
161    #[serde(rename = "rocm5.3")]
162    #[cfg_attr(feature = "clap", clap(name = "rocm5.3"))]
163    Rocm53,
164    /// Use the PyTorch index for ROCm 5.2.
165    #[serde(rename = "rocm5.2")]
166    #[cfg_attr(feature = "clap", clap(name = "rocm5.2"))]
167    Rocm52,
168    /// Use the PyTorch index for ROCm 5.1.1.
169    #[serde(rename = "rocm5.1.1")]
170    #[cfg_attr(feature = "clap", clap(name = "rocm5.1.1"))]
171    Rocm511,
172    /// Use the PyTorch index for ROCm 4.2.
173    #[serde(rename = "rocm4.2")]
174    #[cfg_attr(feature = "clap", clap(name = "rocm4.2"))]
175    Rocm42,
176    /// Use the PyTorch index for ROCm 4.1.
177    #[serde(rename = "rocm4.1")]
178    #[cfg_attr(feature = "clap", clap(name = "rocm4.1"))]
179    Rocm41,
180    /// Use the PyTorch index for ROCm 4.0.1.
181    #[serde(rename = "rocm4.0.1")]
182    #[cfg_attr(feature = "clap", clap(name = "rocm4.0.1"))]
183    Rocm401,
184    /// Use the PyTorch index for Intel XPU.
185    Xpu,
186}
187
188#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
189pub enum TorchSource {
190    /// Download PyTorch builds from the official PyTorch index.
191    #[default]
192    PyTorch,
193    /// Download PyTorch builds from the pyx index.
194    Pyx,
195}
196
197/// The strategy to use when determining the appropriate PyTorch index.
198#[derive(Debug, Clone, Eq, PartialEq)]
199pub enum TorchStrategy {
200    /// Select the appropriate PyTorch index based on the operating system and CUDA driver version (e.g., `550.144.03`).
201    Cuda {
202        os: Os,
203        driver_version: Version,
204        source: TorchSource,
205    },
206    /// Select the appropriate PyTorch index based on the operating system and AMD GPU architecture (e.g., `gfx1100`).
207    Amd {
208        os: Os,
209        gpu_architecture: AmdGpuArchitecture,
210        source: TorchSource,
211    },
212    /// Select the appropriate PyTorch index based on the operating system and Intel GPU presence.
213    Xpu { os: Os, source: TorchSource },
214    /// Use the specified PyTorch index.
215    Backend {
216        backend: TorchBackend,
217        source: TorchSource,
218    },
219}
220
221impl TorchStrategy {
222    /// Determine the [`TorchStrategy`] from the given [`TorchMode`], [`Os`], and [`Accelerator`].
223    pub fn from_mode(
224        mode: TorchMode,
225        source: TorchSource,
226        os: &Os,
227    ) -> Result<Self, AcceleratorError> {
228        let backend = match mode {
229            TorchMode::Auto => match Accelerator::detect()? {
230                Some(Accelerator::Cuda { driver_version }) => {
231                    return Ok(Self::Cuda {
232                        os: os.clone(),
233                        driver_version: driver_version.clone(),
234                        source,
235                    });
236                }
237                Some(Accelerator::Amd { gpu_architecture }) => {
238                    return Ok(Self::Amd {
239                        os: os.clone(),
240                        gpu_architecture,
241                        source,
242                    });
243                }
244                Some(Accelerator::Xpu) => {
245                    return Ok(Self::Xpu {
246                        os: os.clone(),
247                        source,
248                    });
249                }
250                None => TorchBackend::Cpu,
251            },
252            TorchMode::Cpu => TorchBackend::Cpu,
253            TorchMode::Cu130 => TorchBackend::Cu130,
254            TorchMode::Cu129 => TorchBackend::Cu129,
255            TorchMode::Cu128 => TorchBackend::Cu128,
256            TorchMode::Cu126 => TorchBackend::Cu126,
257            TorchMode::Cu125 => TorchBackend::Cu125,
258            TorchMode::Cu124 => TorchBackend::Cu124,
259            TorchMode::Cu123 => TorchBackend::Cu123,
260            TorchMode::Cu122 => TorchBackend::Cu122,
261            TorchMode::Cu121 => TorchBackend::Cu121,
262            TorchMode::Cu120 => TorchBackend::Cu120,
263            TorchMode::Cu118 => TorchBackend::Cu118,
264            TorchMode::Cu117 => TorchBackend::Cu117,
265            TorchMode::Cu116 => TorchBackend::Cu116,
266            TorchMode::Cu115 => TorchBackend::Cu115,
267            TorchMode::Cu114 => TorchBackend::Cu114,
268            TorchMode::Cu113 => TorchBackend::Cu113,
269            TorchMode::Cu112 => TorchBackend::Cu112,
270            TorchMode::Cu111 => TorchBackend::Cu111,
271            TorchMode::Cu110 => TorchBackend::Cu110,
272            TorchMode::Cu102 => TorchBackend::Cu102,
273            TorchMode::Cu101 => TorchBackend::Cu101,
274            TorchMode::Cu100 => TorchBackend::Cu100,
275            TorchMode::Cu92 => TorchBackend::Cu92,
276            TorchMode::Cu91 => TorchBackend::Cu91,
277            TorchMode::Cu90 => TorchBackend::Cu90,
278            TorchMode::Cu80 => TorchBackend::Cu80,
279            TorchMode::Rocm64 => TorchBackend::Rocm64,
280            TorchMode::Rocm63 => TorchBackend::Rocm63,
281            TorchMode::Rocm624 => TorchBackend::Rocm624,
282            TorchMode::Rocm62 => TorchBackend::Rocm62,
283            TorchMode::Rocm61 => TorchBackend::Rocm61,
284            TorchMode::Rocm60 => TorchBackend::Rocm60,
285            TorchMode::Rocm57 => TorchBackend::Rocm57,
286            TorchMode::Rocm56 => TorchBackend::Rocm56,
287            TorchMode::Rocm55 => TorchBackend::Rocm55,
288            TorchMode::Rocm542 => TorchBackend::Rocm542,
289            TorchMode::Rocm54 => TorchBackend::Rocm54,
290            TorchMode::Rocm53 => TorchBackend::Rocm53,
291            TorchMode::Rocm52 => TorchBackend::Rocm52,
292            TorchMode::Rocm511 => TorchBackend::Rocm511,
293            TorchMode::Rocm42 => TorchBackend::Rocm42,
294            TorchMode::Rocm41 => TorchBackend::Rocm41,
295            TorchMode::Rocm401 => TorchBackend::Rocm401,
296            TorchMode::Xpu => TorchBackend::Xpu,
297        };
298        Ok(Self::Backend { backend, source })
299    }
300
301    /// Returns `true` if the [`TorchStrategy`] applies to the given [`PackageName`].
302    pub fn applies_to(&self, package_name: &PackageName) -> bool {
303        let source = match self {
304            Self::Cuda { source, .. } => *source,
305            Self::Amd { source, .. } => *source,
306            Self::Xpu { source, .. } => *source,
307            Self::Backend { source, .. } => *source,
308        };
309        match source {
310            TorchSource::PyTorch => {
311                matches!(
312                    package_name.as_str(),
313                    "pytorch-triton"
314                        | "pytorch-triton-rocm"
315                        | "pytorch-triton-xpu"
316                        | "torch"
317                        | "torch-tensorrt"
318                        | "torchao"
319                        | "torcharrow"
320                        | "torchaudio"
321                        | "torchcsprng"
322                        | "torchdata"
323                        | "torchdistx"
324                        | "torchserve"
325                        | "torchtext"
326                        | "torchvision"
327                        | "triton"
328                )
329            }
330            TorchSource::Pyx => {
331                matches!(
332                    package_name.as_str(),
333                    "deepspeed"
334                        | "flash-attn"
335                        | "flash-attn-3"
336                        | "megablocks"
337                        | "natten"
338                        | "pyg-lib"
339                        | "pytorch-triton"
340                        | "pytorch-triton-rocm"
341                        | "pytorch-triton-xpu"
342                        | "torch"
343                        | "torch-cluster"
344                        | "torch-scatter"
345                        | "torch-sparse"
346                        | "torch-spline-conv"
347                        | "torch-tensorrt"
348                        | "torchao"
349                        | "torcharrow"
350                        | "torchaudio"
351                        | "torchcsprng"
352                        | "torchdata"
353                        | "torchdistx"
354                        | "torchserve"
355                        | "torchtext"
356                        | "torchvision"
357                        | "triton"
358                        | "vllm"
359                )
360            }
361        }
362    }
363
364    /// Returns `true` if the given [`PackageName`] has a system dependency (e.g., CUDA or ROCm).
365    ///
366    /// For example, `triton` is hosted on the PyTorch indexes, but does not have a system
367    /// dependency on the associated CUDA version (i.e., the `triton` on the `cu128` index doesn't
368    /// depend on CUDA 12.8).
369    pub fn has_system_dependency(&self, package_name: &PackageName) -> bool {
370        matches!(
371            package_name.as_str(),
372            "deepspeed"
373                | "flash-attn"
374                | "flash-attn-3"
375                | "megablocks"
376                | "natten"
377                | "torch"
378                | "torch-tensorrt"
379                | "torchao"
380                | "torcharrow"
381                | "torchaudio"
382                | "torchcsprng"
383                | "torchdata"
384                | "torchdistx"
385                | "torchtext"
386                | "torchvision"
387                | "vllm"
388        )
389    }
390
391    /// Return the appropriate index URLs for the given [`TorchStrategy`].
392    pub fn index_urls(&self) -> impl Iterator<Item = &IndexUrl> {
393        match self {
394            Self::Cuda {
395                os,
396                driver_version,
397                source,
398            } => {
399                // If this is a GPU-enabled package, and CUDA drivers are installed, use PyTorch's CUDA
400                // indexes.
401                //
402                // See: https://github.com/pmeier/light-the-torch/blob/33397cbe45d07b51ad8ee76b004571a4c236e37f/light_the_torch/_patch.py#L36-L49
403                match os {
404                    Os::Manylinux { .. } | Os::Musllinux { .. } => {
405                        Either::Left(Either::Left(Either::Left(
406                            LINUX_CUDA_DRIVERS
407                                .iter()
408                                .filter_map(move |(backend, version)| {
409                                    if driver_version >= version {
410                                        Some(backend.index_url(*source))
411                                    } else {
412                                        None
413                                    }
414                                })
415                                .chain(std::iter::once(TorchBackend::Cpu.index_url(*source))),
416                        )))
417                    }
418                    Os::Windows => Either::Left(Either::Left(Either::Right(
419                        WINDOWS_CUDA_VERSIONS
420                            .iter()
421                            .filter_map(move |(backend, version)| {
422                                if driver_version >= version {
423                                    Some(backend.index_url(*source))
424                                } else {
425                                    None
426                                }
427                            })
428                            .chain(std::iter::once(TorchBackend::Cpu.index_url(*source))),
429                    ))),
430                    Os::Macos { .. }
431                    | Os::FreeBsd { .. }
432                    | Os::NetBsd { .. }
433                    | Os::OpenBsd { .. }
434                    | Os::Dragonfly { .. }
435                    | Os::Illumos { .. }
436                    | Os::Haiku { .. }
437                    | Os::Android { .. }
438                    | Os::Pyodide { .. }
439                    | Os::Ios { .. } => Either::Right(Either::Left(std::iter::once(
440                        TorchBackend::Cpu.index_url(*source),
441                    ))),
442                }
443            }
444            Self::Amd {
445                os,
446                gpu_architecture,
447                source,
448            } => match os {
449                Os::Manylinux { .. } | Os::Musllinux { .. } => Either::Left(Either::Right(
450                    LINUX_AMD_GPU_DRIVERS
451                        .iter()
452                        .filter_map(move |(backend, architecture)| {
453                            if gpu_architecture == architecture {
454                                Some(backend.index_url(*source))
455                            } else {
456                                None
457                            }
458                        })
459                        .chain(std::iter::once(TorchBackend::Cpu.index_url(*source))),
460                )),
461                Os::Windows
462                | Os::Macos { .. }
463                | Os::FreeBsd { .. }
464                | Os::NetBsd { .. }
465                | Os::OpenBsd { .. }
466                | Os::Dragonfly { .. }
467                | Os::Illumos { .. }
468                | Os::Haiku { .. }
469                | Os::Android { .. }
470                | Os::Pyodide { .. }
471                | Os::Ios { .. } => Either::Right(Either::Left(std::iter::once(
472                    TorchBackend::Cpu.index_url(*source),
473                ))),
474            },
475            Self::Xpu { os, source } => match os {
476                Os::Manylinux { .. } | Os::Windows => Either::Right(Either::Right(Either::Left(
477                    std::iter::once(TorchBackend::Xpu.index_url(*source)),
478                ))),
479                Os::Musllinux { .. }
480                | Os::Macos { .. }
481                | Os::FreeBsd { .. }
482                | Os::NetBsd { .. }
483                | Os::OpenBsd { .. }
484                | Os::Dragonfly { .. }
485                | Os::Illumos { .. }
486                | Os::Haiku { .. }
487                | Os::Android { .. }
488                | Os::Pyodide { .. }
489                | Os::Ios { .. } => Either::Right(Either::Left(std::iter::once(
490                    TorchBackend::Cpu.index_url(*source),
491                ))),
492            },
493            Self::Backend { backend, source } => Either::Right(Either::Right(Either::Right(
494                std::iter::once(backend.index_url(*source)),
495            ))),
496        }
497    }
498}
499
500/// The available backends for PyTorch.
501#[derive(Debug, Copy, Clone, Eq, PartialEq)]
502pub enum TorchBackend {
503    Cpu,
504    Cu130,
505    Cu129,
506    Cu128,
507    Cu126,
508    Cu125,
509    Cu124,
510    Cu123,
511    Cu122,
512    Cu121,
513    Cu120,
514    Cu118,
515    Cu117,
516    Cu116,
517    Cu115,
518    Cu114,
519    Cu113,
520    Cu112,
521    Cu111,
522    Cu110,
523    Cu102,
524    Cu101,
525    Cu100,
526    Cu92,
527    Cu91,
528    Cu90,
529    Cu80,
530    Rocm64,
531    Rocm63,
532    Rocm624,
533    Rocm62,
534    Rocm61,
535    Rocm60,
536    Rocm57,
537    Rocm56,
538    Rocm55,
539    Rocm542,
540    Rocm54,
541    Rocm53,
542    Rocm52,
543    Rocm511,
544    Rocm42,
545    Rocm41,
546    Rocm401,
547    Xpu,
548}
549
550impl TorchBackend {
551    /// Return the appropriate index URL for the given [`TorchBackend`].
552    fn index_url(self, source: TorchSource) -> &'static IndexUrl {
553        match self {
554            Self::Cpu => match source {
555                TorchSource::PyTorch => &PYTORCH_CPU_INDEX_URL,
556                TorchSource::Pyx => &PYX_CPU_INDEX_URL,
557            },
558            Self::Cu130 => match source {
559                TorchSource::PyTorch => &PYTORCH_CU130_INDEX_URL,
560                TorchSource::Pyx => &PYX_CU130_INDEX_URL,
561            },
562            Self::Cu129 => match source {
563                TorchSource::PyTorch => &PYTORCH_CU129_INDEX_URL,
564                TorchSource::Pyx => &PYX_CU129_INDEX_URL,
565            },
566            Self::Cu128 => match source {
567                TorchSource::PyTorch => &PYTORCH_CU128_INDEX_URL,
568                TorchSource::Pyx => &PYX_CU128_INDEX_URL,
569            },
570            Self::Cu126 => match source {
571                TorchSource::PyTorch => &PYTORCH_CU126_INDEX_URL,
572                TorchSource::Pyx => &PYX_CU126_INDEX_URL,
573            },
574            Self::Cu125 => match source {
575                TorchSource::PyTorch => &PYTORCH_CU125_INDEX_URL,
576                TorchSource::Pyx => &PYX_CU125_INDEX_URL,
577            },
578            Self::Cu124 => match source {
579                TorchSource::PyTorch => &PYTORCH_CU124_INDEX_URL,
580                TorchSource::Pyx => &PYX_CU124_INDEX_URL,
581            },
582            Self::Cu123 => match source {
583                TorchSource::PyTorch => &PYTORCH_CU123_INDEX_URL,
584                TorchSource::Pyx => &PYX_CU123_INDEX_URL,
585            },
586            Self::Cu122 => match source {
587                TorchSource::PyTorch => &PYTORCH_CU122_INDEX_URL,
588                TorchSource::Pyx => &PYX_CU122_INDEX_URL,
589            },
590            Self::Cu121 => match source {
591                TorchSource::PyTorch => &PYTORCH_CU121_INDEX_URL,
592                TorchSource::Pyx => &PYX_CU121_INDEX_URL,
593            },
594            Self::Cu120 => match source {
595                TorchSource::PyTorch => &PYTORCH_CU120_INDEX_URL,
596                TorchSource::Pyx => &PYX_CU120_INDEX_URL,
597            },
598            Self::Cu118 => match source {
599                TorchSource::PyTorch => &PYTORCH_CU118_INDEX_URL,
600                TorchSource::Pyx => &PYX_CU118_INDEX_URL,
601            },
602            Self::Cu117 => match source {
603                TorchSource::PyTorch => &PYTORCH_CU117_INDEX_URL,
604                TorchSource::Pyx => &PYX_CU117_INDEX_URL,
605            },
606            Self::Cu116 => match source {
607                TorchSource::PyTorch => &PYTORCH_CU116_INDEX_URL,
608                TorchSource::Pyx => &PYX_CU116_INDEX_URL,
609            },
610            Self::Cu115 => match source {
611                TorchSource::PyTorch => &PYTORCH_CU115_INDEX_URL,
612                TorchSource::Pyx => &PYX_CU115_INDEX_URL,
613            },
614            Self::Cu114 => match source {
615                TorchSource::PyTorch => &PYTORCH_CU114_INDEX_URL,
616                TorchSource::Pyx => &PYX_CU114_INDEX_URL,
617            },
618            Self::Cu113 => match source {
619                TorchSource::PyTorch => &PYTORCH_CU113_INDEX_URL,
620                TorchSource::Pyx => &PYX_CU113_INDEX_URL,
621            },
622            Self::Cu112 => match source {
623                TorchSource::PyTorch => &PYTORCH_CU112_INDEX_URL,
624                TorchSource::Pyx => &PYX_CU112_INDEX_URL,
625            },
626            Self::Cu111 => match source {
627                TorchSource::PyTorch => &PYTORCH_CU111_INDEX_URL,
628                TorchSource::Pyx => &PYX_CU111_INDEX_URL,
629            },
630            Self::Cu110 => match source {
631                TorchSource::PyTorch => &PYTORCH_CU110_INDEX_URL,
632                TorchSource::Pyx => &PYX_CU110_INDEX_URL,
633            },
634            Self::Cu102 => match source {
635                TorchSource::PyTorch => &PYTORCH_CU102_INDEX_URL,
636                TorchSource::Pyx => &PYX_CU102_INDEX_URL,
637            },
638            Self::Cu101 => match source {
639                TorchSource::PyTorch => &PYTORCH_CU101_INDEX_URL,
640                TorchSource::Pyx => &PYX_CU101_INDEX_URL,
641            },
642            Self::Cu100 => match source {
643                TorchSource::PyTorch => &PYTORCH_CU100_INDEX_URL,
644                TorchSource::Pyx => &PYX_CU100_INDEX_URL,
645            },
646            Self::Cu92 => match source {
647                TorchSource::PyTorch => &PYTORCH_CU92_INDEX_URL,
648                TorchSource::Pyx => &PYX_CU92_INDEX_URL,
649            },
650            Self::Cu91 => match source {
651                TorchSource::PyTorch => &PYTORCH_CU91_INDEX_URL,
652                TorchSource::Pyx => &PYX_CU91_INDEX_URL,
653            },
654            Self::Cu90 => match source {
655                TorchSource::PyTorch => &PYTORCH_CU90_INDEX_URL,
656                TorchSource::Pyx => &PYX_CU90_INDEX_URL,
657            },
658            Self::Cu80 => match source {
659                TorchSource::PyTorch => &PYTORCH_CU80_INDEX_URL,
660                TorchSource::Pyx => &PYX_CU80_INDEX_URL,
661            },
662            Self::Rocm64 => match source {
663                TorchSource::PyTorch => &PYTORCH_ROCM64_INDEX_URL,
664                TorchSource::Pyx => &PYX_ROCM64_INDEX_URL,
665            },
666            Self::Rocm63 => match source {
667                TorchSource::PyTorch => &PYTORCH_ROCM63_INDEX_URL,
668                TorchSource::Pyx => &PYX_ROCM63_INDEX_URL,
669            },
670            Self::Rocm624 => match source {
671                TorchSource::PyTorch => &PYTORCH_ROCM624_INDEX_URL,
672                TorchSource::Pyx => &PYX_ROCM624_INDEX_URL,
673            },
674            Self::Rocm62 => match source {
675                TorchSource::PyTorch => &PYTORCH_ROCM62_INDEX_URL,
676                TorchSource::Pyx => &PYX_ROCM62_INDEX_URL,
677            },
678            Self::Rocm61 => match source {
679                TorchSource::PyTorch => &PYTORCH_ROCM61_INDEX_URL,
680                TorchSource::Pyx => &PYX_ROCM61_INDEX_URL,
681            },
682            Self::Rocm60 => match source {
683                TorchSource::PyTorch => &PYTORCH_ROCM60_INDEX_URL,
684                TorchSource::Pyx => &PYX_ROCM60_INDEX_URL,
685            },
686            Self::Rocm57 => match source {
687                TorchSource::PyTorch => &PYTORCH_ROCM57_INDEX_URL,
688                TorchSource::Pyx => &PYX_ROCM57_INDEX_URL,
689            },
690            Self::Rocm56 => match source {
691                TorchSource::PyTorch => &PYTORCH_ROCM56_INDEX_URL,
692                TorchSource::Pyx => &PYX_ROCM56_INDEX_URL,
693            },
694            Self::Rocm55 => match source {
695                TorchSource::PyTorch => &PYTORCH_ROCM55_INDEX_URL,
696                TorchSource::Pyx => &PYX_ROCM55_INDEX_URL,
697            },
698            Self::Rocm542 => match source {
699                TorchSource::PyTorch => &PYTORCH_ROCM542_INDEX_URL,
700                TorchSource::Pyx => &PYX_ROCM542_INDEX_URL,
701            },
702            Self::Rocm54 => match source {
703                TorchSource::PyTorch => &PYTORCH_ROCM54_INDEX_URL,
704                TorchSource::Pyx => &PYX_ROCM54_INDEX_URL,
705            },
706            Self::Rocm53 => match source {
707                TorchSource::PyTorch => &PYTORCH_ROCM53_INDEX_URL,
708                TorchSource::Pyx => &PYX_ROCM53_INDEX_URL,
709            },
710            Self::Rocm52 => match source {
711                TorchSource::PyTorch => &PYTORCH_ROCM52_INDEX_URL,
712                TorchSource::Pyx => &PYX_ROCM52_INDEX_URL,
713            },
714            Self::Rocm511 => match source {
715                TorchSource::PyTorch => &PYTORCH_ROCM511_INDEX_URL,
716                TorchSource::Pyx => &PYX_ROCM511_INDEX_URL,
717            },
718            Self::Rocm42 => match source {
719                TorchSource::PyTorch => &PYTORCH_ROCM42_INDEX_URL,
720                TorchSource::Pyx => &PYX_ROCM42_INDEX_URL,
721            },
722            Self::Rocm41 => match source {
723                TorchSource::PyTorch => &PYTORCH_ROCM41_INDEX_URL,
724                TorchSource::Pyx => &PYX_ROCM41_INDEX_URL,
725            },
726            Self::Rocm401 => match source {
727                TorchSource::PyTorch => &PYTORCH_ROCM401_INDEX_URL,
728                TorchSource::Pyx => &PYX_ROCM401_INDEX_URL,
729            },
730            Self::Xpu => match source {
731                TorchSource::PyTorch => &PYTORCH_XPU_INDEX_URL,
732                TorchSource::Pyx => &PYX_XPU_INDEX_URL,
733            },
734        }
735    }
736
737    /// Extract a [`TorchBackend`] from an index URL.
738    pub fn from_index(index: &Url) -> Option<Self> {
739        let backend_identifier = if index.host_str() == Some("download.pytorch.org") {
740            // E.g., `https://download.pytorch.org/whl/cu124`
741            let mut path_segments = index.path_segments()?;
742            if path_segments.next() != Some("whl") {
743                return None;
744            }
745            path_segments.next()?
746        // TODO(zanieb): We should consolidate this with `is_known_url` somehow
747        } else if index.host_str() == PYX_API_BASE_URL.strip_prefix("https://") {
748            // E.g., `https://api.pyx.dev/simple/astral-sh/cu124`
749            let mut path_segments = index.path_segments()?;
750            if path_segments.next() != Some("simple") {
751                return None;
752            }
753            if path_segments.next() != Some("astral-sh") {
754                return None;
755            }
756            path_segments.next()?
757        } else {
758            return None;
759        };
760        Self::from_str(backend_identifier).ok()
761    }
762
763    /// Returns the CUDA [`Version`] for the given [`TorchBackend`].
764    pub fn cuda_version(&self) -> Option<Version> {
765        match self {
766            Self::Cpu => None,
767            Self::Cu130 => Some(Version::new([13, 0])),
768            Self::Cu129 => Some(Version::new([12, 9])),
769            Self::Cu128 => Some(Version::new([12, 8])),
770            Self::Cu126 => Some(Version::new([12, 6])),
771            Self::Cu125 => Some(Version::new([12, 5])),
772            Self::Cu124 => Some(Version::new([12, 4])),
773            Self::Cu123 => Some(Version::new([12, 3])),
774            Self::Cu122 => Some(Version::new([12, 2])),
775            Self::Cu121 => Some(Version::new([12, 1])),
776            Self::Cu120 => Some(Version::new([12, 0])),
777            Self::Cu118 => Some(Version::new([11, 8])),
778            Self::Cu117 => Some(Version::new([11, 7])),
779            Self::Cu116 => Some(Version::new([11, 6])),
780            Self::Cu115 => Some(Version::new([11, 5])),
781            Self::Cu114 => Some(Version::new([11, 4])),
782            Self::Cu113 => Some(Version::new([11, 3])),
783            Self::Cu112 => Some(Version::new([11, 2])),
784            Self::Cu111 => Some(Version::new([11, 1])),
785            Self::Cu110 => Some(Version::new([11, 0])),
786            Self::Cu102 => Some(Version::new([10, 2])),
787            Self::Cu101 => Some(Version::new([10, 1])),
788            Self::Cu100 => Some(Version::new([10, 0])),
789            Self::Cu92 => Some(Version::new([9, 2])),
790            Self::Cu91 => Some(Version::new([9, 1])),
791            Self::Cu90 => Some(Version::new([9, 0])),
792            Self::Cu80 => Some(Version::new([8, 0])),
793            Self::Rocm64 => None,
794            Self::Rocm63 => None,
795            Self::Rocm624 => None,
796            Self::Rocm62 => None,
797            Self::Rocm61 => None,
798            Self::Rocm60 => None,
799            Self::Rocm57 => None,
800            Self::Rocm56 => None,
801            Self::Rocm55 => None,
802            Self::Rocm542 => None,
803            Self::Rocm54 => None,
804            Self::Rocm53 => None,
805            Self::Rocm52 => None,
806            Self::Rocm511 => None,
807            Self::Rocm42 => None,
808            Self::Rocm41 => None,
809            Self::Rocm401 => None,
810            Self::Xpu => None,
811        }
812    }
813
814    /// Returns the ROCM [`Version`] for the given [`TorchBackend`].
815    pub fn rocm_version(&self) -> Option<Version> {
816        match self {
817            Self::Cpu => None,
818            Self::Cu130 => None,
819            Self::Cu129 => None,
820            Self::Cu128 => None,
821            Self::Cu126 => None,
822            Self::Cu125 => None,
823            Self::Cu124 => None,
824            Self::Cu123 => None,
825            Self::Cu122 => None,
826            Self::Cu121 => None,
827            Self::Cu120 => None,
828            Self::Cu118 => None,
829            Self::Cu117 => None,
830            Self::Cu116 => None,
831            Self::Cu115 => None,
832            Self::Cu114 => None,
833            Self::Cu113 => None,
834            Self::Cu112 => None,
835            Self::Cu111 => None,
836            Self::Cu110 => None,
837            Self::Cu102 => None,
838            Self::Cu101 => None,
839            Self::Cu100 => None,
840            Self::Cu92 => None,
841            Self::Cu91 => None,
842            Self::Cu90 => None,
843            Self::Cu80 => None,
844            Self::Rocm64 => Some(Version::new([6, 4])),
845            Self::Rocm63 => Some(Version::new([6, 3])),
846            Self::Rocm624 => Some(Version::new([6, 2, 4])),
847            Self::Rocm62 => Some(Version::new([6, 2])),
848            Self::Rocm61 => Some(Version::new([6, 1])),
849            Self::Rocm60 => Some(Version::new([6, 0])),
850            Self::Rocm57 => Some(Version::new([5, 7])),
851            Self::Rocm56 => Some(Version::new([5, 6])),
852            Self::Rocm55 => Some(Version::new([5, 5])),
853            Self::Rocm542 => Some(Version::new([5, 4, 2])),
854            Self::Rocm54 => Some(Version::new([5, 4])),
855            Self::Rocm53 => Some(Version::new([5, 3])),
856            Self::Rocm52 => Some(Version::new([5, 2])),
857            Self::Rocm511 => Some(Version::new([5, 1, 1])),
858            Self::Rocm42 => Some(Version::new([4, 2])),
859            Self::Rocm41 => Some(Version::new([4, 1])),
860            Self::Rocm401 => Some(Version::new([4, 0, 1])),
861            Self::Xpu => None,
862        }
863    }
864}
865
866impl FromStr for TorchBackend {
867    type Err = String;
868
869    fn from_str(s: &str) -> Result<Self, Self::Err> {
870        match s {
871            "cpu" => Ok(Self::Cpu),
872            "cu130" => Ok(Self::Cu130),
873            "cu129" => Ok(Self::Cu129),
874            "cu128" => Ok(Self::Cu128),
875            "cu126" => Ok(Self::Cu126),
876            "cu125" => Ok(Self::Cu125),
877            "cu124" => Ok(Self::Cu124),
878            "cu123" => Ok(Self::Cu123),
879            "cu122" => Ok(Self::Cu122),
880            "cu121" => Ok(Self::Cu121),
881            "cu120" => Ok(Self::Cu120),
882            "cu118" => Ok(Self::Cu118),
883            "cu117" => Ok(Self::Cu117),
884            "cu116" => Ok(Self::Cu116),
885            "cu115" => Ok(Self::Cu115),
886            "cu114" => Ok(Self::Cu114),
887            "cu113" => Ok(Self::Cu113),
888            "cu112" => Ok(Self::Cu112),
889            "cu111" => Ok(Self::Cu111),
890            "cu110" => Ok(Self::Cu110),
891            "cu102" => Ok(Self::Cu102),
892            "cu101" => Ok(Self::Cu101),
893            "cu100" => Ok(Self::Cu100),
894            "cu92" => Ok(Self::Cu92),
895            "cu91" => Ok(Self::Cu91),
896            "cu90" => Ok(Self::Cu90),
897            "cu80" => Ok(Self::Cu80),
898            "rocm6.4" => Ok(Self::Rocm64),
899            "rocm6.3" => Ok(Self::Rocm63),
900            "rocm6.2.4" => Ok(Self::Rocm624),
901            "rocm6.2" => Ok(Self::Rocm62),
902            "rocm6.1" => Ok(Self::Rocm61),
903            "rocm6.0" => Ok(Self::Rocm60),
904            "rocm5.7" => Ok(Self::Rocm57),
905            "rocm5.6" => Ok(Self::Rocm56),
906            "rocm5.5" => Ok(Self::Rocm55),
907            "rocm5.4.2" => Ok(Self::Rocm542),
908            "rocm5.4" => Ok(Self::Rocm54),
909            "rocm5.3" => Ok(Self::Rocm53),
910            "rocm5.2" => Ok(Self::Rocm52),
911            "rocm5.1.1" => Ok(Self::Rocm511),
912            "rocm4.2" => Ok(Self::Rocm42),
913            "rocm4.1" => Ok(Self::Rocm41),
914            "rocm4.0.1" => Ok(Self::Rocm401),
915            "xpu" => Ok(Self::Xpu),
916            _ => Err(format!("Unknown PyTorch backend: {s}")),
917        }
918    }
919}
920
921/// Linux CUDA driver versions and the corresponding CUDA versions.
922///
923/// See: <https://github.com/pmeier/light-the-torch/blob/33397cbe45d07b51ad8ee76b004571a4c236e37f/light_the_torch/_cb.py#L150-L213>
924static LINUX_CUDA_DRIVERS: LazyLock<[(TorchBackend, Version); 26]> = LazyLock::new(|| {
925    [
926        // Table 2 from
927        // https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html
928        (TorchBackend::Cu130, Version::new([580])),
929        (TorchBackend::Cu129, Version::new([525, 60, 13])),
930        (TorchBackend::Cu128, Version::new([525, 60, 13])),
931        (TorchBackend::Cu126, Version::new([525, 60, 13])),
932        (TorchBackend::Cu125, Version::new([525, 60, 13])),
933        (TorchBackend::Cu124, Version::new([525, 60, 13])),
934        (TorchBackend::Cu123, Version::new([525, 60, 13])),
935        (TorchBackend::Cu122, Version::new([525, 60, 13])),
936        (TorchBackend::Cu121, Version::new([525, 60, 13])),
937        (TorchBackend::Cu120, Version::new([525, 60, 13])),
938        // Table 2 from
939        // https://docs.nvidia.com/cuda/archive/11.8.0/cuda-toolkit-release-notes/index.html
940        (TorchBackend::Cu118, Version::new([450, 80, 2])),
941        (TorchBackend::Cu117, Version::new([450, 80, 2])),
942        (TorchBackend::Cu116, Version::new([450, 80, 2])),
943        (TorchBackend::Cu115, Version::new([450, 80, 2])),
944        (TorchBackend::Cu114, Version::new([450, 80, 2])),
945        (TorchBackend::Cu113, Version::new([450, 80, 2])),
946        (TorchBackend::Cu112, Version::new([450, 80, 2])),
947        (TorchBackend::Cu111, Version::new([450, 80, 2])),
948        (TorchBackend::Cu110, Version::new([450, 36, 6])),
949        // Table 1 from
950        // https://docs.nvidia.com/cuda/archive/10.2/cuda-toolkit-release-notes/index.html
951        (TorchBackend::Cu102, Version::new([440, 33])),
952        (TorchBackend::Cu101, Version::new([418, 39])),
953        (TorchBackend::Cu100, Version::new([410, 48])),
954        (TorchBackend::Cu92, Version::new([396, 26])),
955        (TorchBackend::Cu91, Version::new([390, 46])),
956        (TorchBackend::Cu90, Version::new([384, 81])),
957        (TorchBackend::Cu80, Version::new([375, 26])),
958    ]
959});
960
961/// Windows CUDA driver versions and the corresponding CUDA versions.
962///
963/// See: <https://github.com/pmeier/light-the-torch/blob/33397cbe45d07b51ad8ee76b004571a4c236e37f/light_the_torch/_cb.py#L150-L213>
964static WINDOWS_CUDA_VERSIONS: LazyLock<[(TorchBackend, Version); 26]> = LazyLock::new(|| {
965    [
966        // Table 2 from
967        // https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html
968        (TorchBackend::Cu130, Version::new([580])),
969        (TorchBackend::Cu129, Version::new([528, 33])),
970        (TorchBackend::Cu128, Version::new([528, 33])),
971        (TorchBackend::Cu126, Version::new([528, 33])),
972        (TorchBackend::Cu125, Version::new([528, 33])),
973        (TorchBackend::Cu124, Version::new([528, 33])),
974        (TorchBackend::Cu123, Version::new([528, 33])),
975        (TorchBackend::Cu122, Version::new([528, 33])),
976        (TorchBackend::Cu121, Version::new([528, 33])),
977        (TorchBackend::Cu120, Version::new([528, 33])),
978        // Table 2 from
979        // https://docs.nvidia.com/cuda/archive/11.8.0/cuda-toolkit-release-notes/index.html
980        (TorchBackend::Cu118, Version::new([452, 39])),
981        (TorchBackend::Cu117, Version::new([452, 39])),
982        (TorchBackend::Cu116, Version::new([452, 39])),
983        (TorchBackend::Cu115, Version::new([452, 39])),
984        (TorchBackend::Cu114, Version::new([452, 39])),
985        (TorchBackend::Cu113, Version::new([452, 39])),
986        (TorchBackend::Cu112, Version::new([452, 39])),
987        (TorchBackend::Cu111, Version::new([452, 39])),
988        (TorchBackend::Cu110, Version::new([451, 22])),
989        // Table 1 from
990        // https://docs.nvidia.com/cuda/archive/10.2/cuda-toolkit-release-notes/index.html
991        (TorchBackend::Cu102, Version::new([441, 22])),
992        (TorchBackend::Cu101, Version::new([418, 96])),
993        (TorchBackend::Cu100, Version::new([411, 31])),
994        (TorchBackend::Cu92, Version::new([398, 26])),
995        (TorchBackend::Cu91, Version::new([391, 29])),
996        (TorchBackend::Cu90, Version::new([385, 54])),
997        (TorchBackend::Cu80, Version::new([376, 51])),
998    ]
999});
1000
1001/// Linux AMD GPU architectures and the corresponding PyTorch backends.
1002///
1003/// These were inferred by running the following snippet for each ROCm version:
1004///
1005/// ```python
1006/// import torch
1007///
1008/// print(torch.cuda.get_arch_list())
1009/// ```
1010///
1011/// AMD also provides a compatibility matrix: <https://rocm.docs.amd.com/en/latest/compatibility/compatibility-matrix.html>;
1012/// however, this list includes a broader array of GPUs than those in the matrix.
1013static LINUX_AMD_GPU_DRIVERS: LazyLock<[(TorchBackend, AmdGpuArchitecture); 55]> =
1014    LazyLock::new(|| {
1015        [
1016            // ROCm 6.4
1017            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx900),
1018            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx906),
1019            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx908),
1020            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx90a),
1021            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx942),
1022            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx1030),
1023            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx1100),
1024            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx1101),
1025            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx1102),
1026            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx1200),
1027            (TorchBackend::Rocm64, AmdGpuArchitecture::Gfx1201),
1028            // ROCm 6.3
1029            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx900),
1030            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx906),
1031            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx908),
1032            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx90a),
1033            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx942),
1034            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1030),
1035            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1100),
1036            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1101),
1037            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1102),
1038            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1200),
1039            (TorchBackend::Rocm63, AmdGpuArchitecture::Gfx1201),
1040            // ROCm 6.2.4
1041            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx900),
1042            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx906),
1043            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx908),
1044            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx90a),
1045            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx942),
1046            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1030),
1047            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1100),
1048            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1101),
1049            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1102),
1050            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1200),
1051            (TorchBackend::Rocm624, AmdGpuArchitecture::Gfx1201),
1052            // ROCm 6.2
1053            (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx900),
1054            (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx906),
1055            (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx908),
1056            (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx90a),
1057            (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx1030),
1058            (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx1100),
1059            (TorchBackend::Rocm62, AmdGpuArchitecture::Gfx942),
1060            // ROCm 6.1
1061            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx900),
1062            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx906),
1063            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx908),
1064            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx90a),
1065            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx942),
1066            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx1030),
1067            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx1100),
1068            (TorchBackend::Rocm61, AmdGpuArchitecture::Gfx1101),
1069            // ROCm 6.0
1070            (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx900),
1071            (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx906),
1072            (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx908),
1073            (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx90a),
1074            (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx1030),
1075            (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx1100),
1076            (TorchBackend::Rocm60, AmdGpuArchitecture::Gfx942),
1077        ]
1078    });
1079
1080static PYTORCH_CPU_INDEX_URL: LazyLock<IndexUrl> =
1081    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cpu").unwrap());
1082static PYTORCH_CU130_INDEX_URL: LazyLock<IndexUrl> =
1083    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu130").unwrap());
1084static PYTORCH_CU129_INDEX_URL: LazyLock<IndexUrl> =
1085    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu129").unwrap());
1086static PYTORCH_CU128_INDEX_URL: LazyLock<IndexUrl> =
1087    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu128").unwrap());
1088static PYTORCH_CU126_INDEX_URL: LazyLock<IndexUrl> =
1089    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu126").unwrap());
1090static PYTORCH_CU125_INDEX_URL: LazyLock<IndexUrl> =
1091    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu125").unwrap());
1092static PYTORCH_CU124_INDEX_URL: LazyLock<IndexUrl> =
1093    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu124").unwrap());
1094static PYTORCH_CU123_INDEX_URL: LazyLock<IndexUrl> =
1095    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu123").unwrap());
1096static PYTORCH_CU122_INDEX_URL: LazyLock<IndexUrl> =
1097    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu122").unwrap());
1098static PYTORCH_CU121_INDEX_URL: LazyLock<IndexUrl> =
1099    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu121").unwrap());
1100static PYTORCH_CU120_INDEX_URL: LazyLock<IndexUrl> =
1101    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu120").unwrap());
1102static PYTORCH_CU118_INDEX_URL: LazyLock<IndexUrl> =
1103    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu118").unwrap());
1104static PYTORCH_CU117_INDEX_URL: LazyLock<IndexUrl> =
1105    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu117").unwrap());
1106static PYTORCH_CU116_INDEX_URL: LazyLock<IndexUrl> =
1107    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu116").unwrap());
1108static PYTORCH_CU115_INDEX_URL: LazyLock<IndexUrl> =
1109    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu115").unwrap());
1110static PYTORCH_CU114_INDEX_URL: LazyLock<IndexUrl> =
1111    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu114").unwrap());
1112static PYTORCH_CU113_INDEX_URL: LazyLock<IndexUrl> =
1113    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu113").unwrap());
1114static PYTORCH_CU112_INDEX_URL: LazyLock<IndexUrl> =
1115    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu112").unwrap());
1116static PYTORCH_CU111_INDEX_URL: LazyLock<IndexUrl> =
1117    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu111").unwrap());
1118static PYTORCH_CU110_INDEX_URL: LazyLock<IndexUrl> =
1119    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu110").unwrap());
1120static PYTORCH_CU102_INDEX_URL: LazyLock<IndexUrl> =
1121    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu102").unwrap());
1122static PYTORCH_CU101_INDEX_URL: LazyLock<IndexUrl> =
1123    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu101").unwrap());
1124static PYTORCH_CU100_INDEX_URL: LazyLock<IndexUrl> =
1125    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu100").unwrap());
1126static PYTORCH_CU92_INDEX_URL: LazyLock<IndexUrl> =
1127    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu92").unwrap());
1128static PYTORCH_CU91_INDEX_URL: LazyLock<IndexUrl> =
1129    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu91").unwrap());
1130static PYTORCH_CU90_INDEX_URL: LazyLock<IndexUrl> =
1131    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu90").unwrap());
1132static PYTORCH_CU80_INDEX_URL: LazyLock<IndexUrl> =
1133    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/cu80").unwrap());
1134static PYTORCH_ROCM64_INDEX_URL: LazyLock<IndexUrl> =
1135    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.4").unwrap());
1136static PYTORCH_ROCM63_INDEX_URL: LazyLock<IndexUrl> =
1137    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.3").unwrap());
1138static PYTORCH_ROCM624_INDEX_URL: LazyLock<IndexUrl> =
1139    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.2.4").unwrap());
1140static PYTORCH_ROCM62_INDEX_URL: LazyLock<IndexUrl> =
1141    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.2").unwrap());
1142static PYTORCH_ROCM61_INDEX_URL: LazyLock<IndexUrl> =
1143    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.1").unwrap());
1144static PYTORCH_ROCM60_INDEX_URL: LazyLock<IndexUrl> =
1145    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm6.0").unwrap());
1146static PYTORCH_ROCM57_INDEX_URL: LazyLock<IndexUrl> =
1147    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.7").unwrap());
1148static PYTORCH_ROCM56_INDEX_URL: LazyLock<IndexUrl> =
1149    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.6").unwrap());
1150static PYTORCH_ROCM55_INDEX_URL: LazyLock<IndexUrl> =
1151    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.5").unwrap());
1152static PYTORCH_ROCM542_INDEX_URL: LazyLock<IndexUrl> =
1153    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.4.2").unwrap());
1154static PYTORCH_ROCM54_INDEX_URL: LazyLock<IndexUrl> =
1155    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.4").unwrap());
1156static PYTORCH_ROCM53_INDEX_URL: LazyLock<IndexUrl> =
1157    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.3").unwrap());
1158static PYTORCH_ROCM52_INDEX_URL: LazyLock<IndexUrl> =
1159    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.2").unwrap());
1160static PYTORCH_ROCM511_INDEX_URL: LazyLock<IndexUrl> =
1161    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm5.1.1").unwrap());
1162static PYTORCH_ROCM42_INDEX_URL: LazyLock<IndexUrl> =
1163    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.2").unwrap());
1164static PYTORCH_ROCM41_INDEX_URL: LazyLock<IndexUrl> =
1165    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.1").unwrap());
1166static PYTORCH_ROCM401_INDEX_URL: LazyLock<IndexUrl> =
1167    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/rocm4.0.1").unwrap());
1168static PYTORCH_XPU_INDEX_URL: LazyLock<IndexUrl> =
1169    LazyLock::new(|| IndexUrl::from_str("https://download.pytorch.org/whl/xpu").unwrap());
1170
1171static PYX_API_BASE_URL: LazyLock<Cow<'static, str>> = LazyLock::new(|| {
1172    std::env::var(EnvVars::PYX_API_URL)
1173        .map(Cow::Owned)
1174        .unwrap_or(Cow::Borrowed("https://api.pyx.dev"))
1175});
1176static PYX_CPU_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1177    let api_base_url = &*PYX_API_BASE_URL;
1178    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cpu")).unwrap()
1179});
1180static PYX_CU130_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1181    let api_base_url = &*PYX_API_BASE_URL;
1182    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu130")).unwrap()
1183});
1184static PYX_CU129_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1185    let api_base_url = &*PYX_API_BASE_URL;
1186    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu129")).unwrap()
1187});
1188static PYX_CU128_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1189    let api_base_url = &*PYX_API_BASE_URL;
1190    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu128")).unwrap()
1191});
1192static PYX_CU126_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1193    let api_base_url = &*PYX_API_BASE_URL;
1194    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu126")).unwrap()
1195});
1196static PYX_CU125_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1197    let api_base_url = &*PYX_API_BASE_URL;
1198    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu125")).unwrap()
1199});
1200static PYX_CU124_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1201    let api_base_url = &*PYX_API_BASE_URL;
1202    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu124")).unwrap()
1203});
1204static PYX_CU123_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1205    let api_base_url = &*PYX_API_BASE_URL;
1206    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu123")).unwrap()
1207});
1208static PYX_CU122_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1209    let api_base_url = &*PYX_API_BASE_URL;
1210    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu122")).unwrap()
1211});
1212static PYX_CU121_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1213    let api_base_url = &*PYX_API_BASE_URL;
1214    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu121")).unwrap()
1215});
1216static PYX_CU120_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1217    let api_base_url = &*PYX_API_BASE_URL;
1218    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu120")).unwrap()
1219});
1220static PYX_CU118_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1221    let api_base_url = &*PYX_API_BASE_URL;
1222    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu118")).unwrap()
1223});
1224static PYX_CU117_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1225    let api_base_url = &*PYX_API_BASE_URL;
1226    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu117")).unwrap()
1227});
1228static PYX_CU116_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1229    let api_base_url = &*PYX_API_BASE_URL;
1230    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu116")).unwrap()
1231});
1232static PYX_CU115_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1233    let api_base_url = &*PYX_API_BASE_URL;
1234    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu115")).unwrap()
1235});
1236static PYX_CU114_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1237    let api_base_url = &*PYX_API_BASE_URL;
1238    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu114")).unwrap()
1239});
1240static PYX_CU113_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1241    let api_base_url = &*PYX_API_BASE_URL;
1242    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu113")).unwrap()
1243});
1244static PYX_CU112_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1245    let api_base_url = &*PYX_API_BASE_URL;
1246    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu112")).unwrap()
1247});
1248static PYX_CU111_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1249    let api_base_url = &*PYX_API_BASE_URL;
1250    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu111")).unwrap()
1251});
1252static PYX_CU110_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1253    let api_base_url = &*PYX_API_BASE_URL;
1254    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu110")).unwrap()
1255});
1256static PYX_CU102_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1257    let api_base_url = &*PYX_API_BASE_URL;
1258    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu102")).unwrap()
1259});
1260static PYX_CU101_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1261    let api_base_url = &*PYX_API_BASE_URL;
1262    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu101")).unwrap()
1263});
1264static PYX_CU100_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1265    let api_base_url = &*PYX_API_BASE_URL;
1266    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu100")).unwrap()
1267});
1268static PYX_CU92_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1269    let api_base_url = &*PYX_API_BASE_URL;
1270    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu92")).unwrap()
1271});
1272static PYX_CU91_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1273    let api_base_url = &*PYX_API_BASE_URL;
1274    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu91")).unwrap()
1275});
1276static PYX_CU90_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1277    let api_base_url = &*PYX_API_BASE_URL;
1278    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu90")).unwrap()
1279});
1280static PYX_CU80_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1281    let api_base_url = &*PYX_API_BASE_URL;
1282    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/cu80")).unwrap()
1283});
1284static PYX_ROCM64_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1285    let api_base_url = &*PYX_API_BASE_URL;
1286    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm6.4")).unwrap()
1287});
1288static PYX_ROCM63_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1289    let api_base_url = &*PYX_API_BASE_URL;
1290    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm6.3")).unwrap()
1291});
1292static PYX_ROCM624_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1293    let api_base_url = &*PYX_API_BASE_URL;
1294    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm6.2.4")).unwrap()
1295});
1296static PYX_ROCM62_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1297    let api_base_url = &*PYX_API_BASE_URL;
1298    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm6.2")).unwrap()
1299});
1300static PYX_ROCM61_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1301    let api_base_url = &*PYX_API_BASE_URL;
1302    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm6.1")).unwrap()
1303});
1304static PYX_ROCM60_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1305    let api_base_url = &*PYX_API_BASE_URL;
1306    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm6.0")).unwrap()
1307});
1308static PYX_ROCM57_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1309    let api_base_url = &*PYX_API_BASE_URL;
1310    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.7")).unwrap()
1311});
1312static PYX_ROCM56_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1313    let api_base_url = &*PYX_API_BASE_URL;
1314    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.6")).unwrap()
1315});
1316static PYX_ROCM55_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1317    let api_base_url = &*PYX_API_BASE_URL;
1318    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.5")).unwrap()
1319});
1320static PYX_ROCM542_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1321    let api_base_url = &*PYX_API_BASE_URL;
1322    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.4.2")).unwrap()
1323});
1324static PYX_ROCM54_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1325    let api_base_url = &*PYX_API_BASE_URL;
1326    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.4")).unwrap()
1327});
1328static PYX_ROCM53_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1329    let api_base_url = &*PYX_API_BASE_URL;
1330    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.3")).unwrap()
1331});
1332static PYX_ROCM52_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1333    let api_base_url = &*PYX_API_BASE_URL;
1334    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.2")).unwrap()
1335});
1336static PYX_ROCM511_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1337    let api_base_url = &*PYX_API_BASE_URL;
1338    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm5.1.1")).unwrap()
1339});
1340static PYX_ROCM42_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1341    let api_base_url = &*PYX_API_BASE_URL;
1342    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm4.2")).unwrap()
1343});
1344static PYX_ROCM41_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1345    let api_base_url = &*PYX_API_BASE_URL;
1346    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm4.1")).unwrap()
1347});
1348static PYX_ROCM401_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1349    let api_base_url = &*PYX_API_BASE_URL;
1350    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/rocm4.0.1")).unwrap()
1351});
1352static PYX_XPU_INDEX_URL: LazyLock<IndexUrl> = LazyLock::new(|| {
1353    let api_base_url = &*PYX_API_BASE_URL;
1354    IndexUrl::from_str(&format!("{api_base_url}/simple/astral-sh/xpu")).unwrap()
1355});