TL;DR Memuat model ML adalah lambat, bahkan dengan cache halaman Linux yang sudah dipanaskan. Jadi kami membangun perpustakaan untuk membuatnya cepat. Ada beberapa detail teknis menarik yang ingin kami bagikan, jadi kami menulis blog ini. Perpustakaan ini juga memberikan dampak yang tidak terduga, dibahas di bagian akhir.
Rasional
Semuanya dimulai 2 tahun lalu, ketika kami meluncurkan percobaan pertama mode generasi lowpoly. Mode lowpoly tidak berjalan dengan baik, menghasilkan hasil yang buruk dari perspektif saat ini, tetapi kami membayar mahal untuk itu -- GPU khusus hanya memproses tugas satu digit per hari. Ini memiliki bobot yang disesuaikan dengan baik, cukup besar untuk mengeluarkan semua bobot model lainnya dari VRAM. Lebih buruk lagi, kami memiliki mungkin 3 model seperti itu (tidak ingat jumlah pastinya), mereka merupakan bagian signifikan dari infrastruktur inferensi kami, membuat rasio efisiensi yang cukup tidak memaafkan. Dan tidak, kami tidak bisa memuat model secara naif tepat waktu, itu memakan waktu 30 detik, lebih besar dari waktu pemrosesan sebenarnya.
Kami tidak memiliki insinyur pipeline khusus saat itu, pengembang algoritma kami mencoba yang terbaik untuk mengatasi ini. Beberapa hari kemudian, basis kode kami dipenuhi dengan this.to('cpu') dan that.to('cuda'). Pendekatan ini bekerja untuk sementara waktu, tetapi mengganggu alur pengembang algo kami dari waktu ke waktu. Bagaimana jika semuanya bisa terjadi secara otomatis? Ini Python, hal-hal memang terjadi secara otomatis di Python.
Bagaimana Anda mendefinisikan 'secara otomatis'?
Mari kita masuk ke peran pengembang algoritma. Semuanya cukup jelas: Saya tidak ingin peduli tentang kinerja runtime di luar algoritma inti saya kecuali saya benar-benar harus. Saya lebih suka tidak tahu apa-apa tentang pertukaran model masuk dan keluar.
Tentu saja kami tidak bisa mencapai itu, tetapi kami bisa mencoba meminimalkan gangguan yang harus kami perkenalkan ke kode algoritma. Ini mengingatkan saya pada monkey-patching dari perpustakaan gevent, yang menambal (terutama) perpustakaan socket, menggantinya dengan gevent.socket yang dapat beralih ke greenlet lain ketika IO akan memblokir, mirip dengan goroutine (sebenarnya gevent lebih tua dari Golang!).
Karena kami hanya menggunakan pustaka HuggingFace (transformers, diffusers) untuk memuat model pada saat itu, targetnya menjadi jelas: Kami hanya memperkenalkan panggilan monkey-patch, dan sisa kode harus tetap tidak berubah, XXXPipeline.from_pretrained(...) harus jauh lebih cepat.
Beberapa Fakta, Keputusan Jelas dan Asumsi
Overmind adalah perpustakaan caching, ia menyimpan hasil panggilan pemuatan model ke dalam memori sistem dan kemudian merekonstruksinya dengan cepat.
Kami melewatkan pembahasan tentang bagaimana monkey-patching diimplementasikan, itu adalah detail yang tidak terlalu menarik. Yang perlu kita ketahui adalah, itu mengarahkan semua panggilan XXXPipeline.from_pretrained(...) ke overmind.api.load(XXXPipeline.from_pretrained, ...).
Kami menggunakan pickle untuk menyerialkan hasil cache kami karena... kami tidak punya pilihan, dan torch.save sendiri menggunakan pickle, aneh jika tidak menggunakannya.
Kami menggunakan arsitektur klien/server karena kami tidak ingin membatalkan cache kami ketika proses berakhir. Ada banyak panggilan subprocess yang bisa mendapatkan manfaat darinya.
Kami mengasumsikan parameter XXXPipeline.from_pretrained adalah hal-hal yang dapat di-hash sederhana (str dan hal-hal serupa) dan model lain dimuat oleh overmind (dijelaskan nanti).
Nama overmind dipinjam dari Starcraft, seperti yang mungkin Anda tebak.
Merekonstruksi dengan cepat!
Kami tidak bisa dengan naif menyimpan hasil pickle.loads dalam memori dan menyebutnya selesai. Bagaimanapun, dalam skenario yang sudah dipanaskan, cache halaman Linux melakukan tugasnya menyimpan model di disk dan kami masih dapat melihat waktu pemuatan yang diukur dalam puluhan detik.
Ketidakefisienan berasal dari penyalinan memori. Dalam Python, bahkan membuat jutaan objek tidak akan memakan waktu lebih dari beberapa ratus ms. Namun, untuk penyalinan memori sebesar 10GiB, itu akan memakan waktu setengah detik. Kami harus menghindari penyalinan memori sebanyak mungkin.
Untungnya, sebagian besar potongan memori besar adalah tensor Torch, kami dapat dengan aman hanya mengalamatkan mereka dan mengabaikan sisanya.
Sebenarnya, saya mendapatkan pengetahuan tentang struktur internal tensor Torch dalam kode reduksi saat meneliti mekanisme berbagi tensor:
# Disalin dari torch.multiprocessing.reductions, sebagian besar kode dihapus
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))Cukup mudah: tensor adalah jenisnya, metadata-nya, dan penyimpanan dasarnya. Di sini storage adalah tipe TypedStorage, tetapi sebenarnya TypedStorage hanyalah pembungkus sederhana untuk UntypedStorage. UntypedStorage adalah kelas yang sebenarnya menyimpan semua data tensor.
Tugas kita menjadi lebih spesifik sekarang: Bagaimana kita menghindari menyalin UntypedStorage? Bisakah kita mengelola memori tensor ini sendiri dan membangun UntypedStorage dengan menunjuk ke memori yang kita kelola?
Jawabannya adalah ya!
Dengan melihat sekilas kode C++ di mana UntypedStorage dibangun, kita dapat dengan mudah menemukan potongan kode seperti ini:
// Disalin dari torch/csrc/Storage.cpp
static PyObject* THPStorage_get(THPStorage* self, PyObject* index) {
// ...menghilangkan kode yang tidak terkait...
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;
}Tidak hanya kita dapat menggunakan pointer, tetapi kelas at::DataPtr juga dapat menangani penghancuran, membuat manajemen masa pakai menjadi lebih sederhana.
Di sisi Python, pointer ke area memori direpresentasikan oleh objek memoryview, objek-objek ini mendukung protokol buffer. Kita bisa mendapatkan objek memoryview dari banyak hal, bytes dan mmap adalah 2 hal utama yang mendukungnya, dan mereka juga yang kita pedulikan.
Akhirnya, kita tahu apa yang harus kita lakukan: membuat fungsi yang menerima objek memoryview dan mengubahnya menjadi UntypedStorage tanpa menyalin. Dengan kemampuan untuk merekonstruksi UntypedStorage dari memoryview, data tensor sebenarnya tidak perlu berada dalam aliran pickle, sangat mengurangi ukuran data yang harus kita salin.
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,
)
));
});
}Itulah blok bangunan inti dari overmind.
Berbagi tensor!
Catatan: Sudah ada mekanisme berbagi tensor di PyTorch, tetapi tidak sesuai dengan kebutuhan kita. Lebih lanjut tentang ini nanti.
Pertama, berbagi memori antara klien dan server
Ketika kita melihat 'berbagi' dan 'memori' datang bersama, kita semua memiliki dorongan untuk menggunakan shmget dan teman-temannya. Ini "dirancang" untuk digunakan sebagai mekanisme berbagi memori, bukan? Tetapi ia memiliki 2 kelemahan utama:
- POSIX shm adalah sumber daya yang langka, apa yang dapat Anda gunakan ditentukan oleh bagaimana sysadmin mengkonfigurasi sistem. Contoh ekstrim tetapi umum adalah kontainer Docker, secara default Anda hanya memiliki 64MiB POSIX shm yang dapat digunakan.
- POSIX shm hidup lebih lama daripada proses anda, anda perlu mengurusnya sendiri. Jika proses pengurusan dihentikan secara paksa, atau tidak menanganinya dengan berhati-hati, objek shm boleh ditinggalkan pada sistem untuk tempoh yang tidak ditentukan.
Jika anda melihat dengan teliti, Linux penuh dengan panggilan sistem yang menarik. memfd_create adalah salah satu yang menarik minat kita: Ia memberikan anda fd yang mewakili peruntukan memori tanpa nama. Anda boleh melakukan semua jenis operasi fail padanya: baca, tulis, dan, sudah tentu, mmap. Jika kita boleh berkongsi fd, kita boleh berkongsi memori.
Berkongsi fd mempunyai cara 'standard' tetapi sukar untuk melakukannya: sendmsg dengan SCM_RIGHTS. Kita boleh memanfaatkan perpustakaan untuk membantu kita menyembunyikan butiran menakutkan proses sendmsg, tetapi kita masih perlu melakukan koordinasi antara proses pelayan dan klien. Kami memutuskan untuk menggunakan helah di sini: Hanya buka /proc/{pidof(server)}/fd/{memfd} di sisi klien, sambil tidak pernah menutup fd di sisi pelayan overmind. Satu-satunya komunikasi yang diperlukan adalah tuple (pid, fd). Ia berfungsi dengan sempurna dalam kes kami.
Kata-kata di atas diringkaskan kepada baris-baris ini:
class SharedMemory:
@classmethod
def create(cls, shift):
# Dipanggil di sisi pelayan
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):
# Dipanggil di sisi klien
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):
# Dipanggil di kedua-dua sisi
self._mmap = mmap.mmap(self._fd, size)
self._buf = memoryview(self._mmap)
return self._bufIntegrasi dengan pickling
Seperti yang kita bincangkan sebelum ini, kita perlu mengubah proses pickling UntypedStorage. Sama seperti yang dilaksanakan dalam torch.multiprocessing.reductions, kita mendefinisikan fungsi pengurangan khusus kita untuk pickle:
# Hoarder dan borrower adalah pembungkus kepada SharedMemory di atas, mengandungi
# perkara membosankan seperti arena memori, dll.
def _reduce_storage(storage):
# Dipanggil oleh pelayan
device = storage.device
storage = storage.cpu()
# Simpan kandungan dalam memori bersama
# `frag` mengandungi maklumat lengkap yang diperlukan untuk mencari kandungan.
frag = hoarder.put(storage)
return (_rebuild_storage_on_client, (frag, device))
def _rebuild_storage_on_client(frag, device):
# Dipanggil oleh klien
mv = borrower.borrow(frag) # Dapatkan memoryview dari memori bersama
storage = _make_untyped_storage(mv) # Zero-copy!
if device.type == 'cuda':
return storage.cuda(device.index)
return storage
class OvermindPickler(dill.Pickler):
...
OvermindPickler.register(torch.storage.UntypedStorage, _reduce_storage)Sekarang, OvermindPickler.dumps dan OvermindPickler.loads yang mudah akan memanfaatkan memori bersama untuk mempercepatkan. Anda boleh berhenti membaca di sini jika anda sudah bosan. Selebihnya adalah butiran.
Syaitan dalam Butiran
Kenapa tidak menggunakan perkongsian tensor dalaman PyTorch?
Untuk 'perkongsian tensor dalaman', saya maksudkan torch.multiprocessing.reductions.
- Pada tahap tinggi, kaedah PyTorch direka untuk 'menghantar tensor ke subprocess', nampaknya sama tetapi dengan perbezaan halus.
- PyTorch menggunakan POSIX shm untuk berkongsi memori, tertakluk kepada had yang disebutkan sebelum ini.
- Untuk setiap tensor (atau
UntypedStorage), PyTorch memperuntukkan objek POSIX shm khusus untuknya, walaupun ia hanya mengandungi 4 bait. Setiap objek menggunakan fd. - PyTorch menyahperuntukkan POSIX shm sebaik sahaja ia dinyahkod, menjadikannya tidak sesuai untuk keperluan kita. Kita perlu menyahkod aliran pickle yang sama beberapa kali.
- Terdapat banyak logik perkongsian berkaitan CUDA, yang merupakan bunyi dan masalah tulen untuk kes penggunaan kita.
Kenapa anda mengatakan 'data tensor disalin beberapa kali'?
Untuk torch.load pada cakera yang tipikal:
- Fail
torch.savepada cakera dibaca ke dalam memori. - Dapatkan data sebenar
torch.UntypedStoragesebagaibytesmelalui pengekstrakan fail Zip (torch.savemenghasilkan fail zip). - Kod C++ akan menyalin data ke dalam memori yang diuruskan sendiri dalam konstruktor
torch.UntypedStorage.
Untuk pickle.dumps yang naif dan kemudian pickle.loads:
- Aliran pickle yang dihasilkan secara dalaman mengandungi aliran pickle lain,
pickle.loadsakan menyalin aliran dalaman ke dalambytesbaru. - Data
torch.UntypedStoragemengandungi dalam aliran pickle dalaman, satu salinan lagi berlaku semasa pembinaantorch.UntypedStorage. - Kod C++ akan menyalin data ke dalam memori yang diuruskan sendiri dalam konstruktor
torch.UntypedStorage.
diffusers mempunyai modul dinamik
Repositori model boleh memasukkan fail Python yang diimport semasa runtime ke dalam namespace diffusers_modules. Klien tidak mempunyai ini dalam sys.path, menyebabkan unpickling gagal. Nasib baik, diffusers akan menulis fail Python dinamik ini pada cakera, jadi kita hanya perlu mengimport modul dan selesai.
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)Sokongan untuk bitsandbytes
Perkara yang paling menjengkelkan tentang menyokong bitsandbytes adalah bahawa proses kuantisasi berlaku pada GPU. Setelah kita memulakan CUDA dan torch dalam pelayan overmind, tiada cara mudah untuk membatalkannya, yang boleh menyebabkan masalah untuk beban kerja sebenar (terutamanya VRAM yang kurang boleh digunakan). Oleh itu, kami mengubah suai pelayan kami untuk memulakan subprocess, memuatkannya ke dalam memori dikongsi, dan menamatkannya. Ini berlaku untuk meningkatkan kestabilan pelayan overmind.
Parameter yang dikuantisasi adalah subkelas khas yang disediakan oleh bitsandbytes. Mereka tidak direka dengan 'picklability' dalam fikiran, jadi kita perlu melakukannya sendiri.
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)Model yang dikuantisasi melalui bitsandbytes datang dengan hooks dan monkey-patches yang tidak boleh dipickle, kita mesti menghapusnya:
from accelerate.hooks import remove_hook_from_module
remove_hook_from_module(model, True)
model.__dict__.pop('to', None) # Hapus monkeypatches amaran
model.__dict__.pop('cuda', None)Kami juga menghadapi masalah di mana fungsi bersarang dalam fungsi lain (bukannya berada di peringkat atas), yang menjadikannya tidak boleh dipickle. Kami cuba untuk mengatasi ini, tetapi tidak berjaya. Kami terpaksa menukar pickle kami dari yang disediakan stdlib kepada dill untuk mempickle ini. dill jauh lebih berkuasa, tetapi ia adalah implementasi Python tulen, yang jauh lebih perlahan daripada versi perpustakaan standard. Nasib baik, kos ini hanya akan dibayar sekali apabila kami memuatkan model kali pertama (hanya mempengaruhi pickling, bukan unpickling).
Sokongan untuk stable-fast
stable-fast menghasilkan hasil torch.compile, yang tidak boleh dipickle. Tetapi dengan torch.jit.save, kita boleh menyimpan hasil sebagai fail zip. Ini kelihatan tidak efisien, tetapi lebih baik daripada tiada.
Dengan hanya torch.jit.save ia tidak mencukupi untuk mempickle hasil stable-fast. stable-fast menggunakan proses 'flatten' untuk menjadikan modul Torch dapat dikesan. Apabila menemui sesuatu yang tidak dikenali (contohnya, kelas dataclass), ia tidak akan menyerapnya, tetapi hanya akan menyimpan rujukan kepada kelas sebenar. Kami telah menampal logik yang berkaitan untuk benar-benar menyimpan kelas yang dipickle dalam aliran 'flatten'.
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_dataclassTerdapat dua lagi helah di sini:
- Kami membungkus semula fail ZIP dengan
ZIP_STORED, jadi kami tidak perlu menyahmampatkan fail ZIP untuk setiap muatan seterusnya. - Antara muka
torch.jit.loadjuga menimbulkan isu salinan memori, jadi kami menulis pembungkus mudah untuk memuatkannya melalui protokol penimbal Python, sama sepertiUntypedStorage.
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); // No copy!
return import_ir_module(std::move(cu), in, ...);
}
);
}Corak vae=vae
Pangkalan kod kami mempunyai sesuatu seperti ini, ia cuba memuatkan model dengan model yang dimuatkan sebelumnya sebagai argumennya:
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, # Di sini!
controlnet=[controlnet_edge, controlnet_depth], # dan Di sini!
torch_dtype=torch.float16,
safety_checker=None,
)
pipeline.to('cuda')Seperti yang kami nyatakan sebelum ini, argumen fungsi diandaikan sebagai objek yang mudah dan boleh dipickle, tetapi corak ini melanggar andaian tersebut. Untuk menangani ini, kami menambah logik khas: setiap hasil cache mendapat ID yang dilampirkan. Jika objek itu digunakan sebagai argumen dalam panggilan lain, klien menggantikannya dengan IDnya, dan pelayan kemudian boleh memulihkan objek sebenar berdasarkan ID tersebut.
Model pipeline yang terhasil akan mengandungi rujukan kepada vae. Untuk kesederhanaan, kami hanya mempickle secara langsung di sini. Walau bagaimanapun, apabila memindahkan UntypedStorage sebenar ke memori bersama, kami mengurangkan sebarang data yang berulang.
Kami mungkin telah menggunakan mekanisme persistent_id pickle, tetapi saya tidak mencuba laluan ini. Itu agak memalukan.
Penanda Aras
Dan sekarang untuk bahagian yang semua orang suka lihat.
Kami menggunakan skrip corak VAE dari bahagian terakhir untuk melakukan ujian kami.
| Ujian | vae | depth | edge | pipeline | to('cuda') | Jumlah |
|---|---|---|---|---|---|---|
| tanpa, 1st | 1.18 | 0.98 | 1.41 | 1.65 | 0.91 | 6.16 |
| tanpa, 2nd | 1.15 | 0.96 | 0.97 | 1.65 | 0.89 | 5.66 |
| tanpa, 3rd | 1.15 | 0.96 | 0.98 | 1.61 | 0.91 | 5.65 |
| w/o, 4th | 1.42 | 1.10 | 1.11 | 1.72 | 0.88 | 6.27 |
| w/o, 5th | 1.28 | 1.08 | 1.10 | 1.72 | 0.92 | 6.13 |
| w/, 1st | 5.44 | 5.17 | 5.41 | 7.29 | 0.86 | 24.20 |
| w/, 2nd | 0.00 | 0.01 | 0.01 | 0.20 | 0.87 | 1.12 |
| w/, 3rd | 0.01 | 0.01 | 0.01 | 0.21 | 0.86 | 1.12 |
| w/, 4th | 0.01 | 0.01 | 0.01 | 0.20 | 0.90 | 1.15 |
| w/, 5th | 0.01 | 0.01 | 0.01 | 0.21 | 0.86 | 1.13 |
Seperti yang anda lihat, pemuatan awal dengan overmind mengambil masa 24.2 saat, yang mana jauh lebih lama berbanding pemuatan tanpanya. Namun, pada pemuatan berikutnya, hanya kos .to('cuda') yang masih ada.
Menjumlahkan saiz semua fail model yang diserialkan, keseluruhan pipeline dianggarkan menggunakan sekitar 5808 megabait memori. Penanda aras cepat memberikan hasil yang serupa.
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)Diuji pada Intel i9-11900K + GeForce RTX 4090.
Kesan Sampingan Tak Terduga (Positif!)
Motivasi utama kami untuk membina overmind adalah untuk membolehkan pertukaran cepat berat model semasa inferens. Walaupun ia memenuhi tujuannya, kami menemui beberapa kelebihan tambahan sepanjang jalan.
Kami menggunakan pelbagai instans aplikasi kami, satu untuk setiap GPU. Oleh itu, akan ada 8 proses per nod. Selepas kami menggunakan overmind, penggunaan memori sistem berkurang secara dramatik. Kami tidak mengalami kekurangan memori sistem, tetapi jika kami mengalaminya, ini akan menjadi kemenangan besar.
Kemudian, kami mendapati ia memberikan dorongan besar kepada pembangun algoritma dan pipeline kami. Untuk setiap gelung ubah-sahih, kami dapat menjimatkan 10 hingga 20 saat masa pemuatan, ini boleh menambah kepada jumlah yang besar. Lebih penting lagi, saat yang dijimatkan dapat mengekalkan pembangun dalam aliran kerja.
Github
Kami membuka sumbernya di Github, kami akan gembira jika ia membantu.


