چیت‌شیت

JAX در برابر PyTorch

JAX against PyTorch

مشتق‌گیری خودکار نواری و بلادرنگ در برابر توابع خالص با ردگیری و کامپایل‌شده. پیاده‌سازی یک MLP مشابه برای MNIST در هر دو فریم‌ورک همراه با APIها و نکات مهم.

  • ۱۱ بخش
  • ۳ درس مرتبط
  • کد و دستورها عیناً از دوره‌ی اصلی
فهرست مطالب
  1. مقایسه شانه به شانه
  2. وظیفه یکسان، دو API
  3. دو مدل اجرا
  4. یک گام آموزش JAX
  5. حلقه آموزش MLP برای MNIST در PyTorch
  6. حلقه آموزش MLP برای MNIST در JAX
  7. تبدیل‌های JAX
  8. نصب و اجرا
  9. نکات و خطاهای رایج
  10. کدام را چه زمانی انتخاب کنیم
  11. پرسش‌ها

مقایسه شانه به شانه

مقایسه
معیارPyTorchJAX
مشتق‌گیری خودکارمشتق‌گیری خودکار مبتنی بر نوار (Tape)تبدیل‌های تابعی
اجرابلادرنگ / Eager (کرنل‌های C++)ردگیری محاسبات + کامپایل‌شده با JIT
مدلnn.Moduleتابع خالص: f(params, x) -> y
پشتیبانی از TPUنگهداری‌شده توسط جامعه کاربری (torch_xla)درجه یک (پیش‌فرض و بومی)
پشتیبانی از GPUبهترین در این رده (CUDA بومی)خوب (CUDA از طریق XLA)
دیباگآسان (بلادرنگ، خط به خط)دشوار (ردگیری محاسبات + کامپایل)
سرعت (MLP روی MNIST)~0.5 ثانیه بر ایپاک~0.3 ثانیه بر ایپاک
چنددستگاهیFSDPjax.pmap(f)

وظیفه یکسان، دو API

مقایسه
وظیفهPyTorchJAX
محاسبه گرادیان‌هاloss.backward()jax.grad(loss_fn)(params, x, y)
صفر کردن گرادیان‌هاoptimizer.zero_grad()نیاز نیست. گرادیان‌ها مقدار بازگشتی هستند
به‌روزرسانی پارامترهاoptimizer.step()optax.apply_updates(params, updates)
بهینه‌ساز Adamtorch.optim.Adam(model.parameters(), lr=1e-3)optax.adam(learning_rate=1e-3)
دسته‌بندی روی نمونه‌هاfor x in batch:jax.vmap(f)
نگه‌داری پارامترهاmodel.parameters()پای‌تری تغییرناپذیر از آرایه‌ها
بذردهی تصادفیtorch.manual_seedjax.random.PRNGKey(42)
ذخیره وزن‌هاtorch.save(model.state_dict(), "model.pt")checkpointer.save('/tmp/model', params)

دو مدل اجرا

معماری
عملیات‌ها.gradردگیری محاسباتmodel(x)اجرای آنی و بلادرنگنوار (Tape)ثبت هر عملیاتloss.backward()بازپخش معکوس نوارoptimizer.step()تغییر مستقیم پارامترهاتابع خالصf(params, x) -> yjax.gradتابع محاسبه گرادیان را برمی‌گرداندjax.jitدر نخستین فراخوانی ردگیری محاسبات را انجام می‌دهدXLAکد ماشین ادغام‌شده

یک گام آموزش JAX

گام‌به‌گام
  1. init_paramsدستور random.split(key, 3) سه کلید می‌دهد. مقیاس He برابر با jnp.sqrt(2.0 / 784) است.
  2. optimizer.initدستور opt_state = optimizer.init(params) وضعیت بهینه‌ساز Adam را نگه می‌دارد.
  3. jax.value_and_gradتابع زیان (loss) و گرادیان‌ها را در یک دور رفت برمی‌گرداند.
  4. optimizer.updateگرادیان‌ها را به مقادیر به‌روزرسانی و یک opt_state جدید تبدیل می‌کند.
  5. optax.apply_updatesپارامترهای جدید را برمی‌گرداند. هیچ متغیری مستقیماً دستکاری نمی‌شود.
  6. نخستین فراخوانیجی‌اکس train_step را ردگیری و به XLA کامپایل می‌کند. ایپاک اول کند است.
  7. فراخوانی‌های بعدیپایتون را دور می‌زند. ایپاک‌های 2 تا 10 سریع هستند. 10 ایپاک حدود 97٪ دقت آزمون می‌دهد.

حلقه آموزش MLP برای MNIST در PyTorch

کد
python
model = MNISTModel().to(device)                  # 784, 256, 128, 10
criterion = nn.CrossEntropyLoss()               # لاجیت‌های خام را دریافت می‌کند
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(10):
    model.train()                               # دراپ‌اوت فعال است
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()                   # گرادیان‌ها انباشته می‌شوند
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()                         # نوار محاسباتی را بازپخش می‌کند
        optimizer.step()
torch.save(model.state_dict(), "mnist_mlp.pt")  # ~97.8٪ دقت آزمون

حلقه آموزش MLP برای MNIST در JAX

کد
python
optimizer = optax.adam(learning_rate=1e-3)
@jax.jit                                                             # کامپایل به XLA
def train_step(params, opt_state, x, y):
    loss, grads = jax.value_and_grad(loss_fn)(params, x, y)
    updates, opt_state = optimizer.update(grads, opt_state, params)
    params = optax.apply_updates(params, updates)
    return params, opt_state, loss                                   # خروجی وضعیت جدید
key = random.PRNGKey(0)
params = init_params(key)                                            # پای‌تری با دیکشنری‌های تودرتو
opt_state = optimizer.init(params)
for epoch in range(n_epochs):
    key, subkey = random.split(key)                                  # کلید جدید در هر ایپاک
    perm = random.permutation(subkey, len(X_train))
    params, opt_state, loss = train_step(params, opt_state, xb, yb)  # اندازه دسته: 128

تبدیل‌های JAX

کار و کد
وظیفهکد
مشتق‌گیری از یک تابعjax.grad(f)
مشتق مرتبه دومjax.grad(jax.grad(f))
دسته‌بندی تابعی که تک‌نمونه می‌گیردjax.vmap(predict, in_axes=(None, 0))
گرادیان‌ها به ازای هر نمونهjax.vmap(jax.grad(loss_fn), in_axes=(None, 0, 0))
توزیع دسته بین دستگاه‌هاjax.pmap(train_step, axis_name='devices')
به‌روزرسانی تمام برگ‌هاjax.tree.map(lambda p, g: p - lr * g, params, grads)
مقداردهی به یک عنصرa = a.at[0].set(5)
برش گرادیان، سپس Adamoptax.chain(optax.clip_by_global_norm(1.0), optax.adam(1e-3))

نصب و اجرا

دستورها
دستورعملکرد
pip install jax jaxlib optax flaxنصب JAX، Optax و Flax
pip install jax[cuda12]نصب JAX با پشتیبانی از GPU
pip install jax[tpu] -f <url>نصب JAX برای TPU روی Google Cloud
XLA_PYTHON_CLIENT_PREALLOCATE=falseغیرفعال‌سازی تخصیص اولیه 75٪ از حافظه GPU
jax.debug.print()چاپ پیام داخل یک تابع جیت‌شده
fast_fn(x).block_until_ready()انتظار برای نتیجه قبل از متوقف کردن تایمر
torch.load("model.pt", weights_only=True)بارگذاری دیکشنری وضعیت پارامترها ذخیره‌شده

نکات و خطاهای رایج

عیب‌یابی
  • Expected all tensors to be on the same device

    علت
    مدل و دسته روی دستگاه‌های متفاوتی هستند
    راه‌حل
    انتقال هر دو با استفاده از .to(device)
  • گرادیان‌های نادرست بدون هیچ خطا

    علت
    ارسال خروجی سافت‌مکس به CrossEntropyLoss
    راه‌حل
    ارسال لاجیت‌های خام
  • گام اول کند است

    علت
    کامپایل JIT در نخستین فراخوانی اتفاق می‌افتد
    راه‌حل
    گرم‌کردن اولیه قبل از بنچ‌مارک
  • خطای if x > 0 درون تابع جیت‌شده

    علت
    جریان کنترل برنامه به مقدار تحت ردگیری وابسته است
    راه‌حل
    استفاده از jax.lax.cond
  • کندی حلقه‌های پایتون درون تابع جیت‌شده

    علت
    حلقه زدن روی آرایه‌های JAX
    راه‌حل
    استفاده از jax.lax.scan یا jax.lax.fori_loop
  • خطای a[0] = 5

    علت
    آرایه‌های JAX تغییرناپذیر هستند
    راه‌حل
    نوشتن a = a.at[0].set(5)

کدام را چه زمانی انتخاب کنیم

نکته‌ها
  • از PyTorch استفاده کنید، مگر این‌که دلیل مشخصی برای رفتن به سراغ JAX داشته باشید.
  • از PyTorch برای دیباگ خط به خط استفاده کنید؛ print()، pdb و breakpoint() به‌خوبی کار می‌کنند.
  • از PyTorch برای ساخت پیش‌نمونه با سرعت بالا و دسترسی به اکوسیستم HuggingFace استفاده کنید.
  • از JAX زمانی استفاده کنید که به TPU دسترسی دارید.
  • از JAX برای محاسبه گرادیان‌ها به ازای هر نمونه استفاده کنید.
  • از JAX برای آموزش چنددستگاهی در مقیاس‌های بسیار بزرگ استفاده کنید.

پرسش‌ها

پرسش و پاسخ
چرا JAX به توابع خالص نیاز دارد؟
برای کامپایل JIT ضروری است. ورودی‌های یکسان باید همیشه خروجی‌های یکسان تولید کنند.
چرا در JAX دستوری به نام zero_grad وجود ندارد؟
تابع jax.grad گرادیان‌ها را به‌صورت مقادیر خروجی برمی‌گرداند. تنسورها گراف محاسباتی ذخیره نمی‌کنند.
چرا باید state_dict() را ذخیره کرد نه خود شیء مدل را؟
شیء مدل از pickle استفاده می‌کند. با بازآرایی کد (Refactor)، ساختار pickle می‌شکند.
چرا در ارزیابی از torch.no_grad() استفاده می‌شود؟
مشتق‌گیری خودکار را غیرفعال می‌کند و میزان مصرف حافظه کاهش می‌یابد.
چه مفاهیمی از مینی‌فریم‌ورک متناظر هستند؟
کلاس‌های Sequential و Adam، توابع zero_grad، backward، step، train و eval.