Lightning-AI / Lightning-AI/litgpt

Bitsandbytes quantization for litgpt 2d parallel model (TP+FSDP) within LightningTrainer

Open
#2,115 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

question
Dominant language
Python
Stars
13.7k
Forks
1.5k
Avg merge
15h 37m
Merged PRs (30d)
1

Description

I'm trying to run inference within the LightningTrainer using a litgpt model with 2d parallelization (TP+FSDP) while using a Bitsandbytes precision plugin to enable quantization, however I get into issues.
Here's a sample script I use

```
from __future__ import annotations

from pathlib import Path
from typing import Union

import lightning as L
import litgpt
import torch
from lightning.pytorch.plugins import (
BitsandbytesPrecision,
)
from lightning.pytorch.strategies.model_parallel import ModelParallelStrategy
from litgpt import GPT, Config
from litgpt.model import CausalSelfAttention, GptNeoxMLP, LLaMAMLP, LLaMAMoE
from torch import nn
from torch.distributed._tensor import Replicate, Shard
from torch.distributed.device_mesh import DeviceMesh
from torch.distributed.fsdp import MixedPrecisionPolicy
from torch.distributed.tensor.parallel import (
ColwiseParallel,
RowwiseParallel,
SequenceParallel,
parallelize_module,
)

class RandomTokenDataset(torch.utils.data.Dataset):
def __init__(self, vocab_size: int, seq_length: int):
self.vocab_size = vocab_size
self.seq_length = seq_length
self.tokens = torch.randint(
self.vocab_size,
size=(len(self), self.seq_length + 1),
# Set a seed to make this toy dataset the same on each rank
# Fabric will add a `DistributedSampler` to shard the data correctly
generator=torch.Generator().manual_seed(42),
)

def __len__(self) -> int:
return 128

def __getitem__(self, item: int):
return self.tokens[item][:-1], self.tokens[item][1:]

def _apply_data_parallel(model: GPT, device_mesh: DeviceMesh, mp_policy: MixedPrecisionPolicy):
"""Apply data parallelism using PyTorch FSDP (Fully Sharded Data Parallelism).

Applies FSDP to each transformer block and the inner model, configuring
mixed precision and customized sharding.

Args:
model: HuggingFace PreTrainedModel to apply data parallelism to
device_mesh: Device mesh containing the data_parallel submesh
mp_policy: MixedPrecisionPolicy to apply
"""
from torch.distributed.fsdp import fully_shard

dp_mesh = device_mesh["data_parallel"]

assert dp_mesh.ndim == 1, f"Hybrid-sharding not supported; dp_mesh.ndim ({dp_mesh.ndim}) != 1"

# NOTE: Currently, the user is required to manually handle precision settings such as the `mp_policy` here
# because the model parallel strategy does not respect all settings of `Fabric(precision=...)` at the moment.
fsdp_config = {"mesh": dp_mesh, "mp_policy": mp_policy}

layers = model.transformer.h
for layer_id, transformer_block in enumerate(layers):
# As an optimization, do not reshard after forward for the last transformer block since FSDP would prefetch it immediately
reshard_after_forward = int(layer_id) < len(layers) - 1
fully_shard(
transformer_block,
**fsdp_config,
reshard_after_forward=reshard_after_forward,
)
layers[layer_id] = transformer_block

# shard embedding
model = fully_shard(model, **fsdp_config) # type: ignore

def tensor_parallel_mlp(mesh, mlp: Union[GptNeoxMLP, LLaMAMLP, LLaMAMoE]) -> None:
plan = {}
if isinstance(mlp, LLaMAMLP):
plan["fc_1"] = ColwiseParallel()
plan["fc_2"] = ColwiseParallel()
plan["proj"] = RowwiseParallel()
elif isinstance(mlp, GptNeoxMLP):
plan["fc"] = ColwiseParallel()
plan["proj"] = RowwiseParallel()
elif isinstance(mlp, LLaMAMoE):
# we use expert slicing across ranks, alternatively, we could create a expert parallelism group
# when the number of experts is a multiple of the world size
for expert in mlp.experts:
tensor_parallel_mlp(mesh, expert)
else:
raise NotImplementedError

parallelize_module(mlp, mesh, plan)

def tensor_parallel_attn(mesh: DeviceMesh, attn: CausalSelfAttention) -> None:
def shard(x, dim, world_size, rank):
assert x.size(dim=dim) % world_size == 0
return torch.tensor_split(x, world_size, dim=dim)[rank]

plan = {
"proj": RowwiseParallel(),
}
query_size = attn.config.n_head * attn.config.head_size
key_size = value_size = attn.config.n_query_groups * attn.config.head_size
q, k, v = attn.qkv.weight.split((query_size, key_size, value_size), dim=0)
q = shard(q, 0, mesh["tensor_parallel"].size(), mesh.get_local_rank("tensor_parallel"))
k = shard(k, 0, mesh["tensor_parallel"].size(), mesh.get_local_rank("tensor_parallel"))
v = shard(v, 0, mesh["tensor_parallel"].size(), mesh.get_local_rank("tensor_parallel"))
attn.qkv.weight = nn.Parameter(torch.cat((q, k, v), dim=0), requires_grad=False)
parallelize_module(attn, mesh["tensor_parallel"], plan)

def parallelize(model, device_mesh):
tp_mesh = device_mesh["tensor_parallel"]
dp_mesh = device_mesh["data_parallel"]

assert tp_mesh.size() > 1

plan = {
"transformer.wte": RowwiseParallel(input_layouts=Replicate()),
"transformer.ln_f": SequenceParallel(),
"lm_head": ColwiseParallel(input_layouts=Shard(1), output_layouts=Replicate()),
}
parallelize_module(model, tp_mesh, plan)

for block in model.transformer.h:
plan = {}
tensor_parallel_mlp(tp_mesh, block.mlp)
tensor_parallel_attn(device_mesh, block.attn)

# update the config values to the shard sizes
# this is only relevant for `tensor_parallel_attn`, but it needs to run only once
attrs = ["n_head", "n_embd", "n_query_groups"]
for attr in attrs:
size = getattr(model.config, attr)
if size % tp_mesh.size() != 0:
raise ValueError(f"This {attr} value ({size}) is not evenly divisible by the world size ({tp_mesh.size()})")
print("Setting %s to %s", attr, size // tp_mesh.size())
setattr(model.config, attr, size // tp_mesh.size())

_apply_data_parallel(
model,
dp_mesh,
mp_policy=MixedPrecisionPolicy(
param_dtype=torch.bfloat16,
reduce_dtype=torch.float32,
output_dtype=torch.bfloat16,
),
)

return model

class LitLLM(L.LightningModule):
def __init__(self):
super().__init__()
config = Config.from_checkpoint(Path("Qwen/Qwen3-8B"))
self.model = GPT(config=config)

def configure_model(self):
state_dict = torch.load(
"checkpoints/Qwen/Qwen3-8B/lit_model.pth", map_location="cpu", mmap=True, weights_only=True
)
self.model.load_state_dict(state_dict=state_dict, assign=True)
parallelize(self.model, self.device_mesh)

def predict_step(self, batch):
input_ids, targets = batch
logits = self.model(input_ids)
loss = litgpt.utils.chunked_cross_entropy(logits, targets)
return loss

if __name__ == "__main__":
data = RandomTokenDataset(vocab_size=151936, seq_length=2048)
dataloader = torch.utils.data.DataLoader(data, batch_size=1)
strategy = ModelParallelStrategy(data_parallel_size=2, tensor_parallel_size=2)
bnb = BitsandbytesPrecision("fp4")
trainer = L.Trainer(
devices=4, max_epochs=2, accumulate_grad_batches=1, precision=None, plugins=bnb, strategy=strategy
)
with trainer.init_module(empty_init=True):
model = LitLLM()

trainer.predict(model, dataloader)
trainer.print("Inference successfully completed!")
trainer.print(f"Peak memory usage: {torch.cuda.max_memory_allocated() / 1e9:.02f} GB")

```

The first issue is inside `torch.distributed.tensor.parallel.styles.py` when applying the different styles it checks for `isinstance(module, nn.Linear)` which fails. (changing that to `isinstance(module, torch.nn.modules.linear.Linear)` moves past that.

The next issue is within bitsandbytes
```[rank1]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[rank1]: File "/home/user/miniconda3/envs/condaenv/lib/python3.11/site-packages/lightning/fabric/plugins/precision/bitsandbytes.py", line 187, in _replace_param
[rank1]: return bnb.nn.Params4bit(
[rank1]: ^^^^^^^^^^^^^^^^^^
[rank1]: File "/home/user/miniconda3/envs/condaenv/lib/python3.11/site-packages/bitsandbytes/nn/modules.py", line 230, in __new__
[rank1]: self = torch.Tensor._make_subclass(cls, data, requires_grad)
[rank1]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[rank1]: RuntimeError: Creating a new Tensor subclass Params4bit but the raw Tensor object is already associated to a python object of type DTensor
```

Any pointers on how to make this work? @awaelchli

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the provided reproduction and inspect torch.distributed.tensor.parallel.styles.py where the Linear type check fails, then lightning/fabric/plugins/precision/bitsandbytes.py where Params4bit is created. Trace how the DTensor reaches bitsandbytes and compare the failing stack trace with the TP+FSDP setup. Done means the sample LightningTrainer inference completes with TP+FSDP and fp4 quantization.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
distributed-systems, machine-learning
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.