-
Notifications
You must be signed in to change notification settings - Fork 3k
Use adaptive per-method batch sizing, drop startup benchmark #390
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
base: master
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -182,16 +182,9 @@ class Settings(BaseSettings): | |
| ) | ||
|
|
||
| batch_size: int = Field( | ||
| default=0, # auto | ||
| description="Number of input sequences to process in parallel (0 = auto).", | ||
| ) | ||
|
|
||
| max_batch_size: int = Field( | ||
| default=128, | ||
| description="Maximum batch size to try when automatically determining the optimal batch size.", | ||
| # When storing a settings object, the batch size is already fixed, | ||
| # either determined by the automatic mechanism or by explicit user choice. | ||
| exclude=True, | ||
| description="Number of input sequences to process in parallel. " | ||
| "If an out-of-memory error occurs, the batch size is halved automatically.", | ||
| ) | ||
|
Comment on lines
184
to
188
Owner
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. No problem. We're in the 2.0 development cycle, where breaking changes are expected.
Comment on lines
184
to
188
Comment on lines
184
to
188
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. Since the default value of References
Comment on lines
184
to
188
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. The default value of References
|
||
|
|
||
| max_response_length: int = Field( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||
| # Copyright (C) 2025-2026 Philipp Emanuel Weidmann <pew@worldwidemann.com> + contributors | ||||||||||||||||||
|
|
||||||||||||||||||
| import math | ||||||||||||||||||
| from collections.abc import Callable | ||||||||||||||||||
| from contextlib import suppress | ||||||||||||||||||
| from dataclasses import dataclass | ||||||||||||||||||
| from typing import Any, Type, cast | ||||||||||||||||||
|
|
@@ -33,7 +34,7 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| from .config import QuantizationMethod, RowNormalization, Settings | ||||||||||||||||||
| from .system import empty_cache | ||||||||||||||||||
| from .utils import Prompt, batchify, format_exception, print | ||||||||||||||||||
| from .utils import Prompt, format_exception, print | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def get_model_class( | ||||||||||||||||||
|
|
@@ -97,6 +98,10 @@ def __init__(self, settings: Settings): | |||||||||||||||||
| self.tokenizer.padding_side = "left" | ||||||||||||||||||
|
|
||||||||||||||||||
| self.model = None # ty:ignore[invalid-assignment] | ||||||||||||||||||
| self._batch_sizes: dict[str, int] = { | ||||||||||||||||||
| key: settings.batch_size if settings.batch_size > 0 else 128 | ||||||||||||||||||
| for key in ("responses", "residuals", "logprobs") | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+101
to
+104
Comment on lines
+101
to
+104
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. To ensure backward compatibility with existing user configurations that have
Suggested change
Comment on lines
+101
to
+104
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. In previous versions of Heretic, self._batch_sizes: dict[str, int] = {\n key: settings.batch_size if settings.batch_size > 0 else 128\n for key in ('responses', 'residuals', 'logprobs')\n } |
||||||||||||||||||
| self.max_memory = ( | ||||||||||||||||||
| {int(k) if k.isdigit() else k: v for k, v in settings.max_memory.items()} | ||||||||||||||||||
| if settings.max_memory | ||||||||||||||||||
|
|
@@ -672,20 +677,43 @@ def get_responses( | |||||||||||||||||
| skip_special_tokens=skip_special_tokens, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| def _batched( | ||||||||||||||||||
| self, prompts: list[Prompt], key: str, fn: Callable[[list[Prompt]], Any] | ||||||||||||||||||
| ) -> list[Any]: | ||||||||||||||||||
| """Run fn on prompt batches, halving the batch size on OOM.""" | ||||||||||||||||||
| results = [] | ||||||||||||||||||
| i = 0 | ||||||||||||||||||
| while i < len(prompts): | ||||||||||||||||||
| n = self._batch_sizes[key] | ||||||||||||||||||
| try: | ||||||||||||||||||
| results.append(fn(prompts[i : i + n])) | ||||||||||||||||||
| i += n | ||||||||||||||||||
| except (torch.cuda.OutOfMemoryError, RuntimeError) as error: | ||||||||||||||||||
| # On non-CUDA platforms (MPS, XPU), OOM raises RuntimeError | ||||||||||||||||||
| # with an "out of memory" message rather than the typed exception. | ||||||||||||||||||
| if ( | ||||||||||||||||||
| isinstance(error, RuntimeError) | ||||||||||||||||||
| and "out of memory" not in str(error).lower() | ||||||||||||||||||
| ): | ||||||||||||||||||
| raise | ||||||||||||||||||
| if n == 1: | ||||||||||||||||||
| raise | ||||||||||||||||||
| self._batch_sizes[key] = n // 2 | ||||||||||||||||||
| empty_cache() | ||||||||||||||||||
|
Comment on lines
+688
to
+702
|
||||||||||||||||||
| return results | ||||||||||||||||||
|
Comment on lines
+680
to
+703
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. There are two key improvements we can make to the
def _batched(
self, prompts: list[Prompt], key: str, fn: Callable[[list[Prompt]], Any]
) -> list[Any]:
"""Run fn on prompt batches, halving the batch size on OOM."""
results = []
i = 0
while i < len(prompts):
n = self._batch_sizes[key]
oom = False
try:
results.append(fn(prompts[i : i + n]))
i += n
except (torch.cuda.OutOfMemoryError, RuntimeError) as error:
if isinstance(error, RuntimeError) and "out of memory" not in str(error):
raise
if n == 1:
raise
self._batch_sizes[key] = n // 2
oom = True
if oom:
empty_cache()
return results |
||||||||||||||||||
|
|
||||||||||||||||||
| def get_responses_batched( | ||||||||||||||||||
| self, | ||||||||||||||||||
| prompts: list[Prompt], | ||||||||||||||||||
| skip_special_tokens: bool = False, | ||||||||||||||||||
| ) -> list[str]: | ||||||||||||||||||
| responses = [] | ||||||||||||||||||
|
|
||||||||||||||||||
| for batch in batchify(prompts, self.settings.batch_size): | ||||||||||||||||||
| for response in self.get_responses( | ||||||||||||||||||
| batch, | ||||||||||||||||||
| skip_special_tokens=skip_special_tokens, | ||||||||||||||||||
| ): | ||||||||||||||||||
| responses.append(response) | ||||||||||||||||||
|
|
||||||||||||||||||
| for batch in self._batched( | ||||||||||||||||||
| prompts, | ||||||||||||||||||
| "responses", | ||||||||||||||||||
| lambda b: self.get_responses(b, skip_special_tokens=skip_special_tokens), | ||||||||||||||||||
| ): | ||||||||||||||||||
| responses.extend(batch) | ||||||||||||||||||
| return responses | ||||||||||||||||||
|
|
||||||||||||||||||
| def get_residuals(self, prompts: list[Prompt]) -> Tensor: | ||||||||||||||||||
|
|
@@ -741,12 +769,10 @@ def get_residuals(self, prompts: list[Prompt]) -> Tensor: | |||||||||||||||||
| return residuals | ||||||||||||||||||
|
|
||||||||||||||||||
| def get_residuals_batched(self, prompts: list[Prompt]) -> Tensor: | ||||||||||||||||||
| residuals = [] | ||||||||||||||||||
|
|
||||||||||||||||||
| for batch in batchify(prompts, self.settings.batch_size): | ||||||||||||||||||
| residuals.append(self.get_residuals(batch)) | ||||||||||||||||||
|
|
||||||||||||||||||
| return torch.cat(residuals, dim=0) | ||||||||||||||||||
| return torch.cat( | ||||||||||||||||||
| self._batched(prompts, "residuals", self.get_residuals), | ||||||||||||||||||
| dim=0, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| def get_residuals_mean(self, prompts: list[Prompt]) -> Tensor: | ||||||||||||||||||
| if not prompts: | ||||||||||||||||||
|
|
@@ -755,18 +781,14 @@ def get_residuals_mean(self, prompts: list[Prompt]) -> Tensor: | |||||||||||||||||
| running_sum = None | ||||||||||||||||||
| total_count = 0 | ||||||||||||||||||
|
|
||||||||||||||||||
| for batch in batchify(prompts, self.settings.batch_size): | ||||||||||||||||||
| batch_residuals = self.get_residuals(batch) | ||||||||||||||||||
|
|
||||||||||||||||||
| for batch in self._batched(prompts, "residuals", self.get_residuals): | ||||||||||||||||||
| # Accumulate in high precision on CPU to reduce peak VRAM usage. | ||||||||||||||||||
| batch_sum = batch_residuals.sum(dim=0, dtype=torch.float64).cpu() | ||||||||||||||||||
|
|
||||||||||||||||||
| batch_sum = batch.sum(dim=0, dtype=torch.float64).cpu() | ||||||||||||||||||
| if running_sum is None: | ||||||||||||||||||
| running_sum = batch_sum | ||||||||||||||||||
| else: | ||||||||||||||||||
| running_sum += batch_sum | ||||||||||||||||||
|
|
||||||||||||||||||
| total_count += batch_residuals.shape[0] | ||||||||||||||||||
| total_count += batch.shape[0] | ||||||||||||||||||
|
|
||||||||||||||||||
| assert running_sum is not None | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -806,12 +828,10 @@ def get_logprobs(self, prompts: list[Prompt]) -> Tensor: | |||||||||||||||||
| return logprobs | ||||||||||||||||||
|
|
||||||||||||||||||
| def get_logprobs_batched(self, prompts: list[Prompt]) -> Tensor: | ||||||||||||||||||
| logprobs = [] | ||||||||||||||||||
|
|
||||||||||||||||||
| for batch in batchify(prompts, self.settings.batch_size): | ||||||||||||||||||
| logprobs.append(self.get_logprobs(batch)) | ||||||||||||||||||
|
|
||||||||||||||||||
| return torch.cat(logprobs, dim=0) | ||||||||||||||||||
| return torch.cat( | ||||||||||||||||||
| self._batched(prompts, "logprobs", self.get_logprobs), | ||||||||||||||||||
| dim=0, | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| def stream_chat_response(self, chat: list[dict[str, str]]) -> str: | ||||||||||||||||||
| # This cast is valid because str is the return type | ||||||||||||||||||
|
|
||||||||||||||||||
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.
100 is a better default per #248.