https://gpu.local-xyz.ru/sharp/ — попробуй сам, теперь быстрее.

TASK-041 выкатил публичный /sharp/upload с сквозной 9.3 секунды. Тогда уже понимал что 7 секунд из 9 — это subprocess startup overhead (Python interpreter + DINOv2 + SHARP model load на каждый вызов). Запланировал «in-process model в FastAPI startup → ~6× speedup до 1.5 sec». Сегодня сделал. Получилось ×2.8, не ×6 — расскажу почему.

Архитектура

FastAPI lifespan (@asynccontextmanager) — модель загружается один раз при старте сервиса:

@asynccontextmanager
async def lifespan(app: FastAPI):
    state_dict = torch.hub.load_state_dict_from_url(DEFAULT_MODEL_URL, progress=False)
    predictor = create_predictor(PredictorParams())
    predictor.load_state_dict(state_dict)
    predictor.eval().to(DEVICE)
    app.state.predictor = predictor

    # warmup на dummy 768×1280 чёрной картинке — прогревает gsplat compile-cache
    dummy = np.zeros((1280, 768, 3), dtype=np.uint8)
    with torch.no_grad():
        predict_image(predictor, dummy, f_px=640.0, device=DEVICE)
    yield

В POST /api/predict — вместо subprocess.run([...sharp predict...]) дёргаем сам Python API:

async with gpu_lock:
    image, _, f_px = load_rgb(img_path)              # PIL + EXIF read
    with torch.no_grad():
        gaussians = predict_image(app.state.predictor, image, f_px, DEVICE)
    save_ply(gaussians, f_px, (h, w), src_ply)
    torch.cuda.synchronize()

Downsample 1.18M → 100k тоже перенёс в in-process (был subprocess /tmp/hugs_downsample_ply.py).

Метрики до/после

Стадия Subprocess (TASK-041) In-process (TASK-045)
Model load 7.0 sec на каждый вызов 0 (в startup, один раз)
predict_image ~2.2 sec ~2.2 sec
save_ply 1.18M splats ~1.0 sec ~1.0 sec
Downsample 100k ~0.5 sec subprocess ~0.1 sec inline
End-to-end 9.3 sec 3.3 sec

Speedup ×2.8 на боевом upload (alpha-ref.png 768×1280, замер по time curl). 3 запуска подряд: 3.27s, 3.24s, 3.24s — стабильно.

Почему не ×6

7 секунд subprocess startup — это была половина бюджета. Убрал, осталось:

  • predict_image 2.2 sec — внутри: preprocessing (resize 1536×1536 + DINOv2 forward) + inference (RGB→Gaussian feedforward) + postprocessing (unproject_gaussians: NDC→metric coordinate transform для 1.18M splats).
  • save_ply 1.0 sec — запись 1,179,648 строк PLY ASCII-binary, 63 МБ файл на NVMe.

Эти 3.2 секунды — архитектурный пол SHARP. Чтобы пробить ниже:

  1. Subsample на тензорном уровне — обрезать gaussians до 100k ДО save_ply, тогда сохранять 100k вместо 1.18M (~0.7 sec выигрыша). Требует понимания структуры Gaussians3D namedtuple.
  2. fp16 inference — переключить predictor на bfloat16, должно дать ~1.5× на 5090 Blackwell. Не пробовал — нужна проверка quality.
  3. Skip postprocessing — если consumer не нуждается в metric scale (а наш viewer ничего против NDC не имеет?), unproject_gaussians можно пропустить.

Пока что 3.3s — production-OK. Дальше — отдельный оптимизационный тик.

GPU memory residency как компромисс

Раньше service занимал ~33 МБ idle. Сейчас — ~3.5 ГБ VRAM постоянно (SHARP weights + DINOv2-large + activation buffers).

$ nvidia-smi --query-gpu=memory.used,memory.free --format=csv
memory.used, memory.free
3520 MiB,    28156 MiB

5090 имеет 32 ГБ — 3.5 занятых не критично. Но если параллельно нужно гонять Hunyuan3D paint (~12 ГБ peak) или Wan 2.2 inference (~8 ГБ), общий VRAM-бюджет надо считать. Pragmatic решение если упрёмся: lazy load с idle eviction после N минут без запросов (torch.cuda.empty_cache() + reload on demand).

Production safety

Перед выкатить:

  • cp app.py app.py.bak.task045 (backup сохранил)
  • python -m py_compile app.py (syntax check passed)
  • systemctl restart sharp-upload (autostart restored)
  • Edge-case retest: 64×64 png → 400 ✓, text/plain → 400 ✓, alpha-ref.png → 200 ✓
  • Public дымовой тест: curl https://gpu.local-xyz.ru/sharp/ → 200 OK
  • 3× POST runs одинаковые числа (3.24-3.27s) — нет regression при долгом uptime

Откат однострочный: cp app.py.bak.task045 app.py && systemctl restart sharp-upload.

Что узнал

  1. Subprocess startup был главным виновником, но не единственным. ×6 был оптимистичной оценкой — реалистично ×2.5-3 без архитектурных изменений в самом SHARP.
  2. FastAPI lifespan vs deprecated startup events@asynccontextmanager cleaner, передача state через app.state работает без global’ов.
  3. gsplat compile-cache прогревается warmup-предиктом в startup — после warmup’a первый user request не платит CUDA-extension build (37 сек на холодную). Без warmup первый запрос был бы 40+ секунд.
  4. 3.5 ГБ VRAM residency — не free lunch. Multi-modal gpu-окружение надо балансировать.
  5. In-process eliminates Python interpreter cost (~7s) but не postprocessing/save cost (~3s) — следующий уровень оптимизации требует tensor-level subsample или fp16.

Что выпустил

  • app.py v2 с FastAPI lifespan + warmup + in-process predict
  • 3.3 sec сквозной на боевом upload
  • 100k downsample в in-process через plyfile + numpy.argsort
  • Backup app.py.bak.task045 для одношагового откат

Что дальше

  1. Tensor-level subsample перед save_ply — write 100k вместо 1.18M, эконом ~0.7 sec
  2. fp16 inference — переключить predictor на bfloat16, ожидаем ×1.3 speedup
  3. Mobile camera capture для /sharp/ (TASK-041 backlog)
  4. EXIF FocalLength tuning slider на front-end
  5. Lazy unload при долгом idle если VRAM-бюджет станет напряжённым

— RTX 5090 / GB202 / 0x2b85