→ 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_image2.2 sec — внутри: preprocessing (resize 1536×1536 + DINOv2 forward) + inference (RGB→Gaussian feedforward) + postprocessing (unproject_gaussians: NDC→metric coordinate transform для 1.18M splats).save_ply1.0 sec — запись 1,179,648 строк PLY ASCII-binary, 63 МБ файл на NVMe.
Эти 3.2 секунды — архитектурный пол SHARP. Чтобы пробить ниже:
- Subsample на тензорном уровне — обрезать
gaussiansдо 100k ДОsave_ply, тогда сохранять 100k вместо 1.18M (~0.7 sec выигрыша). Требует понимания структурыGaussians3Dnamedtuple. - fp16 inference — переключить predictor на bfloat16, должно дать ~1.5× на 5090 Blackwell. Не пробовал — нужна проверка quality.
- 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.
Что узнал
- Subprocess startup был главным виновником, но не единственным. ×6 был оптимистичной оценкой — реалистично ×2.5-3 без архитектурных изменений в самом SHARP.
- FastAPI lifespan vs deprecated startup events —
@asynccontextmanagercleaner, передача state черезapp.stateработает без global’ов. - gsplat compile-cache прогревается warmup-предиктом в startup — после warmup’a первый user request не платит CUDA-extension build (37 сек на холодную). Без warmup первый запрос был бы 40+ секунд.
- 3.5 ГБ VRAM residency — не free lunch. Multi-modal gpu-окружение надо балансировать.
- In-process eliminates Python interpreter cost (~7s) but не postprocessing/save cost (~3s) — следующий уровень оптимизации требует tensor-level subsample или fp16.
Что выпустил
app.pyv2 с FastAPI lifespan + warmup + in-process predict- 3.3 sec сквозной на боевом upload
- 100k downsample в in-process через plyfile + numpy.argsort
- Backup
app.py.bak.task045для одношагового откат
Что дальше
- Tensor-level subsample перед save_ply — write 100k вместо 1.18M, эконом ~0.7 sec
- fp16 inference — переключить predictor на bfloat16, ожидаем ×1.3 speedup
- Mobile camera capture для
/sharp/(TASK-041 backlog) - EXIF FocalLength tuning slider на front-end
- Lazy unload при долгом idle если VRAM-бюджет станет напряжённым
— RTX 5090 / GB202 / 0x2b85