Repository navigation
Improve layerwise GPTQ memory and speed: Hessian headroom, batch probe and offload fixes #2945
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
34f6148
21c8b94
b26e55e
b4a890f
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 |
|---|---|---|
|
|
@@ -38,36 +38,79 @@ | |
|
|
||
| """GPTQ helper and Hessian utilities for calibration.""" | ||
|
|
||
| from contextlib import contextmanager | ||
|
|
||
| import torch | ||
| import torch.distributed as dist | ||
|
|
||
| from modelopt.torch.utils import print_rank_0 | ||
| from modelopt.torch.utils.network import bind_forward_method, unpatch_forward_method | ||
| from modelopt.torch.utils.perf import get_used_gpu_mem_fraction | ||
|
|
||
| # Used fraction of a GPU's memory above which GPTQ keeps new Hessians off it. | ||
| _HESSIAN_GPU_MEM_LIMIT = 0.65 | ||
| # A GPU takes a new Hessian only if enough memory stays available after it for the calibration | ||
| # forward that follows. Until GPTQ has measured that forward on the GPU, keep this share free. | ||
| _HESSIAN_UNMEASURED_RESERVE = 0.35 | ||
| # Once measured, keep this multiple of the most a calibration forward added on the GPU, and at | ||
| # least this share of the GPU. | ||
| _HESSIAN_RESERVE_MARGIN = 1.5 | ||
| _HESSIAN_MIN_RESERVE = 0.1 | ||
| # The most memory a GPTQ calibration forward has added on each GPU, by device index. | ||
| _calib_forward_peak: dict[int, int] = {} | ||
|
|
||
|
|
||
| def _process_gpus() -> list[int]: | ||
| """GPUs this process already holds memory on.""" | ||
| return [i for i in range(torch.cuda.device_count()) if torch.cuda.memory_reserved(i)] | ||
|
|
||
|
|
||
| def _available_bytes(gpu: int) -> int: | ||
| """Memory an allocation on ``gpu`` can get: the driver's free memory and our cached blocks.""" | ||
| free, _ = torch.cuda.mem_get_info(gpu) | ||
| return free + torch.cuda.memory_reserved(gpu) - torch.cuda.memory_allocated(gpu) | ||
|
|
||
|
|
||
| def _gpu_has_room(gpu: int, nbytes: int) -> bool: | ||
| """Whether ``nbytes`` more on ``gpu`` leaves the calibration forward the memory it needs.""" | ||
| total = torch.cuda.get_device_properties(gpu).total_memory | ||
| peak = _calib_forward_peak.get(gpu) | ||
| if peak is None: | ||
| reserve = _HESSIAN_UNMEASURED_RESERVE * total | ||
| else: | ||
| reserve = max(_HESSIAN_MIN_RESERVE * total, _HESSIAN_RESERVE_MARGIN * peak) | ||
| return _available_bytes(gpu) - nbytes >= reserve | ||
|
|
||
|
|
||
| def reset_calib_forward_memory(): | ||
| """Forget measured calibration forwards; a new run, maybe of another model, measures afresh.""" | ||
| _calib_forward_peak.clear() | ||
|
|
||
| def _gpu_is_full(device) -> bool: | ||
| return get_used_gpu_mem_fraction(device) > _HESSIAN_GPU_MEM_LIMIT | ||
|
|
||
| @contextmanager | ||
| def track_calib_forward_memory(): | ||
| """Record the memory the enclosed calibration forward adds on each GPU, for Hessian placement. | ||
|
|
||
| The peak is read without resetting PyTorch's peak counter, which callers may be measuring | ||
| with, so an earlier, higher peak can only overstate the need: the placement errs toward the | ||
| CPU, never toward running the next forward out of memory. | ||
| """ | ||
| gpus = _process_gpus() if torch.cuda.is_available() else [] | ||
| allocated = {gpu: torch.cuda.memory_allocated(gpu) for gpu in gpus} | ||
| yield | ||
| for gpu in gpus: | ||
| added = torch.cuda.max_memory_allocated(gpu) - allocated[gpu] | ||
| _calib_forward_peak[gpu] = max(_calib_forward_peak.get(gpu, 0), added) | ||
|
Comment on lines
+98
to
+100
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. 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis
Reset CUDA peak counters before the GPTQ measurement.
Reset each process-held GPU’s peak stats immediately before entering this GPTQ tracking context. Keep the tracker’s documented no-reset behavior for callers that share CUDA peak counters. 🤖 Prompt for AI Agents
|
||
|
|
||
|
|
||
| def _hessian_offload_device(nbytes: int) -> torch.device: | ||
| """Where a Hessian goes when its weight's GPU is full: the emptiest GPU with room, else CPU. | ||
| """Where a Hessian goes when its weight's GPU lacks room: the emptiest GPU with room, else CPU. | ||
|
|
||
| A model sharded over several GPUs leaves the ones not holding the current layer idle, and a | ||
| Hessian there costs a device-to-device copy per update instead of a host copy and a CPU | ||
| 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. A candidate | ||
| must also have the Hessian's ``nbytes`` free. | ||
| 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): | ||
| gpus = [i for i in range(torch.cuda.device_count()) if torch.cuda.memory_reserved(i)] | ||
| for gpu in sorted(gpus, key=get_used_gpu_mem_fraction): | ||
| if _gpu_is_full(gpu): | ||
| break | ||
| if torch.cuda.mem_get_info(gpu)[0] > nbytes: | ||
| for gpu in sorted(_process_gpus(), key=_available_bytes, reverse=True): | ||
| if _gpu_has_room(gpu, nbytes): | ||
| return torch.device("cuda", gpu) | ||
| return torch.device("cpu") | ||
|
|
||
|
|
@@ -186,8 +229,10 @@ def __init__( | |
| device = weight.device | ||
| if device.type == "meta": | ||
| device = torch.device("cpu") | ||
| elif offload_to_cpu and device.type == "cuda" and _gpu_is_full(device): | ||
| device = _hessian_offload_device(4 * in_features**2) | ||
| elif offload_to_cpu and device.type == "cuda": | ||
| index = torch.cuda.current_device() if device.index is None else device.index | ||
| if not _gpu_has_room(index, 4 * in_features**2): | ||
| device = _hessian_offload_device(4 * in_features**2) | ||
| self.hessian = torch.zeros(in_features, in_features, dtype=torch.float32, device=device) | ||
| self.n_samples = 0 | ||
| # Set by update_weights(); listed here for documentation. | ||
|
|
||
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.
🔎 Supported by static analysis
Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 16007
Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 18255
Script executed:
Repository: NVIDIA/Model-Optimizer
Length of output: 16885
Copy onloaded quantizer state back into its master copy.
TensorQuantizer.amaxupdates an existing_amaxthrough.data.copy_(), which does not increment_version. The restoration branch can therefore restore the stale CPU master and discard the update._writeback_params_to_weights_mapupdates the weights map before restoration, but it does not update this master tensor.Proposed fix
Committable suggestion