Dlaczego FLUX.2-dev GGUF Q4_K_M zużywa 29 GB pamięci VRAM na Apple Silicon?
Model FLUX.2-dev w wersji skwantyzowanej GGUF Q4_K_M powinien teoretycznie zajmować około 19 GB pamięci, ale w praktyce na Apple Silicon z chipem M3 Pro osiąga szczytowy poziom 28,92 GB. Analiza wykazuje, że dodatkowe gigabajty są zużywane przez narzut MPS, dekodowanie wag oraz przesyłanie danych między CPU a GPU w jednolitej pamięci.
Architektura ładowania i kolejne wyładowanie
FLUX.2-dev używa tekstowego enkodera Mistral-Small-3.2-24B, który w pełnej wersji wymaga 45 GB pamięci VRAM. W przypadku Apple Silicon stosowana jest wersja 4-bitowa poprzez MLX, zużywająca 16 GB. Proces działa jako kolejne wyładowanie:
- Załadowanie enkodera MLX (~13 GB).
- Kodowanie promptu do embeddings na CPU.
- Wyładowanie enkodera z wyczyszczeniem cache.
- Załadowanie modelu DiT w formacie GGUF Q4_K_M (~19 GB na dysku).
- Inicjalizacja VAE i schedulera na CPU.
- Przeniesienie całego pipeline’a na MPS.
Teoretyczny szczyt to maksimum rozmiarów komponentów, ale rzeczywistość różni się z powodu specyfiki jednolitej pamięci.
Środowisko testowe
- macOS: 26.3.1 (Tahoe)
- Chip: Apple M3 Pro, 36 GB jednolitej pamięci
- Python: 3.14.3
- PyTorch: 2.11.0
- diffusers: 0.38.0.dev
- mlx-lm: 0.31.1
Profiling prowadzony był za pomocą phys_footprint (analog metryki Activity Monitor) oraz metryk MPS w PyTorch. psutil.rss nie uwzględnia alokacji MPS.
Szczegółowa analiza zużycia pamięci
Pomiar etapowy
| Etap | phys_footprint | MPS driver | MPS current |
|------|----------------|------------|-------------|
| Podstawa | 0,27 GB | 0 | 0 |
| Enkoder MLX załadowany | 13,22 GB | 0 | 0 |
| GGUF na CPU | 19,23 GB | 0 | 0 |
| pipe.to(mps) | 23,79 GB | 20,06 GB | 18,75 GB |
| Pośrodku inferencji | 26,50 GB | 25,62 GB | 18,78 GB |
Szczyt 28,92 GB pojawia się podczas pipe.to("mps"), ponieważ jednocześnie istnieją kopie danych na CPU i MPS.
Anatomia 28 GB
- DiT Q4_K_M (uint8): 18,59 GB
- VAE (bf16): 0,16 GB
- Narzut puli MPS: +1,31 GB
- IOKit/Metal graphCache: ~3,73 GB (shader'y, tabele stron, limbo buffers)
Wagi GGUF pozostają w formacie uint8 na MPS, dekodowanie do bf16 odbywa się w trakcie forward pass po warstwach. Cache tymczasowych tensorów bf16 (do 6,92 GB) gromadzi się stopniowo.
Optymalizacje zmniejszające szczyt
- Krokowe przenoszenie na MPS: Przenoszenie parametrów krok po kroku z
gc.collect()co 30 parametrów.
def _move_to_device_incremental(pipe, device):
n = 0
for attr in list(vars(pipe)):
comp = getattr(pipe, attr, None)
if not isinstance(comp, torch.nn.Module):
continue
for param in comp.parameters():
param.data = param.data.to(device)
n += 1
if n % 30 == 0:
gc.collect()
for buf in comp.buffers():
buf.data = buf.data.to(device)
gc.collect()
torch.mps.synchronize()
torch.mps.empty_cache()
- empty_cache() między krokami: Callback w trakcie inferencji z
torch.mps.empty_cache()do wyczyszczenia cache dekodowania.
- Flush po generacji: Po zakończeniu inferencji —
gc.collect()+torch.mps.empty_cache().
Wyniki optymalizacji
| Metryka | Przed | Po | Delta |
|---------|-------|----|-------|
| Szczyt phys_footprint | 28,92 GB | 27,36 GB | -1,56 GB |
| Stała po inferencji | 26,25 GB | 21,76 GB | -4,49 GB |
| Czas generacji (1 krok) | 2:04 | 2:11 | +7s |
Szczyt spada dzięki inkrementalnemu GC podczas ładowania, stan stały dzięki flush’owi cache. Nieoptymalizowalny narzut MPS/IOKit wynosi ok. 3,5 GB.
Co ważne:
- Minimalny stan stały: ~24 GB, szczyt inferencji ~27 GB.
- Główny koszt: 18,59 GB na wagach uint8 DiT + 3,5 GB narzutu MPS.
- Dekodowanie do bf16 dodaje 3–5 GB buforów tymczasowych.
- Przesyłanie z CPU do MPS podwaja szczyt ze względu na jednolitą pamięć.
- Optymalizacje dają -1,5 GB szczytu i -4,5 GB stanu stałego bez utraty jakości.
Rzeczywisty minimum dla FLUX.2-dev GGUF na M3 Pro to 24+ GB jednolitej pamięci. Dla mniejszych ilości potrzebna jest bardziej agresywna kwantyzacja lub wyładowanie na CPU.
— Editorial Team
Brak komentarzy.