پرش به محتوا
← همهٔ یادداشت‌ها

سرور GPU چندکارته: کی لازم است و چطور از همه استفاده کنیم

·۸ دقیقه مطالعه·تیم جی‌پی‌یو

یک گروه سگ سورتمه که در یک ردیف کشیده، با هم سورتمه را روی دشت برفی می‌کشند، با جنگل کاج و کوه‌های برفی زیر آسمان آبی صاف.

اگر مدلتان روی یک کارت جا نمی‌شود یا آموزشش روزها طول می‌کشد، یک سرور GPU با دو یا چهار کارت گرافیک جواب است؛ به شرطی که کدتان واقعاً از همهٔ کارت‌ها استفاده کند. در این آموزش می‌بینید کی ماشین چندکارته به‌صرفه است، چطور مطمئن شوید همهٔ کارت‌ها دیده می‌شوند، و سه الگوی عملی برای به کار گرفتنشان را قدم‌به‌قدم اجرا می‌کنید.

خواندن و اجرای این راهنما حدود یک ساعت وقت می‌گیرد. یک ماشین دوکارتهٔ RTX 3060 در زمان نوشتن این مطلب از حدود ۴۱ هزار تومان در ساعت اجاره می‌شود، پس تمرین کردن همهٔ مرحله‌ها خرج زیادی ندارد. اشتباه رایج این است که کسی دو کارت اجاره کند و کدش فقط روی کارت اول بچرخد؛ یعنی دو برابر پول بدهد و همان سرعت را بگیرد. این‌جا یاد می‌گیرید جلوی این را بگیرید.

پیش‌نیازها

سرور GPU چندکارته کی لازم است؟

سه موقعیت واقعی هست که دو یا چند کارت به دردتان می‌خورد:

  1. مدل در حافظهٔ یک کارت جا نمی‌شود. مثلاً یک مدل زبانی ۷۰ میلیارد پارامتری حتی با کوانتیزه‌سازی ۴ بیتی حدود ۴۰ گیگابایت VRAM می‌خواهد و روی یک کارت ۲۴ گیگ بالا نمی‌آید. با دو کارت ۲۴ گیگ، مدل بین دو کارت تقسیم می‌شود. برای حساب دقیق حافظه، اجرای مدل زبانی: جدول VRAM و انتخاب کارت گرافیک را ببینید.
  2. آموزش مدل طولانی است. با آموزش موازی داده (Data Parallel)، هر کارت بخشی از داده را پردازش می‌کند و زمان هر epoch تقریباً بر تعداد کارت‌ها تقسیم می‌شود.
  3. کارهای مستقل زیادی دارید. مثلاً هزار تصویر یا صد فایل صوتی. لازم نیست کار را پیچیده کنید؛ روی هر کارت یک پردازش جدا اجرا می‌کنید.

اگر هیچ‌کدام از این‌ها نیست، یک کارت قوی‌تر معمولاً ساده‌تر و ارزان‌تر است. ارتباط بین کارت‌ها همیشه کمی سربار دارد و هر کدی خودبه‌خود چندکارته نمی‌شود.

انتخاب ماشین مناسب

در بازار GPU فیلتری به اسم «تعداد GPU» هست؛ آن را روی ۲ یا ۴ بگذارید. روی کارت هر ماشین، تعداد کارت‌ها کنار نام مدل با علامت × نوشته شده (مثلاً RTX 3090 ×2) و قیمت نمایش‌داده‌شده، قیمت ساعتی کل ماشین است. قیمت‌های زیر در زمان نوشتن این مطلب است و ارزان‌ترین پیشنهاد هر گروه را نشان می‌دهد:

ماشین VRAM کل از حدود (تومان در ساعت)
RTX 3060 ×1 ۱۲ گیگ ۲۱ هزار
RTX 3060 ×2 ۲۴ گیگ ۴۱ هزار
RTX 3090 ×1 ۲۴ گیگ ۵۴ هزار
RTX 3090 ×2 ۴۸ گیگ ۱۰۰ هزار
RTX 3090 ×4 ۹۶ گیگ ۳۰۹ هزار
RTX 4090 ×1 ۲۴ گیگ ۱۲۵ هزار
RTX 4090 ×2 ۴۸ گیگ ۲۴۳ هزار

پیشنهاد اقتصادی: دو کارت RTX 3060 برای یادگرفتن و آزمایش کد چندکارته کافی است. پیشنهاد جدی: دو کارت RTX 3090 با ۴۸ گیگ VRAM کل، برای مدل‌های زبانی بزرگ و آموزش واقعی. توجه کنید حافظهٔ هر کارت جداست؛ دو کارت ۲۴ گیگ یعنی دو تکهٔ ۲۴ گیگی، نه یک کارت ۴۸ گیگی. نرم‌افزار باید بلد باشد مدل را بین آن‌ها تقسیم کند.

اجاره و راه‌اندازی

قالب را بر اساس کاری که می‌خواهید بکنید انتخاب کنید: برای آموزش با کد خودتان PyTorch، برای سرو مدل زبانی با API قالب vLLM، و برای چت ساده با مدل‌های آماده Ollama. بعد دکمهٔ اجاره را بزنید و صبر کنید وضعیت ماشین «آماده» شود. در صفحهٔ ماشین دستور SSH آماده را کپی کنید و وارد شوید. برای PyTorch لینک Jupyter هم همان‌جا ظاهر می‌شود و می‌توانید ترمینال Jupyter را باز کنید.

مرحلهٔ ۱: مطمئن شوید همهٔ کارت‌ها دیده می‌شوند

اولین دستور روی هر ماشین چندکارته این است:

nvidia-smi

باید به تعداد کارت‌هایی که اجاره کرده‌اید سطر ببینید، با شماره‌های ۰ و ۱ و الی آخر. بعد ببینید کارت‌ها چطور به هم وصل‌اند:

nvidia-smi topo -m

در جدول خروجی، اگر بین دو کارت NV و یک عدد ببینید یعنی با NVLink وصل‌اند و ارتباطشان سریع است. PIX، PXB، PHB یا SYS یعنی ارتباط از مسیر PCIe است؛ کار می‌کند ولی کندتر. برای کارهای مستقل این فرقی نمی‌کند؛ برای تقسیم یک مدل بین کارت‌ها مهم است. در قالب PyTorch با این دستور هم چک کنید:

python -c "import torch; print(torch.cuda.device_count())"

مرحلهٔ ۲: کارهای مستقل، هر کارت یک پردازش

ساده‌ترین و پرفایده‌ترین الگو این است. متغیر محیطی CUDA_VISIBLE_DEVICES تعیین می‌کند هر برنامه کدام کارت را ببیند. اگر یک اسکریپت دارید که روی یک کارت کار می‌کند، دو نسخه‌اش را هم‌زمان روی دو کارت اجرا کنید و ورودی را نصف کنید:

CUDA_VISIBLE_DEVICES=0 nohup python run.py --part 1 > part1.log 2>&1 &
CUDA_VISIBLE_DEVICES=1 nohup python run.py --part 2 > part2.log 2>&1 &

این‌جا run.py و آرگومان --part مال اسکریپت خودتان است. هر پردازش فکر می‌کند فقط یک کارت دارد و کارت شمارهٔ ۰ را می‌بیند. با nvidia-smi چک کنید که هر دو کارت مشغول‌اند. nohup باعث می‌شود اگر اتصال SSH قطع شد، کار ادامه پیدا کند.

مرحلهٔ ۳: تقسیم یک مدل زبانی بین کارت‌ها

با vLLM

vLLM با گزینهٔ --tensor-parallel-size لایه‌های مدل را بین کارت‌ها تقسیم می‌کند. مدل Qwen2.5-14B-Instruct با دقت کامل حدود ۳۰ گیگابایت است و روی یک کارت ۲۴ گیگ جا نمی‌شود؛ روی دو کارت ۲۴ گیگ این‌طور بالا می‌آید:

vllm serve Qwen/Qwen2.5-14B-Instruct --tensor-parallel-size 2 --max-model-len 8192

عدد --tensor-parallel-size را برابر تعداد کارت‌ها بگذارید. دانلود مدل روی ماشین با سرعت بالا انجام می‌شود، چون ماشین‌ها خارج از ایران‌اند و به اینترنت شما ربطی ندارد. بعد از بالا آمدن، API سازگار با OpenAI روی پورت ۸۰۰۰ در دسترس است؛ نحوهٔ اتصال در vLLM روی GPU ابری: اجرای مدل زبانی با API اختصاصی آمده است.

با Ollama

Ollama خودش تصمیم می‌گیرد. طبق مستندات رسمی، اگر مدل روی یک کارت جا شود همان‌جا بارگذاری می‌شود و اگر نه، بین همهٔ کارت‌ها پخش می‌شود. پس روی دو کارت ۲۴ گیگ کافی است بزنید:

ollama run llama3.3:70b

این مدل حدود ۴۳ گیگابایت است و دانلودش چند دقیقه طول می‌کشد. وقتی بالا آمد، nvidia-smi نشان می‌دهد حافظهٔ هر دو کارت پر شده است.

مرحلهٔ ۴: آموزش موازی با PyTorch DDP

برای آموزش، روش استاندارد PyTorch کلاس DistributedDataParallel است. هر کارت یک نسخه از مدل دارد، تکهٔ خودش از داده را می‌بیند و گرادیان‌ها بعد از هر قدم بین کارت‌ها یکی می‌شوند. این یک اسکریپت کامل و کوچک است؛ با نام train_ddp.py ذخیره‌اش کنید:

import os
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, TensorDataset
from torch.utils.data.distributed import DistributedSampler

dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)

data = TensorDataset(torch.randn(20000, 100), torch.randn(20000, 1))
sampler = DistributedSampler(data)
loader = DataLoader(data, batch_size=256, sampler=sampler)

model = torch.nn.Sequential(
    torch.nn.Linear(100, 512), torch.nn.ReLU(), torch.nn.Linear(512, 1)
).cuda()
model = DDP(model, device_ids=[local_rank])
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
loss_fn = torch.nn.MSELoss()

for epoch in range(5):
    sampler.set_epoch(epoch)
    for x, y in loader:
        x, y = x.cuda(), y.cuda()
        opt.zero_grad()
        loss = loss_fn(model(x), y)
        loss.backward()
        opt.step()
    if dist.get_rank() == 0:
        print(f"epoch {epoch} loss {loss.item():.4f}")

if dist.get_rank() == 0:
    torch.save(model.module.state_dict(), "model.pt")
dist.destroy_process_group()

و با torchrun روی همهٔ کارت‌ها اجرایش کنید:

torchrun --standalone --nproc_per_node=2 train_ddp.py

عدد --nproc_per_node را برابر تعداد کارت‌ها بگذارید. چند نکتهٔ مهم: sampler.set_epoch باعث می‌شود ترتیب داده در هر epoch عوض شود؛ ذخیره و چاپ فقط در پردازش شمارهٔ صفر انجام می‌شود تا فایل‌ها روی هم نوشته نشوند؛ و اندازهٔ دستهٔ مؤثر، ۲۵۶ ضرب در تعداد کارت‌هاست. اگر از Hugging Face Trainer یا Axolotl استفاده می‌کنید، همین torchrun یا ابزار راه‌انداز خودشان کار تقسیم را انجام می‌دهد.

اگر به مشکل خوردید

  • فقط کارت ۰ مشغول است: کدتان چندکارته نیست. یا الگوی مرحلهٔ ۲ را به کار ببرید، یا با torchrun و DDP اجرا کنید. model.cuda() به‌تنهایی همیشه فقط یک کارت را می‌گیرد.
  • برنامه در شروع گیر می‌کند و جلو نمی‌رود: روی برخی کارت‌های خانگی ارتباط مستقیم کارت‌به‌کارت (P2P) درست کار نمی‌کند. همان دستور را با NCCL_P2P_DISABLE=1 در ابتدایش اجرا کنید؛ ارتباط از مسیر حافظهٔ سیستم انجام می‌شود.
  • خطای CUDA out of memory روی کارت ۰: معمولاً پردازش‌های دیگر هم روی کارت ۰ حافظه گرفته‌اند. مطمئن شوید torch.cuda.set_device(local_rank) قبل از ساخت مدل صدا زده شده است.
  • vLLM می‌گوید تعداد attention head بر تعداد کارت بخش‌پذیر نیست: --tensor-parallel-size باید عددی مثل ۲، ۴ یا ۸ باشد؛ ماشین سه‌کارته برای این حالت مناسب نیست.

هزینه و صرفه‌جویی

ماشین چندکارته گران‌تر است، پس وقت مرده‌اش هم گران‌تر تمام می‌شود. قبل از اجاره، کد را روی یک ماشین ارزان تک‌کارته آزمایش کنید و فقط برای اجرای اصلی سراغ چندکارته بروید. برای همین آموزش، یک ساعت روی دو کارت RTX 3060 کافی است. بعد از تمام شدن کار ماشین را متوقف یا حذف کنید؛ ماشین متوقف‌شده هزینهٔ ساعتی ندارد. دیسک ماشین با حذف پاک می‌شود، پس checkpointها و خروجی را قبلش با scp یا rsync دانلود کنید.

قدم بعدی

اگر هدفتان چت با مدل بزرگ است، سراغ اجرای مدل زبانی با Ollama روی GPU ابری بروید و همان را روی ماشین دوکارته امتحان کنید. اگر می‌خواهید مدل خودتان را آموزش دهید، آموزش مدل زبانی با Axolotl روی GPU ابری: قدم‌به‌قدم گام منطقی بعدی است.

پرسش‌های متداول

آیا دو کارت گرافیک سرعت را دو برابر می‌کند؟

فقط اگر کد از هر دو استفاده کند. در کارهای مستقل و آموزش با DDP سرعت نزدیک به دو برابر می‌شود. در تقسیم یک مدل بین کارت‌ها، هدف اصلی جا شدن مدل است و سرعت به نوع اتصال کارت‌ها بستگی دارد.

آیا حافظهٔ دو کارت گرافیک با هم جمع می‌شود؟

نه خودبه‌خود. هر کارت حافظهٔ جدای خودش را دارد. ابزارهایی مثل vLLM و Ollama مدل را بین کارت‌ها تقسیم می‌کنند تا عملاً از مجموع حافظه استفاده شود.

چطور بفهمم برنامه‌ام از همهٔ GPUها استفاده می‌کند؟

در یک ترمینال دیگر nvidia-smi را اجرا کنید. اگر ستون مصرف GPU و حافظهٔ همهٔ کارت‌ها بالا باشد، همه مشغول‌اند؛ اگر فقط کارت ۰ کار می‌کند، کدتان تک‌کارته است.

برای اجرای مدل ۷۰ میلیاردی چند کارت گرافیک لازم است؟

با کوانتیزه‌سازی ۴ بیتی، دو کارت ۲۴ گیگ کافی است. با دقت کامل بیش از ۱۴۰ گیگابایت VRAM لازم است که برای کاربرد معمولی منطقی نیست.

تصویر کاور: «Sled dog team out for a run (53515923134)» اثر BLM Alaska، از ویکی‌مدیا کامنز، لایسنس Public Domain.