Skip to content

Commit ad66750

Browse files
committed
AutoBitQuantizer vLLM-compatible quantization_config
1 parent 3eb097b commit ad66750

10 files changed

Lines changed: 345 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ SpinQuant/OstQuant-based rotation preprocessing that reduces quantization error
4242
- Tikhonov regularization for over-fitting (X^T X + nλI)
4343
- Three initialization strategies: Clip-Optimize, Clip-Optimize with Error Propagation, and GPTQ
4444

45+
### AutoBitQuantizer vLLM-compatible quantization_config
46+
47+
- `AutoBitQuantizer` now emits `mixed_gptq`-compatible `quantization_config` (`onecomp/quantizer/autobit/_autobit.py`)
48+
- ILP solver now enforces fused-layer equality constraints (`onecomp/quantizer/autobit/ilp.py`)
49+
- vLLM fuses q/k/v → `qkv_proj` and gate/up → `gate_up_proj`
50+
4551
### API changes
4652

4753
- Made `Runner.create_quantized_model()` a public method (renamed from `_create_quantized_model`)
@@ -75,6 +81,8 @@ SpinQuant/OstQuant-based rotation preprocessing that reduces quantization error
7581
- Added `example/example_jointq.py`: JointQ 4-bit (groupsize=128) quantization example with dequantized model PPL evaluation
7682
- Added `example/pre_process/example_llama_preprocess_rtn.py`: Rotation preprocessing + RTN quantization (TinyLlama-1.1B)
7783
- Added `example/pre_process/example_preprocess_save_load.py`: Rotation preprocessing + GPTQ quantization → save → load → PPL verification
84+
- Added `example/vllm_inference/example_gptq_vllm_inference.py`: GPTQ + QEP quantization and vLLM inference end-to-end example
85+
- Added `example/vllm_inference/example_autobit_vllm_inference.py`: AutoBit mixed-precision quantization and vLLM inference example
7886

7987
### Documentation
8088

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ Then open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
157157
| | [example_preprocess_save_load.py](./example/pre_process/example_preprocess_save_load.py) | Save and load rotation-preprocessed quantized models |
158158
| Post-Process | [example_lora_sft.py](./example/post_process/example_lora_sft.py) | LoRA SFT post-quantization fine-tuning |
159159
| | [example_lora_sft_knowledge.py](./example/post_process/example_lora_sft_knowledge.py) | LoRA SFT knowledge injection |
160-
| vLLM | [example_vllm_inference.py](./example/example_vllm_inference.py) | Serve quantized models with vLLM |
160+
| vLLM | [example_gptq_vllm_inference.py](./example/vllm_inference/example_gptq_vllm_inference.py) | GPTQ + QEP quantization and vLLM inference |
161+
| | [example_autobit_vllm_inference.py](./example/vllm_inference/example_autobit_vllm_inference.py) | AutoBit quantization and vLLM inference |
161162

162163
## 🔌 vLLM Inference
163164

docs/user-guide/vllm-inference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ print(outputs[0].outputs[0].text)
120120
When combining quantization and vLLM inference in a single script, you **must** wrap your code in `if __name__ == "__main__":`. vLLM spawns worker processes that re-import the script, so without this guard the quantization step will run again in each child process.
121121

122122
A complete working example (quantization + vLLM inference) is available at
123-
[`example/example_vllm_inference.py`](https://github.com/FujitsuResearch/OneCompression/blob/main/example/example_vllm_inference.py).
123+
[`example/vllm_inference/example_gptq_vllm_inference.py`](https://github.com/FujitsuResearch/OneCompression/blob/main/example/vllm_inference/example_gptq_vllm_inference.py).
124124

125125
### Environment Variables
126126

example/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
buf*.py
22
run_*.sh
33
tinyllama_gptq4_*/
4+
TinyLlama*/

example/example_vllm_inference.py renamed to example/vllm_inference/example_autobit_vllm_inference.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
"""
22
3-
Example: Quantize a model with OneComp and run inference with vLLM
3+
Example: Quantize a model with auto_run and run inference with vLLM
44
55
Performs the following steps:
6-
1. Quantize with GPTQ (4-bit, groupsize=128) + QEP using auto_run
6+
1. Quantize with AutoBit (mixed-precision) + QEP using auto_run
77
2. Load the quantized model with vLLM's offline LLM interface
88
3. Generate text
99
@@ -25,12 +25,10 @@
2525

2626
def main():
2727
# Step 1: Quantize and save the model
28-
save_dir = "./TinyLlama-1.1B-gptq-4bit"
28+
save_dir = "./TinyLlama-1.1B-autobit"
2929

3030
runner = Runner.auto_run(
3131
model_id="TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T",
32-
wbits=4,
33-
groupsize=128,
3432
save_dir=save_dir,
3533
evaluate=False,
3634
)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
3+
Example: Quantize a model with OneComp and run inference with vLLM
4+
5+
Performs the following steps:
6+
1. Quantize with GPTQ (4-bit, groupsize=128) + QEP
7+
2. Save the quantized model
8+
3. Load the quantized model with vLLM's offline LLM interface
9+
4. Generate text
10+
11+
Requirements:
12+
pip install vllm
13+
14+
Copyright 2025-2026 Fujitsu Ltd.
15+
16+
Author: Keiji Kimura
17+
18+
"""
19+
20+
import gc
21+
22+
import torch
23+
from onecomp import Runner, ModelConfig, GPTQ, setup_logger
24+
from vllm import LLM, SamplingParams
25+
26+
27+
def main():
28+
setup_logger()
29+
30+
# Step 1: Quantize with GPTQ + QEP and save the model
31+
save_dir = "./TinyLlama-1.1B-gptq-4bit"
32+
33+
model_config = ModelConfig(
34+
model_id="TinyLlama/TinyLlama-1.1B-intermediate-step-1431k-3T",
35+
)
36+
quantizer = GPTQ(wbits=4, groupsize=128)
37+
runner = Runner(model_config=model_config, quantizer=quantizer, qep=True)
38+
runner.run()
39+
runner.save_quantized_model(save_dir)
40+
41+
# Free GPU memory used by quantization before loading vLLM
42+
del runner
43+
gc.collect()
44+
torch.cuda.empty_cache()
45+
46+
# Step 3: Load the quantized model with vLLM and generate text
47+
llm = LLM(
48+
model=save_dir,
49+
max_model_len=512,
50+
dtype="float16",
51+
enforce_eager=True,
52+
)
53+
54+
prompts = [
55+
"Explain what post-training quantization is in one sentence:",
56+
"The capital of France is",
57+
]
58+
59+
outputs = llm.generate(prompts, SamplingParams(max_tokens=64, temperature=0.0))
60+
61+
for output in outputs:
62+
print(f"Prompt: {output.prompt}")
63+
print(f"Response: {output.outputs[0].text}")
64+
print()
65+
66+
67+
if __name__ == "__main__":
68+
main()

onecomp/quantizer/autobit/_autobit.py

Lines changed: 159 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
77
"""
88

9+
import re
910
from dataclasses import dataclass, field
1011
from enum import StrEnum
11-
from typing import Union
12+
from typing import Any, Optional, Union
1213

1314
import torch
1415

@@ -121,6 +122,16 @@ class AutoBitQuantizer(Quantizer):
121122
dbf_threshold: float = 2.0
122123
dbf_iters: int = None # None → DBF default (600); set low (e.g. 10) for fast testing
123124

125+
# --- vLLM fused-layer constraints ---
126+
# Groups of module suffixes that vLLM fuses into a single linear
127+
fused_groups: list = field(
128+
default_factory=lambda: [
129+
["self_attn.q_proj", "self_attn.k_proj", "self_attn.v_proj"],
130+
["mlp.gate_proj", "mlp.up_proj"],
131+
]
132+
)
133+
enable_fused_groups: bool = False
134+
124135
# internal variables
125136
_module_to_quantizer: dict = field(default_factory=dict, repr=False, init=False)
126137
_name_to_quantizer: dict = field(default_factory=dict, repr=False, init=False)
@@ -130,14 +141,9 @@ def __post_init__(self):
130141
if not isinstance(self.assignment_strategy, AssignmentStrategy):
131142
self.assignment_strategy = AssignmentStrategy(self.assignment_strategy)
132143
if not self.quantizers:
133-
self.quantizers = self._default_quantizers()
144+
raise ValueError("quantizers must be provided")
134145
self._sync_flags()
135146

136-
@staticmethod
137-
def _default_quantizers():
138-
"""GPTQ candidates for 2–8 bit (auto-generated when ``quantizers=[]``)."""
139-
return [GPTQ(wbits=b) for b in (2, 3, 4, 5, 6, 7, 8)]
140-
141147
def validate_params(self):
142148
"""Validate AutoBitQuantizer parameters."""
143149
bad = []
@@ -187,6 +193,23 @@ def validate_params(self):
187193
except (ValueError, TypeError) as e:
188194
bad.append(f"quantizers[{i}] ({type(q).__name__}): {e}")
189195

196+
_VLLM_SUPPORTED_BITS = {2, 3, 4, 8}
197+
if self.enable_fused_groups:
198+
for i, q in enumerate(self.quantizers):
199+
if not isinstance(q, GPTQ):
200+
bad.append(
201+
f"quantizers[{i}] ({type(q).__name__}): "
202+
f"enable_fused_groups=True requires all quantizers to be GPTQ"
203+
)
204+
elif q.wbits not in _VLLM_SUPPORTED_BITS:
205+
bad.append(
206+
f"quantizers[{i}] (GPTQ wbits={q.wbits}): "
207+
f"vLLM only supports GPTQ bit-widths {sorted(_VLLM_SUPPORTED_BITS)}"
208+
)
209+
210+
if self.assignment_strategy == AssignmentStrategy.MANUAL:
211+
bad.extend(self._validate_manual_fused_consistency())
212+
190213
if bad:
191214
raise ValueError("; ".join(bad))
192215

@@ -331,6 +354,47 @@ def _sync_flags(self):
331354
self.flag_hessian = any(q.flag_hessian for q in self.quantizers)
332355
self.flag_xtx = any(q.flag_xtx for q in self.quantizers)
333356

357+
def _validate_manual_fused_consistency(self):
358+
"""Check that manual keyword rules don't split fused groups."""
359+
bad = []
360+
for group in self.fused_groups:
361+
bits_for_suffix = {}
362+
for suffix in group:
363+
for child_q in self.quantizers:
364+
if self._suffix_matches_quantizer(suffix, child_q):
365+
bits_for_suffix[suffix] = getattr(
366+
child_q, "wbits", getattr(child_q, "bits", None)
367+
)
368+
break
369+
unique_bits = set(bits_for_suffix.values())
370+
if len(unique_bits) > 1:
371+
detail = ", ".join(f"{s}={b}bit" for s, b in bits_for_suffix.items())
372+
bad.append(
373+
f"Fused group {group}: mixed bit-widths ({detail}). "
374+
f"vLLM requires identical bit-widths within each fused group"
375+
)
376+
return bad
377+
378+
@staticmethod
379+
def _suffix_matches_quantizer(suffix, quantizer):
380+
"""Return True if quantizer's keyword/name rules would match suffix."""
381+
include_names = getattr(quantizer, "include_layer_names", None)
382+
include_kw = getattr(quantizer, "include_layer_keywords", None)
383+
exclude_kw = getattr(quantizer, "exclude_layer_keywords", None)
384+
385+
if include_names is not None:
386+
if not any(suffix in name for name in include_names):
387+
return False
388+
elif include_kw is not None:
389+
if not any(kw in suffix for kw in include_kw):
390+
return False
391+
392+
if exclude_kw is not None:
393+
if any(kw in suffix for kw in exclude_kw):
394+
return False
395+
396+
return True
397+
334398
def _assign_layer(self, name, module, child_q):
335399
self.module_to_name[module] = name
336400
self._module_to_quantizer[module] = child_q
@@ -422,7 +486,15 @@ def execute_post_processing(self):
422486
self.module_to_name = {}
423487
self._module_to_quantizer = {}
424488

489+
def _all_children_gptq(self) -> bool:
490+
if not self._name_to_quantizer:
491+
return False
492+
return all(isinstance(q, GPTQ) for q in self._name_to_quantizer.values())
493+
425494
def get_quant_config(self) -> dict:
495+
if self._all_children_gptq():
496+
return self._get_mixed_gptq_config()
497+
426498
child_configs = []
427499
for child_q in self.quantizers:
428500
try:
@@ -439,6 +511,86 @@ def get_quant_config(self) -> dict:
439511
"quantizers": child_configs,
440512
}
441513

514+
def _get_mixed_gptq_config(self) -> dict:
515+
unique_quantizers = list({id(q): q for q in self._name_to_quantizer.values()}.values())
516+
dominant_q: GPTQ = max(
517+
unique_quantizers,
518+
key=lambda q: sum(1 for v in self._name_to_quantizer.values() if v is q),
519+
)
520+
result: dict = {
521+
"quant_method": "mixed_gptq",
522+
"bits": dominant_q.wbits,
523+
"groupsize": dominant_q.groupsize,
524+
"actorder": dominant_q.actorder,
525+
"group_size": dominant_q.groupsize,
526+
"desc_act": dominant_q.actorder,
527+
"sym": dominant_q.sym,
528+
"checkpoint_format": "gptq",
529+
}
530+
if dominant_q.mlp_wbits is not None:
531+
result["mlp_wbits"] = dominant_q.mlp_wbits
532+
if dominant_q.mlp_groupsize is not None:
533+
result["mlp_groupsize"] = dominant_q.mlp_groupsize
534+
if dominant_q.module_wbits:
535+
result["module_wbits"] = dict(dominant_q.module_wbits)
536+
return result
537+
538+
def _build_quantization_bits(
539+
self,
540+
num_layers: int,
541+
) -> list[dict[str, Any]]:
542+
_LAYER_RE = re.compile(r"\.layers\.(\d+)\.(.*)")
543+
544+
skipped: list[str] = []
545+
layer_modules: dict[int, dict[str, Any]] = {}
546+
for name, child_q in self._name_to_quantizer.items():
547+
m = _LAYER_RE.search(name)
548+
if m is None:
549+
skipped.append(name)
550+
continue
551+
layer_idx = int(m.group(1))
552+
suffix = m.group(2)
553+
layer_modules.setdefault(layer_idx, {})[suffix] = {
554+
"bits": child_q.wbits,
555+
"method": "gptq",
556+
"params": {"group_size": child_q.groupsize},
557+
}
558+
559+
if skipped:
560+
self.logger.debug(
561+
"Skipped %d module(s) not matching layer pattern: %s",
562+
len(skipped),
563+
skipped,
564+
)
565+
566+
if not layer_modules:
567+
return []
568+
569+
return [layer_modules.get(i, {}) for i in range(num_layers)]
570+
571+
def finalize_quant_config_for_save(
572+
self,
573+
quant_config: dict[str, Any],
574+
quantized_layer_names: list[str],
575+
num_hidden_layers: Optional[int] = None,
576+
) -> dict[str, Any]:
577+
if quant_config.get("quant_method") != "mixed_gptq":
578+
return quant_config
579+
580+
if num_hidden_layers is None:
581+
raise ValueError(
582+
"num_hidden_layers is required for mixed_gptq quantization_bits "
583+
"(Runner passes model.config.num_hidden_layers)"
584+
)
585+
586+
quant_config["quantization_bits"] = self._build_quantization_bits(
587+
num_layers=num_hidden_layers,
588+
)
589+
590+
# TODO : DBF fallback is not supported yet
591+
592+
return quant_config
593+
442594
def create_inference_layer(self, result, linear_module, **kwargs):
443595
for name, stored_result in self.results.items():
444596
if stored_result is result:

0 commit comments

Comments
 (0)