Skip to content

Commit 5f63d7a

Browse files
authored
[GPTQ][ddp] enabling DDP for GPTQ (vllm-project#2333)
After the changes in vllm-project/compressed-tensors#572 vllm-project/compressed-tensors#534 vllm-project#2340 we're ready to start rolling out DDP implementations of various modifiers # API: The Api we've landed on attempts to maintain the normal flow with minimal changes necessary to enable DDP: 1) the user will call torchrun --nproc_per_node==<num_threads> script.py to start the script 2) the user will initialize the distributed context, (they can use the helper init_dist to do this) 3) the user will load the model using the new context manager, setting the device map as outlined [here](vllm-project/compressed-tensors#572). (For most users this will be "auto_offload") 4) (optional) the user can partition the dataset at load time using get_rank_partition or just load as normal and oneshot will partition the data later (will load 1 copy of dataset into cpu memory for each rank which may be onerous) ```python from compressed_tensors.offload import load_offloaded_model, init_dist init_dist() with load_offloaded_model(): model = AutoModelForCausalLM.from_pretrained(MODEL_ID, device_map="auto_offload") ... ds = load_dataset( DATASET_ID, split=get_rank_partition(DATASET_SPLIT, NUM_CALIBRATION_SAMPLES) ``` # Implementation Adding the DDP process to GPTQ has relatively straightforward though optimizing it for speed was a bit trickier. There are 4 steps 1) assigning each module to a rank which it will be compressed by 2) for each module assigned to a rank, having all hessian information sent by other ranks to the assigned rank 3) each rank compresses the modules that it was assigned 4) broadcast the final quantized values to all ranks Step 1 required the largest optimization, without any load balancing, we ran into situations where 1 rank could be doing twice as much work as another. Thus we implemented basic load balancing and time estimation that seems to be working well in practice. The other major optimization was using asynchronous ops for thread to thread communication. Before these optimizations, 2 thread GPTQ was as fast as 1 thread GPTQ for llama3-8B, afterward it results in a 27% speedup despite being a relatively small model. | model_id | world_size | max_time | max_memory | save_time | flex_extract | eval_time | |----------|-------------|----------|------------|-----------|--------------|-----------| | Meta-Llama-3-8B-Instruct | 1 | 745.03 | 5.82 | 19.57 | 0.7066 | 95.28 | | Meta-Llama-3-8B-Instruct | 2 | 372.20 | 5.57 | 49.10 | 0.7089 | 95.24 | | Meta-Llama-3-8B-Instruct | 4 | 264.07 | 5.82 | 52.50 | 0.7180 | 96.74 | | Qwen3-30B-A3B | 1 | 14207.53 | 6.56 | 748.23 | 0.8704 | 209.93 | | Qwen3-30B-A3B | 2 | 7018.25 | 6.36 | 696.65 | 0.8810 | 205.89 | | Qwen3-30B-A3B | 4 | 3694.46 | 6.36 | 723.05 | 0.8832 | 217.62 | # GPTQ Changes while validating numerical accuracy of the DDP technique, we noticed that accuracy improved significantly for each thread added. After some debugging we realized this was because the existing [hessian calculation](https://github.com/vllm-project/llm-compressor/pull/2333/changes#diff-18d1319f01629ca65cc54f955dc6177f6dd025f057013932b2ed29842854f3ecL61-L65) was causing an accumulation of floating point errors. By rewriting the hessian calculation to sum the intermediate hessians and only divide by num_samples at the end, we improved the GSM8K evaluation from (.67, .66) to (.71, .71). You can repro these results [here](https://github.com/vllm-project/llm-compressor/pull/2333/changes#diff-d31ce0453051853c17ba2a5225b3d1bfab548e095bab0967d6acfd1b3ce1b35d) --------- Signed-off-by: HDCharles <charlesdavidhernandez@gmail.com>
1 parent 881dd46 commit 5f63d7a

6 files changed

Lines changed: 278 additions & 40 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#############################################################################
2+
# This script is adapted from ./llama3_example.py and adds DDP functionality.
3+
# run this with `torchrun --nproc_per_node=2 llama3_ddp_example.py`
4+
# or change nproc_per_node to your desired configuration
5+
# to adapt other examples to use DDP, see the 2 altered sections below
6+
#############################################################################
7+
8+
import time
9+
10+
import torch
11+
from compressed_tensors.offload import dispatch_model, init_dist, load_offloaded_model
12+
from datasets import load_dataset
13+
from transformers import AutoModelForCausalLM, AutoTokenizer
14+
15+
from llmcompressor import oneshot
16+
from llmcompressor.datasets.utils import get_rank_partition
17+
from llmcompressor.modifiers.quantization import GPTQModifier
18+
19+
model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
20+
21+
###### DDP MODEL LOAD CHANGE #####
22+
init_dist()
23+
with load_offloaded_model():
24+
model = AutoModelForCausalLM.from_pretrained(
25+
model_id, dtype="auto", device_map="auto_offload"
26+
)
27+
##################################
28+
29+
tokenizer = AutoTokenizer.from_pretrained(model_id)
30+
31+
DATASET_ID = "HuggingFaceH4/ultrachat_200k"
32+
DATASET_SPLIT = "train_sft"
33+
NUM_CALIBRATION_SAMPLES = 512
34+
MAX_SEQUENCE_LENGTH = 2048
35+
36+
###### DDP DATA LOAD CHANGE #####
37+
ds = load_dataset(
38+
DATASET_ID, split=get_rank_partition(DATASET_SPLIT, NUM_CALIBRATION_SAMPLES)
39+
)
40+
##########################
41+
42+
ds = ds.shuffle(seed=42)
43+
44+
45+
def preprocess(example):
46+
return {
47+
"text": tokenizer.apply_chat_template(
48+
example["messages"],
49+
tokenize=False,
50+
)
51+
}
52+
53+
54+
ds = ds.map(preprocess)
55+
56+
57+
def tokenize(sample):
58+
return tokenizer(
59+
sample["text"],
60+
padding=False,
61+
max_length=MAX_SEQUENCE_LENGTH,
62+
truncation=True,
63+
add_special_tokens=False,
64+
)
65+
66+
67+
ds = ds.map(tokenize, remove_columns=ds.column_names)
68+
69+
recipe = GPTQModifier(targets="Linear", scheme="W4A16", ignore=["lm_head"])
70+
71+
72+
torch.cuda.reset_peak_memory_stats()
73+
start_time = time.time()
74+
75+
76+
# Apply algorithms.
77+
oneshot(
78+
model=model,
79+
dataset=ds,
80+
recipe=recipe,
81+
max_seq_length=MAX_SEQUENCE_LENGTH,
82+
num_calibration_samples=NUM_CALIBRATION_SAMPLES,
83+
)
84+
85+
elapsed_time = time.time() - start_time
86+
peak_memory_gb = torch.cuda.max_memory_allocated() / (1024**3)
87+
print("Quantization Complete")
88+
print(f"Time: {elapsed_time / 60:.2f} minutes ({elapsed_time:.2f} seconds)")
89+
print(f"Peak GPU Memory: {peak_memory_gb:.2f} GB")
90+
91+
92+
# Confirm generations of the quantized model look sane.
93+
print("\n\n")
94+
print("========== SAMPLE GENERATION ==============")
95+
dispatch_model(model)
96+
sample = tokenizer("Hello my name is", return_tensors="pt")
97+
sample = {key: value.to(model.device) for key, value in sample.items()}
98+
output = model.generate(**sample, max_new_tokens=100)
99+
print(tokenizer.decode(output[0]))
100+
print("==========================================\n\n")
101+
102+
print("Saving...")
103+
# Save to disk compressed.
104+
SAVE_DIR = (
105+
model_id.rstrip("/").split("/")[-1]
106+
+ "-W4A16-G128-DDP"
107+
+ str(torch.distributed.get_world_size())
108+
)
109+
model.save_pretrained(SAVE_DIR, save_compressed=True)
110+
tokenizer.save_pretrained(SAVE_DIR)
111+
112+
torch.distributed.destroy_process_group()

src/llmcompressor/modifiers/quantization/gptq/base.py

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Dict, List, Optional, Tuple, Union
33

44
import torch
5+
from compressed_tensors.offload.dist_utils import is_distributed
56
from compressed_tensors.quantization import (
67
QuantizationConfig,
78
QuantizationScheme,
@@ -17,6 +18,7 @@
1718
)
1819
from loguru import logger
1920
from pydantic import PrivateAttr
21+
from torch import distributed as dist
2022

2123
from llmcompressor.core import Event, EventType, State
2224
from llmcompressor.modifiers import Modifier
@@ -29,10 +31,13 @@
2931
from llmcompressor.modifiers.quantization.quantization import QuantizationMixin
3032
from llmcompressor.modifiers.utils import update_fused_layer_weight_global_scales
3133
from llmcompressor.sentinel import Sentinel
34+
from llmcompressor.utils import greedy_bin_packing, wait_for_comms
3235
from llmcompressor.utils.metric_logging import CompressionLogger
3336

3437
__all__ = ["GPTQModifier"]
3538

39+
_GPTQ_Q_PARAMS = ["weight", "weight_scale", "weight_zero_point", "weight_g_idx"]
40+
3641

3742
class GPTQModifier(Modifier, QuantizationMixin):
3843
"""
@@ -123,7 +128,9 @@ class GPTQModifier(Modifier, QuantizationMixin):
123128
# private variables
124129
_module_names: Dict[torch.nn.Module, str] = PrivateAttr(default_factory=dict)
125130
_hessians: Dict[torch.nn.Module, torch.Tensor] = PrivateAttr(default_factory=dict)
126-
_num_samples: Dict[torch.nn.Module, int] = PrivateAttr(default_factory=dict)
131+
_num_samples: Dict[torch.nn.Module, torch.Tensor] = PrivateAttr(
132+
default_factory=dict
133+
)
127134

128135
def resolve_quantization_config(self) -> QuantizationConfig:
129136
config = super().resolve_quantization_config()
@@ -248,7 +255,9 @@ def calibrate_module(
248255
"cpu" if self.offload_hessians else get_execution_device(module)
249256
)
250257
self._hessians[module] = make_empty_hessian(module, device=init_device)
251-
self._num_samples[module] = 0
258+
self._num_samples[module] = torch.zeros(
259+
tuple(), device=get_execution_device(module)
260+
)
252261

253262
# Accumulate hessian with input with optional offloading
254263
with self._maybe_onload_hessian(module):
@@ -263,7 +272,32 @@ def compress_modules(self):
263272
"""
264273
Quantize modules which have been calibrated
265274
"""
266-
for module in list(self._num_samples.keys()):
275+
### Not Distributed
276+
if not is_distributed():
277+
self.compress_module_list(list(self._num_samples.keys()))
278+
return
279+
280+
### Distributed
281+
rank = dist.get_rank()
282+
world_size = dist.get_world_size()
283+
284+
# Assign modules to ranks
285+
module_list, rank_to_modules, module_to_rank = greedy_bin_packing(
286+
list(self._hessians.keys()),
287+
world_size,
288+
item_weight_fn=lambda mod: self._hessians[mod].shape[0],
289+
)
290+
291+
# send hessians to assigned ranks
292+
self._reduce_hessian_to_target_rank(module_list, module_to_rank)
293+
294+
self.compress_module_list(rank_to_modules[rank])
295+
296+
# broadcast compressed modules to each rank
297+
self._broadcast_quantized_params(module_list, module_to_rank)
298+
299+
def compress_module_list(self, module_list):
300+
for module in module_list:
267301
name = self._module_names[module]
268302
num_samples = self._num_samples[module]
269303
quant_args = getattr_chain(module, "quantization_scheme.weights")
@@ -275,23 +309,59 @@ def compress_modules(self):
275309
self._maybe_onload_hessian(module),
276310
CompressionLogger(module) as comp_logger,
277311
):
278-
loss, quantized_weight, scale, zero_point, g_idx = quantize_weight(
312+
loss, q_param_dict = quantize_weight(
279313
module=module,
280314
quant_args=quant_args,
281-
hessians_dict=self._hessians,
315+
hessian=self._hessians.pop(module) / self._num_samples.pop(module),
282316
blocksize=self.block_size,
283317
percdamp=self.dampening_frac,
284318
)
285319
comp_logger.set_loss(loss)
286320

287-
update_offload_parameter(module, "weight", quantized_weight)
288-
update_offload_parameter(module, "weight_scale", scale)
289-
update_offload_parameter(module, "weight_zero_point", zero_point)
290-
if g_idx is not None:
291-
update_offload_parameter(module, "weight_g_idx", g_idx)
292-
293-
# self._hessians[module] already deleted by quantize_weight
294-
del self._num_samples[module]
321+
for attr, val in q_param_dict.items():
322+
update_offload_parameter(module, attr, val)
323+
324+
def _reduce_hessian_to_target_rank(self, module_list, module_to_rank):
325+
rank = dist.get_rank()
326+
pending_comms = []
327+
for module in module_list:
328+
target_rank = module_to_rank[module]
329+
with self._maybe_onload_hessian(module):
330+
pending_comms.append(
331+
dist.reduce(
332+
self._hessians[module],
333+
op=dist.ReduceOp.SUM,
334+
dst=target_rank,
335+
async_op=True,
336+
)
337+
)
338+
pending_comms.append(
339+
dist.reduce(
340+
self._num_samples[module],
341+
op=dist.ReduceOp.SUM,
342+
dst=target_rank,
343+
async_op=True,
344+
)
345+
)
346+
if rank != target_rank:
347+
self._hessians.pop(module, None)
348+
self._num_samples.pop(module, None)
349+
wait_for_comms(pending_comms)
350+
351+
def _broadcast_quantized_params(self, module_list, module_to_rank):
352+
pending_comms = []
353+
for module in module_list:
354+
src_rank = module_to_rank[module]
355+
356+
# Get parameters from module
357+
for attr in _GPTQ_Q_PARAMS:
358+
if getattr(module, attr, None) is not None:
359+
pending_comms.append(
360+
dist.broadcast(
361+
getattr(module, attr), src=src_rank, async_op=True
362+
)
363+
)
364+
wait_for_comms(pending_comms)
295365

296366
def on_end(self, state: State, event: Event, **kwargs):
297367
"""

src/llmcompressor/modifiers/quantization/gptq/gptq_quantize.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def accumulate_hessian(
3434
inp: torch.Tensor,
3535
module: torch.nn.Module,
3636
H: torch.Tensor | None,
37-
num_samples: int,
38-
) -> tuple[torch.Tensor, int]:
37+
num_samples: torch.Tensor,
38+
) -> tuple[torch.Tensor, torch.Tensor]:
3939
inp = inp.to(device=H.device)
4040
if len(inp.shape) == 2:
4141
inp = inp.unsqueeze(0)
@@ -58,11 +58,10 @@ def accumulate_hessian(
5858
inp = inp.permute([1, 0, 2])
5959
inp = inp.flatten(1)
6060

61-
H *= num_samples / (num_samples + num_added)
6261
num_samples += num_added
6362

6463
inp = inp.to(dtype=GPTQ_PRECISION)
65-
inp = math.sqrt(2 / num_samples) * inp
64+
inp = math.sqrt(2) * inp
6665
H += inp.matmul(inp.t())
6766

6867
return H, num_samples
@@ -71,7 +70,7 @@ def accumulate_hessian(
7170
def quantize_weight(
7271
module: torch.nn.Module,
7372
quant_args: QuantizationArgs,
74-
hessians_dict: dict[torch.nn.Module, torch.Tensor],
73+
hessian: torch.Tensor,
7574
blocksize: int = 128,
7675
percdamp: float = 0.01,
7776
) -> tuple[float, torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor]:
@@ -91,8 +90,7 @@ def quantize_weight(
9190
final_shape = module.weight.shape
9291
final_dtype = module.weight.dtype
9392
W = module.weight.clone()
94-
H = hessians_dict[module] # unfortunately python does not have a `move` keyword
95-
del hessians_dict[module] # so we have to delete the original reference manually
93+
H = hessian
9694

9795
# create observer for calculating quantization parameters
9896
observer = Observer.load_from_registry(
@@ -279,13 +277,14 @@ def quantize_weight(
279277
W = W.reshape(final_shape).to(final_dtype)
280278

281279
loss = torch.sum(losses).item()
282-
return (
283-
loss,
284-
W,
285-
scale.to(dtype=final_dtype),
286-
zero_point.to(dtype=quant_args.zp_dtype),
287-
g_idx,
288-
)
280+
q_param_dict = {
281+
"weight": W,
282+
"weight_scale": scale.to(dtype=final_dtype),
283+
"weight_zero_point": zero_point.to(dtype=quant_args.zp_dtype),
284+
}
285+
if g_idx is not None:
286+
q_param_dict["weight_g_idx"] = g_idx
287+
return (loss, q_param_dict)
289288

290289

291290
def _apply_activation_ordering(

src/llmcompressor/transformers/compression/compressed_tensors_utils.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
SparsityCompressionConfig,
1010
)
1111
from compressed_tensors.config import CompressionFormat
12+
from compressed_tensors.offload import is_rank0
1213
from loguru import logger
1314
from transformers import PreTrainedModel
1415

@@ -88,22 +89,23 @@ def save_pretrained_wrapper(
8889
if compressor is not None:
8990
compressor.compress_model(model)
9091

91-
# save (compressed) model structure
92-
original_save_pretrained.__get__(model, model_class)(
93-
save_directory,
94-
safe_serialization=safe_serialization,
95-
**kwargs,
96-
)
92+
if is_rank0():
93+
# save (compressed) model structure
94+
original_save_pretrained.__get__(model, model_class)(
95+
save_directory,
96+
safe_serialization=safe_serialization,
97+
**kwargs,
98+
)
9799

98-
# update config to reflect compression
99-
if compressor is not None:
100-
compressor.update_config(save_directory)
100+
# update config to reflect compression
101+
if compressor is not None:
102+
compressor.update_config(save_directory)
101103

102-
# update existing recipe
103-
update_and_save_recipe(model.name_or_path, save_directory)
104+
# update existing recipe
105+
update_and_save_recipe(model.name_or_path, save_directory)
104106

105-
# copy python files from cache dir to save_path if any
106-
copy_python_files_from_model_cache(model, save_directory)
107+
# copy python files from cache dir to save_path if any
108+
copy_python_files_from_model_cache(model, save_directory)
107109

108110
save_pretrained_wrapper._overridden = True
109111
return save_pretrained_wrapper

src/llmcompressor/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
from .transformers import *
88
from .dev import *
99
from .helpers import *
10+
from .dist import *

0 commit comments

Comments
 (0)