পাঠ ১০ · ৪০-এর মধ্যে · মডিউল ২
Home / AI Courses / ডিপ লার্নিং / Computational graph

Computational graph

Computational graphs — the engine behind autograd
৬ মিনিট পড়া মাঝারি · Intermediate PyTorch hands-on

এই পাঠে যা শিখবেন

  • Computational graph কী, কেন গুরুত্বপূর্ণ
  • Dynamic vs static graph — দুই philosophy
  • PyTorch-এ .grad_fn দেখা
  • Graph visualization — torchviz দিয়ে
  • detach(), no_grad — graph নিয়ন্ত্রণ

১ · Computational graph কী

একটি computational graphComputational Graphএকটি Directed Acyclic Graph (DAG) যেখানে node = operation/variable, edge = data dependency। DL framework-এ autograd-এর basis। = একটি গণিত expression-এর graph representation। Node হলো operation বা variable, edge হলো data flow।

যেমন $f(x, y) = (x + y) \cdot x$ — এর graph:

  • Leaf node: $x, y$ (input variables)।
  • Add node: $u = x + y$।
  • Mul node: $f = u \cdot x$।
  • Edge: data flow direction।
কেন graph দরকার

১) Modularity: প্রতিটি op self-contained — local gradient জানে।
২) Automatic differentiation: graph traverse করেই backprop।
৩) Optimization: graph-এ pattern detect — fusion, pruning।
৪) Hardware mapping: graph distribute হয় GPU/TPU-তে।

২ · Forward — graph build

যখন আপনি একটি tensor expression লেখেন — PyTorch nodes তৈরি করে। প্রতিটি tensor-এর সাথে যুক্ত থাকে grad_fn — কোন operation এটি তৈরি করল।

Python · PyTorch
import torch

x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

u = x + y           # AddBackward
f = u * x           # MulBackward

print("f =", f.item())
print("f.grad_fn =", f.grad_fn)
print("u.grad_fn =", u.grad_fn)
print("x.grad_fn =", x.grad_fn)   # None (leaf)

# Previous nodes দেখুন
print("\nf.grad_fn.next_functions:")
for fn, _ in f.grad_fn.next_functions:
    print(" ", fn)

    

grad_fn থেকে আপনি পুরো graph backward-traverse করতে পারেন। PyTorch internal-এ ঠিক এটাই করে।

৩ · Backward — graph traverse

loss.backward() call-এ কী ঘটে?

  1. Topological sort: graph-এর nodes কোন order-এ visit করতে হবে — output থেকে input পর্যন্ত।
  2. Initialize: output node-এ $\frac{\partial L}{\partial L} = 1$।
  3. Visit each node: upstream gradient × local gradient → downstream-এ পাঠাও।
  4. Accumulate: একই leaf-এ একাধিক path থাকলে gradient যোগ করো।
  5. Store: leaf tensors-এর .grad-এ result।
Multiple path (যেমন একই tensor দু'বার ব্যবহৃত) → gradients যোগ হয়। এটি chain rule-এর "sum over paths" rule। যেমন $f = x \cdot x$-এ $\frac{df}{dx} = 2x$ — দু'টি path-এর সমষ্টি।

৪ · Dynamic vs Static graph

ML framework দু'রকম philosophy অনুসরণ করে:

Dynamic graph (PyTorch, JAX-এর এক mode):

  • প্রতিটি forward pass-এ নতুন graph build।
  • Python control flow (if/for/while) স্বাভাবিক ভাবে কাজ করে।
  • Debug সহজ — print, breakpoint কাজ করে।
  • Cost: প্রতিবার build overhead।

Static graph (TF 1.x, ONNX, TorchScript):

  • Graph একবার define হয়, বহুবার run।
  • Optimization possible — kernel fusion, dead code elimination।
  • Production-friendly — serialize, deploy।
  • Cost: control flow-এ awkward (placeholder, condition op)।
Dynamic graph = interpreter-এর মতো (Python) — প্রতি লাইনে রান-টাইম decision। Static graph = compiler-এর মতো (C++) — ahead-of-time optimize করে। প্রথমটি flexibility দেয়, দ্বিতীয়টি speed।

৫ · PyTorch — best of both

আধুনিক PyTorch (২.০+) torch.compile দিয়ে dynamic graph-কে JIT-compile করে — research-এ flexibility, production-এ speed।

Python · PyTorch 2.0
import torch
import torch.nn as nn

# Normal model
model = nn.Sequential(
    nn.Linear(10, 64),
    nn.ReLU(),
    nn.Linear(64, 1),
)

# JIT compile — graph optimization
fast_model = torch.compile(model)

x = torch.randn(32, 10)
y = fast_model(x)        # প্রথম call-এ compile, পরে fast
print(y.shape)

    

৬ · Graph visualization — torchviz

Graph "দেখতে" — torchviz library:

Python · torchviz
# pip install torchviz
import torch
from torchviz import make_dot

x = torch.tensor(2.0, requires_grad=True)
y = torch.tensor(3.0, requires_grad=True)

z = x ** 2 + 2 * x * y + y ** 3
dot = make_dot(z, params={'x': x, 'y': y})
dot.render('graph', format='png')   # graph.png save

# অথবা notebook-এ display
# dot

    
Computational graph: f(x,y) = (x+y)·x — y² এর সাথে leaves (blue), ops (green), output (red) x leaf y leaf + u = x+y ² v = y² · w = u·x + f = w+v x reused Backward — output থেকে শুরু, gradient propagate। Multiple path → sum।
Graph DAG: একই variable একাধিক জায়গায় ব্যবহার হলে gradient যোগ হয়।

৭ · Detach ও no_grad — graph নিয়ন্ত্রণ

কখনো আমরা graph build চাই না (যেমন inference) বা graph থেকে subgraph "কাটতে" চাই (transfer learning)। দুটি tool:

Python · PyTorch
import torch

x = torch.tensor(2.0, requires_grad=True)

# detach — graph থেকে বিচ্ছিন্ন copy
y = x.detach()
print(y.requires_grad)   # False
# y-এর কোনো gradient flow হবে না

# no_grad context — graph build off
with torch.no_grad():
    z = x ** 2 + 5
    print(z.requires_grad)   # False

# inference mode — আরও aggressive
with torch.inference_mode():
    z = x ** 2 + 5
    # version counting, autograd তৈরি বন্ধ — fastest

# eval mode — model-এর জন্য (Dropout/BN behavior)
# model.eval() ≠ no_grad — দু'টোই দরকার inference-এ

    
Inference-এ model.eval() + torch.no_grad() দু'টোই দরকার। প্রথমটি Dropout/BatchNorm-এর behavior switch, দ্বিতীয়টি graph build বন্ধ — memory ও speed দু'টোই improve।

৮ · Memory — graph-এর hidden cost

Backprop-এর জন্য সব intermediate activation graph-এ সংরক্ষিত থাকে। Deep network-এ memory $O(\text{depth})$।

  • 50-layer network, batch 32: activations memory প্রায় 4× model-এর weight memory।
  • Solution: gradient checkpointing — কিছু activation save, বাকি forward-এ recompute।
  • Trade-off: 30% বেশি compute, 50%+ কম memory।
Python · PyTorch
import torch
import torch.nn as nn
from torch.utils.checkpoint import checkpoint

class DeepBlock(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(
            *[nn.Linear(512, 512) for _ in range(20)]
        )
    def forward(self, x):
        # সাধারণ forward
        # return self.layers(x)
        # checkpoint দিয়ে — memory save
        return checkpoint(self.layers, x, use_reentrant=False)

model = DeepBlock()
x = torch.randn(64, 512, requires_grad=True)
y = model(x)
loss = y.sum()
loss.backward()
print("OK — checkpoint working")

    

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ TensorFlow ১.x static graph থেকে TF ২.x eager mode-এ গেল PyTorch-এর সাথে compete করতে। PyTorch আবার torch.compile দিয়ে static-এর benefit আনল। এই convergence কী বলে DL framework design-এ?

AI framework history-র সবচেয়ে fascinating arc। ২০১৫-২০২০ TF dominant ছিল production-এ, PyTorch research-এ। এখন PyTorch দু'টোতেই lead। কারণ design philosophy-র বিবর্তন।

Static graph (TF 1.x)-এর rationale:

  • Compiler-style optimization — kernel fusion, constant folding।
  • Distributed training — partition automatic।
  • Cross-platform deployment — mobile, web।
  • Production-grade serving (TF Serving)।

Static graph-এর pain:

  • Define-then-run — debug nightmare।
  • Control flow বিকৃত — tf.cond, tf.while।
  • Variable scoping এক চ্যালেঞ্জ।
  • Researcher-দের জন্য productivity killer।

Dynamic graph (PyTorch)-এর win:

  • Pythonic — control flow স্বাভাবিক।
  • Print, breakpoint কাজ করে।
  • Quick prototyping।
  • Research community দ্রুত adoption।

Dynamic-এর cost:

  • Per-iteration build overhead।
  • Optimization opportunity miss।
  • Hardware-specific code generation কঠিন।

Convergence: best of both:

  • TF 2.x eager mode default — PyTorch-এর experience।
  • tf.function decorator — selective static compilation।
  • PyTorch torch.jit.script — early static option।
  • torch.compile (২০২৩) — TorchInductor backend।
  • JAX — pure functional, jit-compilable।

Lesson: trace-then-compile:

  • Eager execution → trace operations → compile graph → run optimized।
  • Best abstraction — user pythonic, system optimized।

Why PyTorch won research:

  • Cleaner API — fewer abstractions।
  • NumPy-like — familiar।
  • Hugging Face, FastAI — ecosystem।
  • Meta-র aggressive open source push।
  • Academic citation pattern — researcher A use → researcher B follow।

Why production catching up:

  • TorchServe, TorchScript — deployment।
  • ONNX export — cross-framework।
  • Mobile (PyTorch Mobile) — edge।
  • torch.compile — production speed।

JAX-এর position:

  • Functional pure — research-এ elegance।
  • XLA compile — TPU-তে supreme।
  • DeepMind, Google research preferred।
  • Industry adoption সীমিত — learning curve।

Future trend:

  • MLIR (Multi-Level IR) — universal compilation।
  • Hardware-software co-design।
  • OpenAI Triton — high-level kernel programming।
  • Framework-agnostic models (HuggingFace adapters)।

মূল উপলব্ধি: "Static vs dynamic" false dichotomy। Modern framework — eager-by-default, compile-on-demand। Productivity ও performance দু'টোই pursue। Bangladesh-এ — PyTorch start, JAX explore, TF legacy deal — তিন framework ই জানা সমৃদ্ধ।

প্র ০২ Computational graph ML-এর বাইরে — physics simulation, finance, robotics-এ কেমন ব্যবহৃত? Differentiable programming কী ধরনের ক্ষেত্রে revolution আনছে?

Computational graph শুধু DL-এর tool না — যেকোনো differentiable computation-এর foundation। "Differentiable programming" এই broad framework।

Differentiable programming concept:

  • প্রোগ্রামকে এমনভাবে লেখা যাতে input-এর সাপেক্ষে output-এর gradient compute সম্ভব।
  • "Software 2.0" — Karpathy-এর term।
  • Backprop = differentiable programming-এর special case।

Application: Physics simulation:

  • Differentiable physics simulator (DiffTaichi, Brax)।
  • Soft body, fluid, rigid body — সবই differentiable।
  • Robot policy directly through simulation gradient learn।
  • Material property infer — observed deformation থেকে।

Application: Finance:

  • Option pricing — Black-Scholes graph differentiable।
  • Greeks (sensitivity) — auto-compute।
  • Portfolio optimization — gradient-based।
  • Risk model calibration — backprop through Monte Carlo।

Application: Robotics:

  • Differentiable kinematics — joint angle থেকে end-effector।
  • Inverse kinematics — gradient descent।
  • Policy learning — sim-to-real।
  • Motion planning gradient-based।

Application: Computer graphics:

  • Differentiable rendering — image থেকে 3D recover।
  • NeRF (Neural Radiance Fields) — volumetric rendering।
  • Material/lighting estimation।
  • Inverse graphics — vision-এর dual।

Application: Probabilistic programming:

  • Pyro, NumPyro — Bayesian model।
  • Variational inference — gradient-based।
  • Stan-এর modern alternative।

Application: Scientific computing:

  • Climate model gradient-aware।
  • Differential equation solver — neural ODE।
  • Quantum circuit optimization।
  • Drug discovery — molecular property gradient।

JAX-এর role:

  • Pure functional — composable transformations।
  • jit, vmap, pmap, grad — primitive composability।
  • NumPy API — scientific community familiar।
  • DeepMind AlphaFold internal — JAX।

Bangladesh research opportunity:

  • Cyclone simulation differentiable — Bangladesh climate research।
  • Agriculture yield prediction — physics + ML hybrid।
  • River dynamics modeling।
  • Garment supply chain optimization।

Limitations:

  • সব function differentiable না (discrete, conditional)।
  • Soft relaxation often required।
  • Gumbel-softmax, straight-through estimator — tricks।

মূল উপলব্ধি: Computational graph just DL tool নয় — universal computational paradigm। Differentiable programming আগামী দশকে scientific computing transform করবে। Just learning ML থেকে এটি বহু broader।

প্র ০৩ retain_graph=True কেন কখনো কখনো লাগে? "Trying to backward through the graph a second time" error কেন আসে?

PyTorch-এর সবচেয়ে confusing error message-গুলোর একটি। বুঝতে — memory model-এ যেতে হবে।

Default behavior:

  • backward()-এর পর graph-এর intermediate buffer free হয়।
  • Memory save — common case (single backward per forward)।
  • আবার backward() call → buffer নেই → error।

The classic error:

x = torch.tensor(2.0, requires_grad=True)
y = x ** 2

y.backward()      # OK — x.grad = 4
y.backward()      # RuntimeError: backward through graph second time

Solutions:

  • Option 1: retain_graph=True
    y.backward(retain_graph=True)
    y.backward()      # works, x.grad = 8 (accumulated)
  • Option 2: Recompute
    y = x ** 2
    y.backward()
    y = x ** 2        # rebuild
    y.backward()

কখন দরকার:

  • Multiple losses, shared graph:
    shared = encoder(x)
    loss1 = head1(shared).mean()
    loss2 = head2(shared).mean()
    loss1.backward(retain_graph=True)
    loss2.backward()
    অথবা better: (loss1 + loss2).backward()।
  • GAN training:
    # Discriminator update
    fake = G(z)
    d_loss = ...
    d_loss.backward()
    
    # Generator update — same fake!
    g_loss = ...
    g_loss.backward()    # error if same graph
    Solution: fake.detach() in D-loss।
  • Higher-order gradients:
    y.backward(create_graph=True)  # auto-retain
    grad_x = x.grad
    grad2 = torch.autograd.grad(grad_x, x)[0]
  • Custom training loops: meta-learning, MAML।

Pitfall — memory leak:

  • retain_graph=True overuse → graph accumulate, OOM।
  • Solution: explicit del graph_tensor বা scope-এ end।

Better patterns:

  • Combine losses: (l1 + l2 + l3).backward() — single backward।
  • Use autograd.grad instead of backward when partial gradient needed।
  • detach aggressively — clear non-needed graph parts।

Debug strategy:

  • Error হলে — কোন tensor বা loss double-backward হচ্ছে identify।
  • Computation graph print: print(loss.grad_fn)।
  • Hook দিয়ে gradient flow trace।

Performance impact:

  • retain_graph — buffer keep, memory cost।
  • create_graph — backward graph build, even more memory।
  • Inference-এ no_grad mandatory — no graph at all।

মূল উপলব্ধি: Default behavior memory-efficient। retain_graph escape hatch — sparingly use। Better — graph design rethink (single combined loss, detach proper)। GAN, meta-learning-এ unavoidable। Beginner-দের জন্য — error দেখলে graph restructure আগে চেষ্টা করুন।

প্র ০৪ আপনি একটি ৫০০-layer Transformer train করছেন। GPU memory অপ্রতুল। Gradient checkpointing কীভাবে save করে — trade-off কী?

Modern LLM training-এর backbone technique। GPT-3, LLaMA — সবই checkpoint-এ নির্ভরশীল।

Memory problem:

  • Backprop-এর জন্য সব activation save।
  • 50-layer Transformer, 2048 seq, 4096 hidden — ভয়ংকর memory।
  • Activation memory প্রায় $O(\text{layers} \times \text{batch} \times \text{seq} \times \text{hidden})$।

Standard approach without checkpoint:

  • Forward — সব layer activation save।
  • Backward — saved activation read, gradient compute।
  • Memory: O(L) activations।

Gradient checkpointing core idea:

  • Forward — কিছু "checkpoint" save, বাকি discard।
  • Backward — discarded part recompute (forward again from checkpoint)।
  • Memory ↓, compute ↑।

Math: optimal checkpointing:

  • Naive: every layer checkpoint → memory O(L), no save।
  • Sqrt strategy: $\sqrt{L}$ checkpoints → memory $O(\sqrt{L})$, compute $1.5\times$।
  • Chen et al. (২০১৬) — recursive scheme, memory $O(\log L)$।

PyTorch implementation:

from torch.utils.checkpoint import checkpoint

class Block(nn.Module):
    def forward(self, x):
        # heavy computation
        ...

# Without checkpoint
y = block(x)

# With checkpoint
y = checkpoint(block, x, use_reentrant=False)
# Forward — block-এর activation drop
# Backward — block recompute, then gradient

Sequential checkpointing:

from torch.utils.checkpoint import checkpoint_sequential

# Auto-divide layers into segments
y = checkpoint_sequential(model.layers, segments=4, input=x)
# 4 checkpoints — memory O(L/4), compute 1.25x

Trade-off quantification:

  • 50-layer, sqrt strategy → memory ÷ 7, compute × 1.5।
  • Practically — 30-50% memory save, 25-35% compute overhead।
  • Larger batch possible → better GPU utilization compensates।

When to use:

  • OOM error during training।
  • Want larger batch।
  • Want longer sequence।
  • Want bigger model।
  • Compute budget allows।

When NOT to use:

  • Memory enough — pure waste।
  • Compute-bound, latency-sensitive।
  • Inference (no backward — no need)।

Combination with other techniques:

  • Mixed precision (FP16/BF16): activation memory ÷ 2।
  • ZeRO (DeepSpeed): optimizer state shard।
  • Tensor parallelism: activation across GPUs।
  • FlashAttention: attention memory linear in seq।

Modern LLM training stack:

  • Activation checkpointing + ZeRO + FP16 + Tensor parallel।
  • 13B model train possible on consumer multi-GPU।
  • 70B+ → cluster needed।

Bangladesh perspective:

  • Limited GPU (Colab, RTX 3090) → checkpointing critical।
  • Bangla LLM fine-tune (LLaMA-2 7B) → ১২GB GPU + checkpoint workable।
  • Research lab — multi-GPU + DeepSpeed।

Pitfalls:

  • Random ops (dropout) — recompute different result! Use seeded RNG।
  • use_reentrant=False recommended (২.০+) — avoid hidden bugs।
  • Profile carefully — সব time-এ benefit guaranteed না।

মূল উপলব্ধি: Gradient checkpointing — memory ↔ compute trade। Modern LLM-এ "must"। Combination with FP16, distributed training বহু large model-কে accessible করেছে। Memory-constrained scenario-তে first reach for this tool।

অনুশীলন

  1. Graph trace: $f = (x + y) \cdot (y + 2)$, $x = 1, y = 2$। PyTorch-এ f.grad_fn ও prev nodes কী হবে?

    f.grad_fn = MulBackward। Prev: দু'টি AddBackward — একটি $x+y$, একটি $y+2$।

    x = torch.tensor(1.0, requires_grad=True)
    y = torch.tensor(2.0, requires_grad=True)
    f = (x + y) * (y + 2)
    print(f.grad_fn)
    for fn, _ in f.grad_fn.next_functions:
        print(" ", fn)
  2. Multiple paths: $f = x^2 + 3x$। $\frac{df}{dx}$ — graph-এ $x$ দু'বার ব্যবহৃত। Manual ও autograd verify করুন।

    $\frac{df}{dx} = 2x + 3$ — দু'টি path-এর সমষ্টি।

    x = torch.tensor(2.0, requires_grad=True)
    f = x ** 2 + 3 * x
    f.backward()
    print(x.grad)   # 2(2) + 3 = 7
  3. no_grad ব্যবহার: একটি model-এর inference loop লিখুন যেখানে graph build হবে না।
    model.eval()
    with torch.no_grad():
        for x_batch in test_loader:
            y_pred = model(x_batch)
            # graph build না, memory save

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

কোড রানার কাজ না করলে? ব্রাউজারে কাজ না করলে Google Colab ব্যবহার করুন — Google-এর ফ্রি অনলাইন Python পরিবেশ, শুধু Gmail অ্যাকাউন্ট লাগে।
পূর্ববর্তী পাঠ
পাঠ ০৯ · Backpropagation