Repository navigation
Run fused-MoE expert GPTQ updates on all of this process's GPUs #3002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2a0039f
46262c8
88de90b
df3f78f
958a02d
3d90e69
e3123f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,8 @@ | |||||||||||||||||||||||||
| """GPTQ helper and Hessian utilities for calibration.""" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import functools | ||||||||||||||||||||||||||
| import queue | ||||||||||||||||||||||||||
| from concurrent.futures import ThreadPoolExecutor | ||||||||||||||||||||||||||
| from contextlib import contextmanager | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import torch | ||||||||||||||||||||||||||
|
|
@@ -106,13 +108,103 @@ def _hessian_offload_device(nbytes: int) -> torch.device: | |||||||||||||||||||||||||
| rescale of the whole matrix. Only GPUs this process already holds memory on are candidates, | ||||||||||||||||||||||||||
| and only outside distributed runs, so no other job's or rank's GPU is touched. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| if not (dist.is_available() and dist.is_initialized() and dist.get_world_size() > 1): | ||||||||||||||||||||||||||
| for gpu in sorted(get_process_gpus(), key=get_available_gpu_memory, reverse=True): | ||||||||||||||||||||||||||
| if _gpu_has_room(gpu, nbytes): | ||||||||||||||||||||||||||
| return torch.device("cuda", gpu) | ||||||||||||||||||||||||||
| for gpu in sorted(_own_gpus(), key=get_available_gpu_memory, reverse=True): | ||||||||||||||||||||||||||
| if _gpu_has_room(gpu, nbytes): | ||||||||||||||||||||||||||
| return torch.device("cuda", gpu) | ||||||||||||||||||||||||||
| return torch.device("cpu") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def _own_gpus() -> list[int]: | ||||||||||||||||||||||||||
| """GPUs this process already holds memory on; none in a multi-rank run, where they may be another rank's.""" | ||||||||||||||||||||||||||
| if dist.is_available() and dist.is_initialized() and dist.get_world_size() > 1: | ||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||
| return get_process_gpus() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def _update_room(helper, weight, block_size) -> int: | ||||||||||||||||||||||||||
| """Bytes a GPU needs to run ``helper``'s update of ``weight``, as measured with the CUDA codecs. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| The Hessian, the inverse and their temporaries; the float32 copy of the weight with a | ||||||||||||||||||||||||||
| dtype-conversion temporary (a projection much wider than its input makes the weight the larger | ||||||||||||||||||||||||||
| part); the block's errors and up to four temporaries of a group no wider than the block, each | ||||||||||||||||||||||||||
| as large as the weight when the input is no wider than a block; and the codec's tables and | ||||||||||||||||||||||||||
| scratch, about 35 MiB on a GPU's first update. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| columns = weight.shape[-1] | ||||||||||||||||||||||||||
| rows = weight.numel() // columns | ||||||||||||||||||||||||||
| block = min(block_size, columns) | ||||||||||||||||||||||||||
| return 2**26 + 4 * (8 * helper.hessian.numel() + rows * (2 * columns + 5 * block)) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def update_expert_weights(work, block_size, perc_damp): | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion (non-blocking): this repeats the scaffolding of
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in e3123f9. Both callers' 2-GPU tests pass on 2x RTX PRO 6000, with and without inference mode: |
||||||||||||||||||||||||||
| """Run the GPTQ updates of ``(helper, weight)`` pairs, spread over this process's GPUs. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Expert updates are independent, and in a layerwise run of a model sharded over several GPUs | ||||||||||||||||||||||||||
| only the current layer's GPU works. Helpers whose update can run on any device run in one | ||||||||||||||||||||||||||
| worker thread per GPU this process uses that has room for an update, each writing its result | ||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[P2] The CUDA-only restriction fixes the fallback scratch issue, but this bound still omits GPTQ's block/group buffers. For a supported
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 3d90e69. Measured on 2x RTX PRO 6000 (peak allocated on the worker GPU, CUDA encoder):
Across 17 format, shape and block-size combinations (IQ1_S, IQ1_M, IQ2_XXS, IQ2_XS, IQ2_S, Q8_0; block sizes 256 to 2048), the peak is 29-63% of the new room.
|
||||||||||||||||||||||||||
| back to its weight; the others, or all of them when fewer than two GPUs qualify, run in turn on | ||||||||||||||||||||||||||
| their weight's device. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| movable = [(h, w) for h, w in work if w.is_cuda and h.updates_on_any_device()] | ||||||||||||||||||||||||||
| # A worker holds one expert at a time. | ||||||||||||||||||||||||||
| room = max((_update_room(h, w, block_size) for h, w in movable), default=0) | ||||||||||||||||||||||||||
| gpus = [torch.device("cuda", g) for g in _own_gpus() if get_available_gpu_memory(g) > room] | ||||||||||||||||||||||||||
| if len(gpus) < 2 or len(movable) < 2: | ||||||||||||||||||||||||||
| movable = [] | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def update(item, device): | ||||||||||||||||||||||||||
| helper, weight = item | ||||||||||||||||||||||||||
| helper.update_weights(block_size, perc_damp, weight, device=device) | ||||||||||||||||||||||||||
| helper.free() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| moved = {id(helper) for helper, _ in movable} | ||||||||||||||||||||||||||
| for item in work: | ||||||||||||||||||||||||||
| if id(item[0]) not in moved: | ||||||||||||||||||||||||||
| update(item, None) | ||||||||||||||||||||||||||
| if not movable: | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| pending: queue.SimpleQueue = queue.SimpleQueue() | ||||||||||||||||||||||||||
| for item in movable: | ||||||||||||||||||||||||||
| pending.put(item) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def next_item(device): | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| return pending.get_nowait() | ||||||||||||||||||||||||||
| except queue.Empty: | ||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| run_on_each_gpu(gpus, next_item, update) | ||||||||||||||||||||||||||
| for device in {weight.device for _, weight in movable}: | ||||||||||||||||||||||||||
| torch.cuda.synchronize(device) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def run_on_each_gpu(gpus, next_item, run): | ||||||||||||||||||||||||||
| """Call ``run(item, device)`` on the items ``next_item(device)`` hands out, one thread per GPU. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ``next_item`` returns the next item for that GPU, or ``None`` when there is none left; each GPU's | ||||||||||||||||||||||||||
| items can be its own or come from a queue all of them share. The first item runs in the caller's | ||||||||||||||||||||||||||
| thread with ``device=None``, on the device its data is on, so lazy setup that is not thread-safe, | ||||||||||||||||||||||||||
| such as loading a format's CUDA extension, happens once before the workers start. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| first = next_item(gpus[0]) | ||||||||||||||||||||||||||
| if first is not None: | ||||||||||||||||||||||||||
| run(first, None) | ||||||||||||||||||||||||||
| inference = torch.is_inference_mode_enabled() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def worker(device): | ||||||||||||||||||||||||||
| # The device, grad mode and inference mode are per thread: take the caller's. | ||||||||||||||||||||||||||
| torch.cuda.set_device(device) | ||||||||||||||||||||||||||
| with torch.inference_mode() if inference else torch.no_grad(): | ||||||||||||||||||||||||||
| while (item := next_item(device)) is not None: | ||||||||||||||||||||||||||
| run(item, device) | ||||||||||||||||||||||||||
| torch.cuda.synchronize(device) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| with ThreadPoolExecutor(len(gpus)) as pool: | ||||||||||||||||||||||||||
| for future in [pool.submit(worker, device) for device in gpus]: | ||||||||||||||||||||||||||
| future.result() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def update_hessian(input, hessian, n_samples, input_dtype=torch.float32): | ||||||||||||||||||||||||||
| """Update hessian matrix with new input samples using incremental formula. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -359,20 +451,31 @@ def free(self): | |||||||||||||||||||||||||
| self.weight = None | ||||||||||||||||||||||||||
| self.h_inv = None | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def update_weights(self, block_size, perc_damp, weight=None): | ||||||||||||||||||||||||||
| def updates_on_any_device(self) -> bool: | ||||||||||||||||||||||||||
| """Whether :meth:`update_weights` may run on a device other than its weight's. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| True only for helpers whose update reads no quantizer state, which lives on the weight's | ||||||||||||||||||||||||||
| device, and fits the room :func:`update_expert_weights` checks for. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def update_weights(self, block_size, perc_damp, weight=None, device=None): | ||||||||||||||||||||||||||
| """Run GPTQ blockwise weight update on this helper's weight. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Populates ``self.weight`` and ``self.h_inv``, runs the blockwise update, logs MSE, and | ||||||||||||||||||||||||||
| overwrites ``weight`` (default ``module.weight``) in place, so an expert's slice of a | ||||||||||||||||||||||||||
| fused 3-D parameter is written back into that parameter. | ||||||||||||||||||||||||||
| fused 3-D parameter is written back into that parameter. The update runs on ``device``, | ||||||||||||||||||||||||||
| by default the weight's; only a helper whose :meth:`updates_on_any_device` is true may | ||||||||||||||||||||||||||
| pass another. | ||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||
| weight = self.module.weight if weight is None else weight | ||||||||||||||||||||||||||
| hessian = self.hessian.to(weight.device) | ||||||||||||||||||||||||||
| self.weight = weight.data.float().clone() | ||||||||||||||||||||||||||
| device = weight.device if device is None else device | ||||||||||||||||||||||||||
| hessian = self.hessian.to(device) | ||||||||||||||||||||||||||
| self.weight = weight.data.to(device, torch.float32, copy=True) | ||||||||||||||||||||||||||
| self._prepare_hessian_inverse(hessian, perc_damp) | ||||||||||||||||||||||||||
| self._blockwise_update(block_size) | ||||||||||||||||||||||||||
| if self.log_mse: | ||||||||||||||||||||||||||
| self._print_mse_error(hessian, weight) | ||||||||||||||||||||||||||
| self._print_mse_error(hessian, weight.data.to(device)) | ||||||||||||||||||||||||||
| weight.data.copy_(self.weight.reshape(weight.shape)) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # ------------------------------------------------------------------ | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Drop the work-list tensor references before leaving this materialization window.
workcontains slices of both full fused projections and survives until the next list assignment. Accelerate’sweight_access_and_writeback_context()writes back and replaces the parameters with meta tensors, but these slices keep the old GPU storage alive. The next module’s window opens beforeworkis replaced, so non-layerwise GPTQ on consecutive offloaded expert modules holds the previous module’s gate/up and down weights while onloading the next. The old loop retained only its final projection slice; the list adds the entire gate/up storage to that overlap. Keep the updated count separately and clear/deleteworkinside the window after the updates. This also follows AGENTS.md’s rule not to retain onloaded weights past their layer; add an offload memory-lifetime regression.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 88de90b.
gptq()counts the updates, then deletesworkinside the window. That also releases the lastdown_projslice the old loop variable kept alive.The new CPU test
test_gptq_drops_each_fused_experts_weights_before_the_next_windowkeeps weakrefs to every updated expert slice and checks they are dead when the next module's window opens. It fails without thedel.