Het artikel beschrijft de implementatie van deduplicatie op bestandsniveau binnen de wheel-cache. Waar voorheen alleen op het niveau van de volledige 'wheel' werd gecached, worden bestanden nu opgeslagen onder hun BLAKE3-hash in een files-v0 bucket en via hardlinks geplaatst.
Belangrijkste punten:
- Opslagwinst: In tests resulteerde dit in een besparing van ongeveer 545,2 MiB, wat circa 10% van de totale cache beslaat.
- Prestaties: Er is een minimale vertraging van minder dan 4% bij cold installs, terwijl warm installs geen impact ondervinden.
- Buffer-optimalisatie: Door het hergebruik van één buffer van 64 KiB voor de gehele wheel (in plaats van per bestand) is het aantal allocaties drastisch verminderd, wat de snelheid bij cold installs weer ten goede komt.
- Alternatieven: Diverse andere methoden, zoals het opslaan van bestanden als non-executable of pre-fetching van de central directory, zijn onderzocht maar afgewezen vanwege lagere betrouwbaarheid of extra overhead.
Deduplicatie van alle bestanden in de wheel-cache
Samenvatting
Deze wijziging introduceert deduplicatie op bestandsniveau: elk bestand wordt nu opgeslagen onder zijn BLAKE3-hash in een files-v0 bucket. Deze objecten worden via hardlinks geplaatst op hun oorspronkelijke locaties in archive-v0. Hierdoor verandert de installatiestap niet; de deduplicatie vindt uitsluitend plaats binnen de cache. Bij het opschonen van de cache worden bestandsobjecten verwijderd zodra hun hardlink count tot één daalt.
Impact op opslagruimte
Uit een voorstel (#19694) blijkt de potentiële winst in opslagruimte op basis van verschillende bestandsselecties:
| Bestandsselectie | Extra besparing | Gehardlinkte bestanden | Unieke files-v0 objecten |
| Executables en native libraries | 275,7 MiB | 3.336 | 3.088 |
| Elk payload-bestand $\ge$ 10 MiB | 235,0 MiB | 80 | 78 |
| Elk payload-bestand $\ge$ 1 MiB | 279,7 MiB | 373 | 349 |
| Elk payload-bestand $\ge$ 100 KiB | 353,4 MiB | 2.830 | 2.458 |
| Elk payload-bestand $\ge$ 10 KiB | 475,7 MiB | 23.767 | 18.722 |
| Elk payload-bestand $\ge$ 1 KiB | 537,4 MiB | 95.156 | 66.422 |
| Alle payload-bestanden | 545,2 MiB | 134.222 | 87.129 |
Op een lokale machine resulteert dit in een besparing van 545,2 MiB, wat ongeveer 10% van de cache is. De tegenprestatie is een vertraging van minder dan 4% bij cold installs (installaties waarbij de cache leeg is), terwijl warm installs (installaties vanuit de cache) niet worden beïnvloed.
Benchmarkresultaten
De optimalisatie is gebenchmarkt tegenover de binary-only cache, waarbij --preview-features content-addressed-cache is ingeschakeld. Onderstaande waarden zijn medianen voor het volledige uv pip install proces; positieve waarden duiden op een vertraging.
| Wheel | Cache | Parent mediaan | Geoptimaliseerde mediaan | Wijziging (95% CI) |
| AnyIO 4.9.0 | cold | 90,4 ms | 93,4 ms | +3,39% (+2,87% tot +3,86%) |
| AnyIO 4.9.0 | warm | 29,9 ms | 30,3 ms | +1,14% (-1,08% tot +3,85%) |
| SymPy 1.14.0 | cold | 367,1 ms | 381,6 ms | +3,95% (+3,49% tot +4,46%) |
| SymPy 1.14.0 | warm | 84,6 ms | 84,0 ms | -0,64% (-1,04% tot -0,11%) |
| NumPy 2.2.6 | cold | 314,1 ms | 326,5 ms | +3,95% (+3,24% tot +4,82%) |
| NumPy 2.2.6 | warm | 62,5 ms | 62,7 ms | +0,28% (-0,55% tot +1,25%) |
| PyTorch 2.7.1+cpu | cold | 2982,6 ms | 3072,9 ms | +3,03% (+0,98% tot +5,18%) |
| PyTorch 2.7.1+cpu | warm | 404,0 ms | 403,1 ms | -0,21% (-0,72% tot +0,11%) |
Technische details van de benchmarks
- Omgeving: Linux/ext4, AMD EPYC-Milan VM (8 CPU's), Python 3.12.13.
- Build: Rust 1.98.0, geoptimaliseerd profiel zonder LTO.
- Methode: Installatie van één lokale wheel offline, zonder afhankelijkheden of bytecode-compilatie, gebruikmakend van hardlinks naar een nieuwe virtuele omgeving.
- Definities: Cold verwijdert de gehele uv-cache; warm behoudt een voorbereide cache. Setup en cleanup zijn niet meegeteld.
Optimalisatie van buffergebruik (#21340)
Wanneer content hashing is ingeschakeld, wordt er momenteel een nieuwe buffer van 64 KiB gealloceerd en genuld voor elk bestand dat tijdens streaming extraction wordt gekopieerd en gehasht. In PR #21340 wordt één buffer hergebruikt voor de gehele wheel.
Bij de PyTorch-wheel vermindert dit het aantal buffer-allocaties voor hashing van 11.120 naar één, terwijl de buffersize op 64 KiB per actieve wheel blijft.
Resultaten van buffer-hergebruik
De volgende metingen vergelijken de initiële wijziging (#21327) met de toegevoegde buffer-optimalisatie bij cold installs:
| Package | #21327 | #21327 + buffer reuse | Wijziging |
| AnyIO | 110 ms | 107 ms | -2,6% |
| SymPy | 845 ms | 775 ms | -8,3% |
| NumPy | 627 ms | 567 ms | -9,5% |
| PyTorch CPU | 6,50 s | 5,99 s | -7,8% |
| 14-package env (concurrency 4) | 6,95 s | 6,47 s | -7,0% |
Geëvalueerde alternatieven
Om de prestaties verder te verbeteren, zijn de volgende alternatieven onderzocht:
- Bestanden opslaan als non-executable: Door executable-flags pas tijdens de installatie toe te passen, konden bestanden tijdens het unzippen direct naar
files-v0 worden gehardlinkt. Echter, omdat hardlinks dezelfde modus delen, moeten alle executables alsnog gekopieerd worden. Dit bleek uiteindelijk trager.
- Pre-fetching van de central directory: Hiermee kon worden bepaald of een bestand executable is op het moment van extractie (momenteel is dit pas bekend ná het unzippen). Dit was trager vanwege de extra overhead van meer HTTP-verzoeken.
- Heuristieken voor executables: Er is overwogen om te "gokken" of een bestand executable is tijdens extractie en dit indien nodig achteraf te corrigeren. Deze aanpak is verworpen vanwege ontevredenheid over de betrouwbaarheid van de heuristieken.
Concluderend wordt gesteld dat de huidige implementatie, zeker in combinatie met de buffer-optimalisatie uit #21340, een goede balans biedt tussen opslagbesparing en snelheid.
Deduplicatie van alle bestanden in de wheel-cache
Samenvatting
Deze wijziging introduceert deduplicatie op bestandsniveau: elk bestand wordt nu opgeslagen onder zijn BLAKE3-hash in een files-v0 bucket. Deze objecten worden via hardlinks geplaatst op hun oorspronkelijke locaties in archive-v0. Hierdoor verandert de installatiestap niet; de deduplicatie vindt uitsluitend plaats binnen de cache. Bij het opschonen van de cache worden bestandsobjecten verwijderd zodra hun hardlink count tot één daalt.
Impact op opslagruimte
Uit een voorstel (#19694) blijkt de potentiële winst in opslagruimte op basis van verschillende bestandsselecties:
| Bestandsselectie | Extra besparing | Gehardlinkte bestanden | Unieke files-v0 objecten |
| Executables en native libraries | 275,7 MiB | 3.336 | 3.088 |
| Elk payload-bestand $\ge$ 10 MiB | 235,0 MiB | 80 | 78 |
| Elk payload-bestand $\ge$ 1 MiB | 279,7 MiB | 373 | 349 |
| Elk payload-bestand $\ge$ 100 KiB | 353,4 MiB | 2.830 | 2.458 |
| Elk payload-bestand $\ge$ 10 KiB | 475,7 MiB | 23.767 | 18.722 |
| Elk payload-bestand $\ge$ 1 KiB | 537,4 MiB | 95.156 | 66.422 |
| Alle payload-bestanden | 545,2 MiB | 134.222 | 87.129 |
Op een lokale machine resulteert dit in een besparing van 545,2 MiB, wat ongeveer 10% van de cache is. De tegenprestatie is een vertraging van minder dan 4% bij cold installs (installaties waarbij de cache leeg is), terwijl warm installs (installaties vanuit de cache) niet worden beïnvloed.
Benchmarkresultaten
De optimalisatie is gebenchmarkt tegenover de binary-only cache, waarbij --preview-features content-addressed-cache is ingeschakeld. Onderstaande waarden zijn medianen voor het volledige uv pip install proces; positieve waarden duiden op een vertraging.
| Wheel | Cache | Parent mediaan | Geoptimaliseerde mediaan | Wijziging (95% CI) |
| AnyIO 4.9.0 | cold | 90,4 ms | 93,4 ms | +3,39% (+2,87% tot +3,86%) |
| AnyIO 4.9.0 | warm | 29,9 ms | 30,3 ms | +1,14% (-1,08% tot +3,85%) |
| SymPy 1.14.0 | cold | 367,1 ms | 381,6 ms | +3,95% (+3,49% tot +4,46%) |
| SymPy 1.14.0 | warm | 84,6 ms | 84,0 ms | -0,64% (-1,04% tot -0,11%) |
| NumPy 2.2.6 | cold | 314,1 ms | 326,5 ms | +3,95% (+3,24% tot +4,82%) |
| NumPy 2.2.6 | warm | 62,5 ms | 62,7 ms | +0,28% (-0,55% tot +1,25%) |
| PyTorch 2.7.1+cpu | cold | 2982,6 ms | 3072,9 ms | +3,03% (+0,98% tot +5,18%) |
| PyTorch 2.7.1+cpu | warm | 404,0 ms | 403,1 ms | -0,21% (-0,72% tot +0,11%) |
Technische details van de benchmarks
- Omgeving: Linux/ext4, AMD EPYC-Milan VM (8 CPU's), Python 3.12.13.
- Build: Rust 1.98.0, geoptimaliseerd profiel zonder LTO.
- Methode: Installatie van één lokale wheel offline, zonder afhankelijkheden of bytecode-compilatie, gebruikmakend van hardlinks naar een nieuwe virtuele omgeving.
- Definities: Cold verwijdert de gehele uv-cache; warm behoudt een voorbereide cache. Setup en cleanup zijn niet meegeteld.
Optimalisatie van buffergebruik (#21340)
Wanneer content hashing is ingeschakeld, wordt er momenteel een nieuwe buffer van 64 KiB gealloceerd en genuld voor elk bestand dat tijdens streaming extraction wordt gekopieerd en gehasht. In PR #21340 wordt één buffer hergebruikt voor de gehele wheel.
Bij de PyTorch-wheel vermindert dit het aantal buffer-allocaties voor hashing van 11.120 naar één, terwijl de buffersize op 64 KiB per actieve wheel blijft.
Resultaten van buffer-hergebruik
De volgende metingen vergelijken de initiële wijziging (#21327) met de toegevoegde buffer-optimalisatie bij cold installs:
| Package | #21327 | #21327 + buffer reuse | Wijziging |
| AnyIO | 110 ms | 107 ms | -2,6% |
| SymPy | 845 ms | 775 ms | -8,3% |
| NumPy | 627 ms | 567 ms | -9,5% |
| PyTorch CPU | 6,50 s | 5,99 s | -7,8% |
| 14-package env (concurrency 4) | 6,95 s | 6,47 s | -7,0% |
Geëvalueerde alternatieven
Om de prestaties verder te verbeteren, zijn de volgende alternatieven onderzocht:
- Bestanden opslaan als non-executable: Door executable-flags pas tijdens de installatie toe te passen, konden bestanden tijdens het unzippen direct naar
files-v0 worden gehardlinkt. Echter, omdat hardlinks dezelfde modus delen, moeten alle executables alsnog gekopieerd worden. Dit bleek uiteindelijk trager.
- Pre-fetching van de central directory: Hiermee kon worden bepaald of een bestand executable is op het moment van extractie (momenteel is dit pas bekend ná het unzippen). Dit was trager vanwege de extra overhead van meer HTTP-verzoeken.
- Heuristieken voor executables: Er is overwogen om te "gokken" of een bestand executable is tijdens extractie en dit indien nodig achteraf te corrigeren. Deze aanpak is verworpen vanwege ontevredenheid over de betrouwbaarheid van de heuristieken.
Concluderend wordt gesteld dat de huidige implementatie, zeker in combinatie met de buffer-optimalisatie uit #21340, een goede balans biedt tussen opslagbesparing en snelheid.