DUYURULAR

Overmind: ML Model Yükleme Süresini 15 Saniyeden 0.2 Saniyeye Düşürme

Meshy, Overmind'i nasıl inşa etti — ML model yükleme süresini 15 saniyeden 0.2 saniyeye düşüren, sıfır kopyalı paylaşımlı bellek kullanan ve çıkarım kodunda herhangi bir değişiklik gerektirmeyen açık kaynaklı bir kütüphane.

Bin Wang, Senior Infrastructure Engineer
Gönderilme tarihi: 6 Mart 2026

Özet ML model yüklemesi, Linux sayfa önbelleği ısınmış olsa bile yavaş. Bu yüzden bunu hızlı hale getirmek için bir kütüphane oluşturduk. Paylaşmak istediğimiz bazı ilginç teknik detaylar var, bu yüzden bu blogu yazdık. Kütüphane ayrıca beklenmedik bir etki yarattı, bu da sonunda tartışılacak.

Gerekçe

Her şey 2 yıl önce, düşük poligonlu üretim modunun ilk denemesini gönderdiğimizde başladı. Düşük poligon modu iyi gitmedi, bugünün perspektifinden bakıldığında kötü sonuçlar veriyor, ama bunun için çok para ödedik -- özel bir GPU günde sadece tek haneli görevleri işliyor. İnce ayarlı ağırlıklara sahip, diğer tüm model ağırlıklarını VRAM'den çıkaracak kadar büyük. Daha kötüsü, belki 3 tane böyle modelimiz var (kesin sayıyı hatırlayamıyorum), bunlar çıkarım altyapımızın önemli bir kısmını oluşturuyordu ve oldukça affedilmez bir verimlilik oranı yaratıyordu. Ve hayır, modelleri sadece zamanında yükleyemeyiz, bu 30 saniye sürüyor, bu da gerçek işlem süresinden daha uzun.

O zamanlar özel boru hattı mühendislerimiz yoktu, algoritma geliştiricilerimiz bunu aşmak için ellerinden geleni yaptılar. Günler sonra, kod tabanımız this.to('cpu') ve that.to('cuda') ile doluydu. Bu yaklaşım bir süre işe yaradı, ama zaman zaman algoritma geliştiricilerimizin akışını bozdu. Ya işler otomatik olarak gerçekleşseydi? Bu Python, Python'da işler gerçekten otomatik olarak gerçekleşir.

'Otomatik' nasıl tanımlanır?

Bir algoritma geliştiricisinin rolüne atlayalım. İşler oldukça açık: Ana algoritmamın dışındaki çalışma zamanı performansıyla ilgilenmek istemiyorum, zorunda kalmadıkça. Model değişimini bilmek istemem.

Tabii ki bunu başaramayız, ama algoritma koduna getirmemiz gereken müdahaleyi en aza indirmeye çalışabiliriz. Bu bana gevent kütüphanesinin monkey-patching'ini hatırlatıyor, bu kütüphane (öncelikle) socket kütüphanesini yamalar, onu gevent.socket ile değiştirir ve IO'nun engelleyeceği durumlarda diğer greenlet'lere geçiş yapabilir, tıpkı bir goroutine gibi (aslında gevent, Golang'dan daha eskidir!).

O zamanlar sadece HuggingFace kütüphanelerini (transformers, diffusers) kullanarak modelleri yüklediğimiz için hedef netleşti: Sadece bir monkey-patch çağrısı ekliyoruz ve kodun geri kalanı değişmeden kalmalı, XXXPipeline.from_pretrained(...) çok daha hızlı olmalı.

Bazı Gerçekler, Açık Kararlar ve Varsayımlar

Overmind bir önbellekleme kütüphanesidir, model yükleme çağrısı sonuçlarını sistem belleğine önbellekler ve daha sonra hızlı bir şekilde yeniden oluşturur.

Monkey-patching'in nasıl uygulandığını tartışmayı atlıyoruz, bu o kadar da ilginç bir detay değil. Bilmemiz gereken tek şey, tüm XXXPipeline.from_pretrained(...) çağrılarını overmind.api.load(XXXPipeline.from_pretrained, ...) yönlendirdiğidir.

Önbellek sonucumuzu serileştirmek için pickle kullanıyoruz çünkü... başka seçeneğimiz yok ve torch.save kendisi pickle kullanıyor, bunu kullanmamak garip olurdu.

Önbelleğimizi işlem sonlandığında geçersiz kılmak istemediğimiz için bir istemci/sunucu mimarisi kullanıyoruz. Birçok alt işlem çağrısı bundan faydalanabilir.

XXXPipeline.from_pretrained parametrelerinin basit hashlenebilir şeyler (str ve benzeri şeyler) ve overmind tarafından yüklenen diğer modeller olduğunu varsayıyoruz (daha sonra açıklanacak).

Adı overmind, tahmin ettiğiniz gibi Starcraft'tan alınmıştır.

Hızlı Yeniden Oluştur!

pickle.loads sonucunu belleğe kaydedip günü kurtardık diyemeyiz. Sonuçta, ısınmış bir senaryoda, Linux sayfa önbelleği disk üzerindeki modelleri önbelleğe alarak işini yapar ve hala on saniyelerle ölçülen bir yükleme süresi görebiliriz.

Verimsizlik, bellek kopyalamadan kaynaklanıyor. Python'da, milyonlarca nesne oluşturmak bile birkaç yüz milisaniyeden fazla sürmez. Ancak, 10GiB'lik bir bellek kopyası için yarım saniye sürer. Bellek kopyalamadan mümkün olduğunca kaçınmalıyız.

Neyse ki, büyük bellek parçalarının çoğu Torch tensörleridir, sadece onları adresleyip geri kalanını görmezden gelebiliriz.

Aslında, tensör paylaşım mekanizmasını araştırırken bir Torch tensörünün iç yapısı hakkında bilgi edindim:

python
# torch.multiprocessing.reductions'dan kopyalanmıştır, kodun çoğu kaldırıldı
def reduce_tensor(tensor):
    ...
    storage = tensor._typed_storage()
    ...
    metadata = (
        tensor.storage_offset(),
        tensor.size(),
        tensor.stride(),
        tensor.requires_grad,
    )
    return (rebuild_tensor, (type(tensor), storage, metadata))

Oldukça basit: bir tensör, tipi, metadatası ve altında yatan depolamasıdır. Burada storage, TypedStorage türündedir, ancak aslında TypedStorage, UntypedStorage için basit bir sarmalayıcıdır. UntypedStorage, tüm tensör verilerini gerçekten tutan sınıftır.

Görevimiz şimdi daha spesifik hale geldi: UntypedStorage'ı kopyalamaktan nasıl kaçınırız? Bu tensör belleğini kendimiz yönetebilir miyiz ve yönettiğimiz belleğe işaret ederek UntypedStorageları oluşturabilir miyiz?

Cevap evet!

UntypedStorage'ın oluşturulduğu C++ kodunu hızlıca gözden geçirirken, kolayca şu kod parçasını bulabiliriz:

cpp
// torch/csrc/Storage.cpp'den kopyalanmıştır
static PyObject* THPStorage_get(THPStorage* self, PyObject* index) {
    // ...alakasız kodlar atlanmıştır...

    auto new_storage_impl = make_storage_impl(
        c10::StorageImpl::use_byte_size_t(),
        slicelength,
        at::DataPtr(
            static_cast<void*>(data + start),
            old_storage_impl,
            [](void* s) {
              c10::raw::intrusive_ptr::decref(static_cast<at::StorageImpl*>(s));
            },
            old_storage_impl->device()),
        old_storage_impl->allocator(),
        /* resizable */ false,
        device_opt);

    PyObject* _ret =
        THPStorage_NewWithStorage(Py_TYPE(self), std::move(new_storage_impl));

    return _ret;
}

Sadece bir işaretçi kullanmakla kalmaz, at::DataPtr sınıfı aynı zamanda yıkımı da yönetebilir, bu da ömür yönetimini çok daha basit hale getirir.

Python tarafında, bir bellek bölgesine işaret eden bir nesne memoryview nesnesi ile temsil edilir, bu nesneler tampon protokolünü destekler. bytes ve mmap gibi birçok şeyden bir memoryview nesnesi alabiliriz ve bunlar da ilgilendiğimiz şeylerdir.

Sonunda ne yapmamız gerektiğini biliyoruz: bir memoryview nesnesini kabul eden ve kopyalamadan bir UntypedStorage'a dönüştüren bir fonksiyon oluşturmak. memoryview'den UntypedStorage'ı yeniden oluşturma yeteneği ile, gerçek tensör verilerinin pickle akışında olması gerekmez, kopyalamamız gereken veri boyutunu büyük ölçüde azaltır.

cpp
void initOvermindHelpers(py::module m) {
    // ...
    m.def("_make_untyped_storage", [](py::buffer b) {
        auto info = new py::buffer_info(b.request());

        return pybind11::reinterpret_steal<py::object>(THPStorage_NewWithStorage(
            THPStorageClass,
            c10::make_intrusive<at::StorageImpl>(
                c10::StorageImpl::use_byte_size_t(),
                info->size,
                at::DataPtr(
                    info->ptr, info,
                    [](void* ptr) {
                        py::gil_scoped_acquire gil;
                        auto b = static_cast<py::buffer_info*>(ptr);
                        delete b;
                    },
                    at::DeviceType::CPU
                ),
                /*allocator=*/nullptr,
                /*resizable=*/false,
            )
        ));
    });
}

Bu, overmind'in temel yapı taşıdır.

Tensörleri Paylaşmak!

Not: PyTorch'ta zaten bir tensör paylaşım mekanizması var, ancak ihtiyaçlarımızı karşılamıyor. Daha fazlası için daha sonra.

İlk olarak, istemci ve sunucu arasında bellek paylaşımı

'Paylaşım' ve 'bellek' bir araya geldiğinde, hepimiz shmget ve arkadaşlarını kullanma dürtüsüne sahibiz. Bellek paylaşım mekanizması olarak kullanılmak üzere "tasarlanmış", değil mi? Ancak 2 büyük kusuru var:

  • POSIX shm kıt bir kaynaktır, kullanabileceğiniz şey, sistem yöneticisinin sistemi nasıl yapılandırdığına bağlıdır. Aşırı ama yaygın bir örnek Docker konteynerleridir, varsayılan olarak sadece 64MiB POSIX shm kullanılabilir.
  • POSIX shm, işleminizden daha uzun süre yaşar, kendi yönetiminizi yapmanız gerekir. Yönetim süreci zorla sonlandırılırsa veya dikkatli bir şekilde ele alınmazsa, shm nesnesi sistemde süresiz olarak kalabilir.

Dikkatlice bakarsanız, Linux ilginç sistem çağrılarıyla doludur. İlginç bulduğumuz bir tanesi memfd_create: Bu, size anonim belleğin bir tahsisini temsil eden bir fd verir. Üzerinde her türlü dosya işlemini yapabilirsiniz: okuma, yazma ve tabii ki mmap. Eğer fd'yi paylaşabilirsek, belleği de paylaşabiliriz.

Bir fd'yi paylaşmanın 'standart' ama gizemli bir yolu vardır: sendmsg ile SCM_RIGHTS. sendmsg sürecinin göz korkutucu detaylarını gizlememize yardımcı olacak kütüphanelerden yararlanabiliriz, ancak yine de sunucu ve istemci süreçleri arasında koordinasyonumuzu yapmamız gerekir. Burada bir hile kullanmaya karar verdik: İstemci tarafında /proc/{pidof(server)}/fd/{memfd} dosyasını açmak, bu sırada overmind sunucu tarafında fd'yi asla kapatmamak. Gerekli tek iletişim (pid, fd) ikilisidir. Bizim durumumuzda mükemmel çalışıyor.

Yukarıdaki kelimeler şu satırlara indirgenir:

python
class SharedMemory:
    @classmethod
    def create(cls, shift):
        # Sunucu tarafında çağrılır
        libc = ctypes.CDLL(None)
        name = _make_filename(shift).encode('utf-8')
        fd = libc.memfd_create(name, os.O_RDWR)
        os.ftruncate(fd, 1 << shift)
        mem_id = (os.getpid(), fd)
        return cls(fd=fd, mem_id=mem_id)

    @classmethod
    def rebuild(cls, mem_id):
        # İstemci tarafında çağrılır
        pid, fd = mem_id
        local_fd = os.open(f'/proc/{pid}/fd/{fd}', os.O_RDWR)
        return cls(fd=local_fd, mem_id=mem_id)
    
    def get_buffer(self):
        # Her iki tarafta da çağrılır
        self._mmap = mmap.mmap(self._fd, size)
        self._buf = memoryview(self._mmap)
        return self._buf

Pickling ile Entegrasyon

Daha önce tartıştığımız gibi, UntypedStorage'ın pickling sürecini değiştirmemiz gerekiyor. torch.multiprocessing.reductions'da uygulananlara benzer şekilde, pickle için özel azaltma fonksiyonlarımızı tanımlıyoruz:

python
# Hoarder ve borrower, yukarıdaki SharedMemory için bir sarmalayıcıdır, 
# bellek alanı gibi sıkıcı şeyler içerir.
def _reduce_storage(storage):
    # Sunucu tarafından çağrılır
    device = storage.device
    storage = storage.cpu()

    # İçeriği paylaşılan bellekte sakla
    # `frag`, içeriği bulmak için gereken tüm bilgileri içerir.
    frag = hoarder.put(storage)

    return (_rebuild_storage_on_client, (frag, device))

def _rebuild_storage_on_client(frag, device):
    # İstemci tarafından çağrılır
    mv = borrower.borrow(frag)  # Paylaşılan bellekten bir memoryview al
    storage = _make_untyped_storage(mv)  # Sıfır kopya!
    if device.type == 'cuda':
        return storage.cuda(device.index)
    return storage

class OvermindPickler(dill.Pickler):
    ...

OvermindPickler.register(torch.storage.UntypedStorage, _reduce_storage)

Artık basit OvermindPickler.dumps ve OvermindPickler.loads paylaşılan belleği hızlandırmak için kullanacak. Zaten sıkıldıysanız okumayı burada bırakabilirsiniz. Geri kalan detaylar.

Detaylardaki Şeytanlar

Neden PyTorch'un dahili tensör paylaşımı değil?

'Dahili tensör paylaşımı' ile torch.multiprocessing.reductions'u kastediyorum.

  1. Yüksek seviyede, PyTorch'un yöntemleri 'alt sürece tensör geçirme' için tasarlanmıştır, aynı gibi görünüyor ama ince bir farkla.
  2. PyTorch, belleği paylaşmak için POSIX shm kullanır, daha önce bahsedilen sınıra tabidir.
  3. Her tensör (veya UntypedStorage) için, PyTorch, yalnızca 4 bayt içerse bile, özel bir POSIX shm nesnesi tahsis eder. Her nesne bir fd tüketir.
  4. PyTorch, POSIX shm'yi unpickle edildikten sonra tahliye eder, bu da bizim ihtiyaçlarımız için uygun değildir. Aynı pickle akışını birden çok kez deserialize etmemiz gerekiyor.
  5. Çok sayıda CUDA ile ilgili paylaşım mantığı vardır, bu da bizim kullanım durumumuz için saf gürültü ve sorundur.

Neden 'tensör verileri birden çok kez kopyalanıyor' diyorsunuz?

Tipik bir disk üzerindeki torch.load için:

  • Disk üzerindeki torch.save dosyası belleğe okunur.
  • torch.UntypedStorage verilerini bytes olarak almak için Zip dosyası çıkarma işlemi (torch.save bir zip dosyası oluşturur).
  • C++ kodu, verileri torch.UntypedStorage yapıcısında kendi yönetilen belleğine kopyalayacaktır.

Naif bir pickle.dumps ve daha sonra pickle.loads için:

  • Oluşturulan pickle akışı, dahili olarak başka bir pickle akışını gömülü olarak içerir, pickle.loads iç akışı yeni bir bytes içine kopyalayacaktır.
  • torch.UntypedStorage verileri iç pickle akışında gömülüdür, torch.UntypedStorage yapımında başka bir kopya oluşur.
  • C++ kodu, verileri torch.UntypedStorage yapıcısında kendi yönetilen belleğine kopyalayacaktır.

diffusers dinamik bir modüle sahiptir

Model depoları, çalışma zamanında diffusers_modules ad alanına aktarılan Python dosyalarını içerebilir. İstemci, bunları sys.path içinde bulundurmaz, bu da unpickling işlemini bozar. Neyse ki, diffusers bu dinamik Python dosyalarını disk üzerine yazar, böylece sadece modülü içe aktarıp günü kurtarabiliriz.

python
def diffusers_dyn_module_workaround():
    from diffusers.utils.constants import HF_MODULES_CACHE
    modpath = Path(HF_MODULES_CACHE) / "diffusers_modules/__init__.py"
    spec = importlib.util.spec_from_file_location("diffusers_modules", modpath)
    sys.modules["diffusers_modules"] = importlib.util.module_from_spec(spec)

bitsandbytes desteği

bitsandbytes desteğiyle ilgili en can sıkıcı şey, kuantizasyon işleminin bir GPU üzerinde gerçekleşmesidir. CUDA ve torch'u overmind sunucusunda başlattıktan sonra, bunu kolayca sonlandırmanın bir yolu yoktur, bu da gerçek iş yükleri için sorunlara neden olabilir (özellikle daha az kullanılabilir VRAM). Bu nedenle, sunucumuzu bir alt süreç oluşturacak, bunu paylaşılan belleğe yükleyecek ve sonlandıracak şekilde değiştirdik. Bu, overmind sunucusunun kararlılığını artırır.

Kuantize edilmiş parametreler, bitsandbytes tarafından sağlanan özel alt sınıflardır. Bunlar 'pickle' edilebilirlik düşünülerek tasarlanmamıştır, bu yüzden bunu kendimiz yapmalıyız.

python
def _reduce_bnb_param(p):
    dev = p._prev_device
    assert p.quant_state
    return (_rebuild_bnb_param, (type(p), p.data, p.quant_state.as_dict(packed=True), dev))


def _rebuild_bnb_param(typ, data, qs_dict, dev):
    return typ.from_prequantized(data, qs_dict, device=dev)


def bitsandbytes_quirks():
    try:
        import bitsandbytes
    except ImportError as e:
        return

    ForkingPickler.register(bitsandbytes.nn.modules.Params4bit, _reduce_bnb_param)
    ForkingPickler.register(bitsandbytes.nn.modules.Int8Params, _reduce_bnb_param)

bitsandbytes aracılığıyla kuantize edilmiş modeller, pickle edilemeyen kancalar ve maymun yamaları ile gelir, bunları çıkarmamız gerekir:

python
from accelerate.hooks import remove_hook_from_module
remove_hook_from_module(model, True)
model.__dict__.pop('to', None)  # Uyarı maymun yamalarını kaldır
model.__dict__.pop('cuda', None)

Diğer işlevlerin içinde yuvalanmış işlevlerin olduğu durumlarla da karşılaştık (üst düzeyde olmak yerine), bu da onların pickle edilememesine neden olur. Bunu aşmaya çalıştık, ancak başarılı olamadık. Standart kütüphane tarafından sağlanan pickle'ımızı dill ile değiştirmek zorunda kaldık. dill çok daha güçlüdür, ancak saf bir Python uygulamasıdır, bu da standart kütüphane sürümünden çok daha yavaştır. Neyse ki, bu maliyet yalnızca modeli ilk kez yüklerken ödenecektir (sadece pickling'i etkiler, unpickling'i etkilemez).

stable-fast desteği

stable-fast, pickle edilemeyen torch.compile sonuçları üretir. Ancak torch.jit.save ile sonuçları bir zip dosyası olarak kaydedebiliriz. Bu verimsiz görünebilir, ancak hiç yoktan iyidir.

Sadece torch.jit.save ile stable-fast sonuçlarını pickle etmek yeterli değildir. stable-fast, Torch modülünü izlenebilir hale getirmek için bir 'flatten' işlemi kullanır. Tanımadığı bir şeyle karşılaştığında (örneğin, dataclass'ın sınıfı), bunu serileştirmez, ancak gerçek sınıfa bir referans tutar. İlgili mantığı, 'flatten' edilmiş akış içinde bir pickle edilmiş sınıfı gerçekten depolayacak şekilde yamaladık.

python
def stable_fast_quirks():
    ...

    # pickle dataclass type instead of just put it into a container (which will not survive after torch.jit.save)
    def flatten_dataclass(obj):
        from sfast.utils.flat_tensors import flatten_bytes, flatten_dict
        import dataclasses
        d = dict((field.name, getattr(obj, field.name))
                for field in dataclasses.fields(obj))
        import pickle
        pickled = pickle.dumps(obj.__class__)
        return flatten_bytes(pickled) + flatten_dict(d)

    def unflatten_dataclass(tensors, start):
        from sfast.utils.flat_tensors import unflatten_bytes, unflatten_dict
        import pickle
        pickled, start = unflatten_bytes(tensors, start)
        clz = pickle.loads(pickled)
        content, start = unflatten_dict(tensors, start)
        return clz(**content), start

    sfast.utils.flat_tensors.flatten_dataclass = flatten_dataclass
    sfast.utils.flat_tensors.unflatten_dataclass = unflatten_dataclass

Burada iki ek püf noktası daha var:

  1. ZIP dosyasını ZIP_STORED ile yeniden paketliyoruz, böylece her sonraki yükleme için ZIP dosyasını açmak zorunda kalmıyoruz.
  2. torch.jit.load arayüzü de bellek kopyalama sorununu ortaya çıkarıyor, bu yüzden Python tampon protokolü aracılığıyla yüklemek için UntypedStorage gibi basit bir sarmalayıcı yazdık.
cpp
void initOvermindHelpers(py::module m) {
    // ...
    m.def("import_ir_module_from_buffer_0copy",
        [](std::shared_ptr<torch::jit::CompilationUnit> cu, py::buffer buffer) {
            auto info = buffer.request();
            imemstream in((char*)info.ptr, info.size);  // Kopya yok!
            return import_ir_module(std::move(cu), in, ...);
        }
    );
}

vae=vae deseni

Kod tabanımızda buna benzer bir şey var, daha önce yüklenmiş bir modeli argüman olarak kullanarak bir modeli yüklemeye çalışıyor:

python
import overmind.api
overmind.api.monkey_patch_all()

import torch
from diffusers.models import AutoencoderKL

from diffusers import (
    ControlNetModel,
    StableDiffusionControlNetPipeline,
)

vae = AutoencoderKL.from_pretrained(
    "lemon2431/ChineseInkComicStrip_v10",
    subfolder="vae",
    torch_dtype=torch.float16,
)
controlnet_depth = ControlNetModel.from_pretrained(
    "lllyasviel/control_v11f1p_sd15_depth",
    torch_dtype=torch.float16,
    variant="fp16",
)
controlnet_edge = ControlNetModel.from_pretrained(
    "lllyasviel/control_v11p_sd15_softedge",
    torch_dtype=torch.float16,
    variant="fp16",
)

pipeline = StableDiffusionControlNetPipeline.from_pretrained(
    "lemon2431/ChineseInkComicStrip_v10",
    vae=vae,  # Burada!
    controlnet=[controlnet_edge, controlnet_depth],  # ve Burada!
    torch_dtype=torch.float16,
    safety_checker=None,
)

pipeline.to('cuda')

Daha önce belirttiğimiz gibi, fonksiyon argümanlarının basit, kolayca pickle edilebilir nesneler olduğu varsayılır, ancak bu desen bu varsayımı bozar. Bunu ele almak için özel bir mantık ekledik: her önbelleğe alınmış sonuca bir kimlik eklenir. Eğer bu nesne başka bir çağrıda argüman olarak kullanılırsa, istemci bunu kimliği ile değiştirir ve sunucu daha sonra gerçek nesneyi kimliğe göre geri alabilir.

Ortaya çıkan pipeline modeli vae'ye bir referans içerecektir. Basitlik adına, burada doğrudan pickle ediyoruz. Ancak, gerçek UntypedStorage'ı paylaşılan belleğe taşırken, tekrarlanan verileri ayıklıyoruz.

Pickle'ın persistent_id mekanizmasını kullanmış olabiliriz, ama bu yolu denemedim. Bu biraz üzücü.

Performans Testi

Ve şimdi herkesin görmekten hoşlandığı kısım.

Son bölümdeki VAE desen scriptini testimiz için kullanıyoruz.

Testvaedepthedgepipelineto('cuda')Toplam
w/o, 1st1.180.981.411.650.916.16
w/o, 2nd1.150.960.971.650.895.66
w/o, 3rd1.150.960.981.610.915.65
w/o, 4th1.421.101.111.720.886.27
w/o, 5th1.281.081.101.720.926.13
w/, 1st5.445.175.417.290.8624.20
w/, 2nd0.000.010.010.200.871.12
w/, 3rd0.010.010.010.210.861.12
w/, 4th0.010.010.010.200.901.15
w/, 5th0.010.010.010.210.861.13

Gördüğünüz gibi, overmind ile ilk yükleme 24.2 saniye sürüyor, bu da onsuz yüklemeye kıyasla önemli ölçüde daha uzun. Ancak, sonraki yüklemelerde yalnızca .to('cuda') maliyeti hala mevcut.

Tüm serileştirilmiş model dosyalarının boyutlarını topladığımızda, tüm hattın yaklaşık 5808 megabayt bellek kullanacağı tahmin edilmektedir. Hızlı bir kıyaslama benzer bir sonuç veriyor.

text
In [1]: t = torch.ones((5808, 1024, 1024), dtype=torch.uint8)

In [2]: %time a = t.cuda()
CPU times: user 976 ms, sys: 874 μs, total: 977 ms
Wall time: 976 ms

In [3]: %timeit a = t.cuda()
1.01 s ± 56.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Intel i9-11900K + GeForce RTX 4090 üzerinde test edilmiştir.

Beklenmedik Yan Etkiler (Olumlu!)

overmind'i oluşturma konusundaki birincil motivasyonumuz, çıkarım sırasında model ağırlıklarının hızlı bir şekilde değiştirilmesini sağlamaktı. Amacına hizmet ederken, yol boyunca birkaç ek avantaj keşfettik.

Uygulamamızın birden fazla örneğini, her GPU için bir tane olmak üzere dağıtıyoruz. Böylece, her düğümde 8 işlem olacak. overmind'i dağıttıktan sonra, sistem bellek kullanımı dramatik bir şekilde azaldı. Sistem bellek sıkıntısı çekmiyorduk, ancak çekseydik, bu büyük bir kazanç olurdu.

Daha sonra, algoritma ve boru hattı geliştiricilerimiz için büyük bir destek olduğunu gördük. Her değişiklik-doğrulama döngüsü için, yükleme süresinden 10 ila 20 saniye tasarruf edebiliriz, bu büyük bir sayıya dönüşebilir. Daha da önemlisi, tasarruf edilen saniyeler geliştiricilerin akışta kalmasını sağlayabilir.

Github

Bunu Github üzerinde açık kaynak olarak sunuyoruz, yardımcı olduysa mutlu oluruz.

Daha Hızlı Çıkarımın Neler Sağlayabileceğini Görün
Overmind, Meshy'nin AI 3D üretiminin arkasındaki hızı sağlar. Deneyin ve sonuçları ilk elden görün.
Bu yazı faydalı oldu mu?

3D, Komut Üzerine