Skip to content

perf(quant): bypass pcre.match for exact dynamic config patterns - #2987

Merged
Qubitium merged 1 commit into
mainfrom
devin/fix-dynamic-exact-regex-upstream
Jul 27, 2026
Merged

perf(quant): bypass pcre.match for exact dynamic config patterns#2987
Qubitium merged 1 commit into
mainfrom
devin/fix-dynamic-exact-regex-upstream

Conversation

@Qubitium

@Qubitium Qubitium commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

dynamic_get compiled and matched every PCRE pattern for every quantized module during model loading. When a quantize_config.json contains many anchored literal patterns (e.g. +:^model\.layers\.0\.mlp\.down_proj$), this results in millions of unnecessary pcre.Pattern.match calls and dominated GPTQModel.load time.

What Changed

  • gptqmodel/quantization/config.py

    • Added _extract_literal_regex_pattern() to detect regexes that are actually anchored literal strings.
    • Added _get_dynamic_patterns() to compile patterns once and separate exact literals from true regexes.
    • Added _resolve_dynamic_override() that resolves exact literals in O(1) via a dict lookup, falling back to ordered regex matching only for genuine regex patterns.
    • Updated dynamic_get() to use _resolve_dynamic_override() while preserving existing key/sub-key/synonym handling.
  • tests/qcfg/test_dynamic_config.py

    • Tests fully-exact, negative-exact, mixed exact+regex, prefix, and ordering semantics.
    • Adds a large-scale regression test with 2,270 exact patterns and 36,432 module-name lookups, asserting zero pcre.match calls.

Tests

  • I added a new simple/fast unit test for this change.
  • I ran the new targeted test locally before opening this PR.
  • I ran any other directly relevant local tests.
cd /root/repos/GPTQModel
/root/vm314t/bin/python -m pytest tests/qcfg/test_dynamic_config.py -q

Result: 6 passed, 16 warnings in 7.57s

Notes

The fix preserves first-match precedence for mixed configs and only treats patterns as exact literals when they have both leading ^ and trailing $ anchors and no unescaped regex metacharacters.

_resolve_dynamic_override now short-circuits anchored literal patterns with an O(1) exact lookup, avoiding one pcre.match per pattern per module during model loading.

True regex patterns still compile and match in original order; mixed exact+regex configs preserve first-match precedence.

Add tests covering fully-exact, negative-exact, mixed, prefix, and large-scale exact configs.
@Qubitium Qubitium self-assigned this Jul 27, 2026
@devin-ai-integration

Copy link
Copy Markdown

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@Qubitium
Qubitium merged commit b62ba01 into main Jul 27, 2026
6 checks passed
@Qubitium
Qubitium deleted the devin/fix-dynamic-exact-regex-upstream branch July 27, 2026 21:01

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +1616 to +1620
cache_key = id(dynamic)
override_cache = _DYNAMIC_OVERRIDE_CACHE.setdefault(cache_key, {})
cached = override_cache.get(module_name, _DYNAMIC_NO_MATCH)
if cached is not _DYNAMIC_NO_MATCH:
return cached

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Per-module quantization overrides added at runtime are silently ignored, giving some layers the wrong precision

Per-module quantization settings are cached the first time a module is looked up (_resolve_dynamic_override at gptqmodel/quantization/config.py:1616-1620) and are never rebuilt when the settings dictionary is modified afterwards, so rules added later return the previously cached answer instead of the new one.
Impact: The lm_head layer (and MoE expert modules excluded mid-run) end up quantized with the base/default settings instead of their intended overrides, silently producing an incorrectly quantized model.

Stale id()-keyed cache vs. post-construction mutation of the dynamic dict

The new caches (_DYNAMIC_PATTERN_CACHE, _DYNAMIC_OVERRIDE_CACHE, _DYNAMIC_EXACT_LOOKUP_CACHE, etc.) are keyed by id(dynamic) and are populated on the first lookup, then never invalidated. The comment at gptqmodel/quantization/config.py:1525-1527 asserts the dynamic dict is "immutable after config construction", but that assumption is violated:

  • gptqmodel/looper/module_looper.py:1433-1434: dynamic_get(lm_head, default=None) is called first (caching override_cache[id(dynamic)][lm_head] = None and the compiled pattern set), and only then is dynamic[lm_head] = lm_head_quant_config inserted. Every subsequent dynamic_get(lm_head, "bits", ...) during quantization hits the stale cached None (and the stale pattern set that never compiled the new key), so the {bits:8, group_size:32, ...} override is ignored and lm_head is quantized with base config.
  • gptqmodel/looper/stage_subset.py:949-953: negative exclusions -:<escaped_name> are appended to qcfg.dynamic after the dict has already been used (and cached) during the quant loop; those exclusions are not reflected in later dynamic_get(...) is False checks.

Because id() values are also reused after garbage collection and the caches keep no reference to the dynamic dict, two different configs whose dynamic dicts happen to share an id could also cross-contaminate. The previous implementation re-iterated dynamic.items() on every call, so it always reflected the current dict contents.

Prompt for agents
The dynamic override resolution now caches compiled patterns and resolved overrides in module-level dicts keyed by id(dynamic) (config.py: _DYNAMIC_PATTERN_CACHE, _DYNAMIC_OVERRIDE_CACHE, _DYNAMIC_EXACT_LOOKUP_CACHE, _DYNAMIC_ALL_EXACT_CACHE, _DYNAMIC_REGEX_PATTERN_CACHE). The design comment assumes the dynamic dict is immutable after construction, but it is mutated at runtime: gptqmodel/looper/module_looper.py:1434 inserts an lm_head rule after calling dynamic_get(lm_head) first, and gptqmodel/looper/stage_subset.py:951 appends negative exclusions after the dict has already been used. Because the id()-keyed caches are never invalidated, these post-hoc rule additions are silently ignored, so lm_head is quantized with base settings instead of the intended {bits:8,...} override, and pruned MoE modules are not treated as excluded. Additionally, id() can be reused after GC (the caches hold no reference to the dynamic dict), risking cross-config contamination. Fix approaches: (a) invalidate/rebuild the caches whenever the dynamic dict changes (e.g. key by a version counter or by a hash/snapshot of the dict, or attach the cache to the QuantizeConfig instance and clear it on mutation), or (b) make the mutation sites clear the relevant cache entries after mutating, or (c) avoid caching resolved overrides for negative/None results that can later be filled in. Ensure the lm_head insertion path and the MoE exclusion path both see fresh results after mutation.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1528 to +1535
_DYNAMIC_PATTERN_CACHE: Dict[int, List[Tuple[bool, Any, Dict[str, Any], Optional[str], int]]] = {}
_DYNAMIC_OVERRIDE_CACHE: Dict[int, Dict[str, Any]] = {}
# Exact-literal fast-path caches: a per-dynamic dict mapping literal module names
# to their resolved override, and whether every pattern is an exact literal.
_DYNAMIC_EXACT_LOOKUP_CACHE: Dict[int, Dict[str, Tuple[int, Union[Dict[str, Any], bool]]]] = {}
_DYNAMIC_ALL_EXACT_CACHE: Dict[int, bool] = {}
# Pre-separated regex patterns for mixed dynamic configs.
_DYNAMIC_REGEX_PATTERN_CACHE: Dict[int, List[Tuple[int, bool, Any, Dict[str, Any]]]] = {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Global dynamic caches grow unbounded across many configs

The five module-level cache dicts (_DYNAMIC_PATTERN_CACHE etc., gptqmodel/quantization/config.py:1528-1535) are keyed by id(dynamic) and never evicted. In a long-running process that constructs many QuantizeConfig objects (e.g. serving/loading many models, or test suites), these caches accumulate entries indefinitely — a slow memory leak. The tests clear them manually via _clear_dynamic_caches, but production code has no eviction path.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant