- BrainTools - https://www.braintools.ru -

Stable Diffusion 3.5 medium на Apple M1 16Gb

В этой статье, про ИИ, написанной не полностью ИИ, про генерацию изображений – не будет изображений.

В конце этой статьи мы будем запускать эту модель на указанном чипе, но начнем мы с чуть более мощного – он понадобиться чтобы разобраться с проблемой.

Первая проблема с которой я столкнулся – это потребление памяти [1]. Поиски в интернете, описание самой модели говорили о том что она должна помещаться в ~10GB VRAM. Чего должно с запасом хватать для Apple M1 16GB. Однако фактическое зафиксированное потребление памяти составило 21 GB, не зафиксированное 28 GB (после чего я и начал исследование).

На этом моменте мы вынуждены переместиться на оборудование помощнее, например ноутбук с чипом Apple M3 Pro 36GB для исследования проблемы.

Теперь немного исходных технических данных об используемых версиях софта:

Пакет

Версия

macOS

26.3 (Tahoe)

Python

3.14

PyTorch

2.11.0

diffusers

0.38.0.dev0

accelerate

1.13.0

transformers

5.3.0

safetensors

0.7.0

SD 3.5 Medium на Apple Silicon: с 21 ГБ до 11.6 ГБ пиковой памяти

SD 3.5 Medium позиционируется как модель на 2.5B параметров, которой нужно ~5 ГБ VRAM. На практике на Apple Silicon MPS она потребляет 20+ ГБ. Разбираемся почему.

Проблема

Запуск SD 3.5 Medium fp16 на M3 Pro (36 ГБ unified memory):

pipe = SD3Pipeline.from_pretrained(path, torch_dtype=torch.float16, variant="fp16")
pipe.to("mps")

Модель Stable Diffusion Medium состоит из следующих компонентов:

Компонент

Параметры

Размер fp16

T5-XXL текстовый энкодер

5.5B

10.75 ГБ

DiT-трансформер

2.1B

4.18 ГБ

VAE

0.16 ГБ

Итого веса

15.09 ГБ

T5-XXL занимает 71% всей памяти — в 2.5 раза больше самой модели генерации.

Запускаем:

from diffusers import StableDiffusion3Pipeline
import torch

pipe = StableDiffusion3Pipeline.from_pretrained(
    "stabilityai/stable-diffusion-3.5-medium",
    torch_dtype=torch.float16, variant="fp16")
pipe.to("mps")

image = pipe("a warrior with a sword", num_inference_steps=40).images[0]
image.save("warrior.png")

Пиковое потребление: 21.4 ГБ. Без разницы, steps=1 или steps=40.

Пик в 21 ГБ складывается из двух проблем:

  1. Дублирование CPU→MPS. from_pretrained грузит все веса на CPU, затем .to("mps") создаёт MPS-копии. Во время переноса обе копии существуют одновременно: T5 на MPS (11 ГБ) + DiT/VAE на CPU (4.3 ГБ) + overhead MPS = 20+ ГБ пик.

  2. enable_model_cpu_offload() не помогает на Apple Silicon. CPU и MPS разделяют одну физическую RAM — перемещение тензоров между ними не освобождает память.

Решение: Загружаем Decoder и DiT раздельно

Загружаем Decoder T5 и DiT раздельно. Теперь пик потребления памяти – приходится на T5 так эта модель больше, чем сам генератор изображений. Но есть и тонкости:

1. Загрузка с device_map=”balanced” — без CPU-копии

# Было: пик ~15 ГБ (CPU- и MPS-копии сосуществуют)
pipe = SD3Pipeline.from_pretrained(path, torch_dtype=torch.float16)
pipe.to("mps")

# Стало: пик ~11 ГБ (загрузка напрямую в MPS через accelerate)
pipe = SD3Pipeline.from_pretrained(path, torch_dtype=torch.float16,
                                    device_map="balanced")

2. Удаление хуков accelerate перед освобождением модели

Неочевидный момент. device_map использует dispatch-хуки accelerate, которые держат strong references на тензоры. del pipe + gc.collect() + empty_cache() сами по себе не освобождают в этой ситуации память.

from accelerate.hooks import remove_hook_from_submodules

for attr in list(vars(pipe)):
    comp = getattr(pipe, attr, None)
    if isinstance(comp, torch.nn.Module):
        remove_hook_from_submodules(comp)
        setattr(pipe, attr, None)

del pipe
gc.collect()
torch.mps.synchronize()
torch.mps.empty_cache()

Без этого T5 остаётся в памяти (cur=10.75 ГБ) даже после «выгрузки», и DiT грузится поверх — воспроизводя пик в 21 ГБ.

3. Полная последовательность: encode → unload → generate

# Фаза 1: только T5-энкодер
enc_pipe = SD3Pipeline.from_pretrained(path,
    transformer=None, vae=None,
    text_encoder=None, text_encoder_2=None,  # без CLIP
    tokenizer=None, tokenizer_2=None,
    variant="fp16", torch_dtype=torch.float16,
    device_map="balanced")

prompt_embeds = enc_pipe.text_encoder_3(tokens)[0].cpu()

# Выгрузка T5 (с удалением хуков из шага 2)
# ...

# Фаза 2: только DiT + VAE — используем .to("mps"), не device_map
gen_pipe = SD3Pipeline.from_pretrained(path,
    text_encoder=None, text_encoder_2=None, text_encoder_3=None,
    tokenizer=None, tokenizer_2=None, tokenizer_3=None,
    variant="fp16", torch_dtype=torch.float16)
gen_pipe.to("mps")  # заставляет MPS-драйвер освободить закешированные страницы T5

gen_pipe(prompt_embeds=prompt_embeds.to("mps"), ...)

Для DiT намеренно используется .to("mps") — это заставляет MPS-драйвер переиспользовать кеш T5, сбрасывая footprint с 11.5 до 6 ГБ.

С этого момента можно возвращаться на чип M1 16Gb.

Результат

M3 Max 36 ГБ, SD 3.5 Medium fp16, 512×512:

Этап

До

После

Загрузка T5

15.3 ГБ

11.5 ГБ

T5 после выгрузки

cur=10.75 ГБ (утечка)

cur=0.01 ГБ

DiT + диффузия

8.9 ГБ

8.9 ГБ

Пик

21.4 ГБ

11.6 ГБ

Минимальный рабочий пример — пик 11.6 ГБ:

from diffusers import StableDiffusion3Pipeline
from accelerate.hooks import remove_hook_from_submodules
import torch, gc

path = "stabilityai/stable-diffusion-3.5-medium"

# ── Фаза 1: кодирование промпта (только T5-XXL, ~11 ГБ) ──

enc_pipe = StableDiffusion3Pipeline.from_pretrained(path,
    transformer=None, vae=None,                    # без DiT + VAE
    text_encoder=None, text_encoder_2=None,        # без CLIP
    tokenizer=None, tokenizer_2=None,
    variant="fp16", torch_dtype=torch.float16,
    device_map="balanced")                         # напрямую в MPS

prompt_embeds, neg_embeds, pooled, neg_pooled = enc_pipe.encode_prompt(
    prompt="a warrior with a sword",
    prompt_2="a warrior with a sword",
    prompt_3="a warrior with a sword",
    device="mps", num_images_per_prompt=1)

# ── Выгрузка T5 (обязательно снять хуки accelerate!) ──────

for attr in list(vars(enc_pipe)):
    comp = getattr(enc_pipe, attr, None)
    if isinstance(comp, torch.nn.Module):
        remove_hook_from_submodules(comp)
        setattr(enc_pipe, attr, None)
del enc_pipe
gc.collect()
torch.mps.synchronize()
torch.mps.empty_cache()

# ── Фаза 2: генерация (только DiT + VAE, ~8 ГБ) ──────────

gen_pipe = StableDiffusion3Pipeline.from_pretrained(path,
    text_encoder=None, text_encoder_2=None, text_encoder_3=None,
    tokenizer=None, tokenizer_2=None, tokenizer_3=None,
    variant="fp16", torch_dtype=torch.float16)
gen_pipe.to("mps")  # рекле́ймит кеш T5 из MPS-драйвера

image = gen_pipe(
    prompt_embeds=prompt_embeds,
    negative_prompt_embeds=neg_embeds,
    pooled_prompt_embeds=pooled,
    negative_pooled_prompt_embeds=neg_pooled,
    num_inference_steps=40).images[0]
image.save("warrior.png")

Статистика и производительность

Генерация одной картинки:

Apple M1 16GB: ~5 минут | 11 GB – 13 GB VRAM

Apple M3 Pro 36B: ~30 секунд | 9 GB – 11 GB VRAM

Автор: ComputerPers

Источник [2]


Сайт-источник BrainTools: https://www.braintools.ru

Путь до страницы источника: https://www.braintools.ru/article/27667

URLs in this post:

[1] памяти: http://www.braintools.ru/article/4140

[2] Источник: https://habr.com/ru/articles/1014496/?utm_campaign=1014496&utm_source=habrahabr&utm_medium=rss

www.BrainTools.ru

Rambler's Top100