Configuration Management¶
See also: Documentation Index, Hardware-Aware Normalization, PostgreSQL Connection and Knobs, Autotuning Knob Policy
Overview¶
The configuration management layer is responsible for defining what may be tuned and applying tuned values to a live PostgreSQL safely. Two components do this:
KnobSpace— the search space. Loaded from per-tier CSVs (or from a data-driven manifest), exposes per-knob bounds, scale, default, restart context, and hardware-relative fractional encoding. Owns sampling, perturbation, dependency repair, and warm-start translation.KnobApplicator— the runtime applier. Validates againstpg_settings, writes viaALTER SYSTEM(persistent) orSET(session), respects parameter contexts (postmaster/sighup/user), and exposes averify()read-back that returns the actually quantised values PostgreSQL is running with.
These two layers are independent. KnobSpace is the contract PBT and the BO baseline both consume; KnobApplicator is what the orchestrator calls per evaluation.
Table of Contents¶
- Architecture
- KnobSpace
- Knob tiers and data sources
- Hardware-relative fractional encoding
- Sampling, perturbation, and dependency repair
- Warm-start serialisation
- KnobApplicator
- Verifying applied config
- Two-layer validation
- Design decisions
- Related documentation
Architecture¶
┌──────────────────────┐
│ KnobSpace │
│ (search space) │
data/expert_ │ │
defined_knobs/ │ load_knob_space_ │
{tier}.csv ───────► │ for_tier() │
│ │
data/data_driven_ │ resolve_hardware_ │
knobs/{workload}/ │ ranges() │
{tier}.csv ───────► │ │
│ sample / perturb / │
│ repair / fractions │
└──────────┬───────────┘
│ dict[str, Any]
▼
┌──────────────────────┐
│ Worker.knob_config │
└──────────┬───────────┘
│
│ orchestrator.evaluate_worker()
▼
┌──────────────────────┐
│ KnobApplicator │
│ │
│ apply() → ALTER │
│ SYSTEM/SET │
│ verify() → read- │
│ back from │
│ current_ │
│ setting() │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ PostgreSQL │
└──────────────────────┘
KnobSpace¶
Location: src/knobs/knob_space.py
A KnobSpace is a typed collection of KnobDefinition records. It is the single source of truth for which knobs are tunable in this session and with what bounds.
class KnobType(Enum):
INTEGER = "integer"
REAL = "real"
BOOLEAN = "bool"
ENUM = "enum"
class KnobScale(Enum):
LINEAR = "linear"
LOG = "log"
@dataclass
class KnobDefinition:
name: str
knob_type: KnobType
min_value: Optional[Union[int, float]]
max_value: Optional[Union[int, float]]
scale: KnobScale
default: Any
unit: Optional[str]
enum_values: Optional[List[str]]
description: str
category: str
restart_required: bool
# ... see source for full list
KnobSpace public API¶
| Method | Purpose |
|---|---|
__getitem__(name), __contains__(name), __len__() |
Mapping-style access. |
get_restart_required_knobs() |
Names of postmaster-context knobs. |
get_runtime_modifiable_knobs() |
Names of sighup / user knobs. |
split_config_by_restart_requirement(config) |
Splits a config dict into (restart_required, runtime) for the orchestrator's restart policy. |
validate_config(config) |
Returns (is_valid, errors) against tuning bounds. |
resolve_hardware_ranges(resources) |
Rewrites bounds for hardware_relative=True knobs against a WorkerResources slice. |
repair_config_dependencies(config, worker_id, budget_ram_bytes) |
Aggregate memory-budget repair (see below). |
sample_random_config(seed), sample_diverse_configs(n, seed) |
LHS-style population seeding. |
perturb_config(config, factors) |
The explore step. |
config_to_fractions(config), fractions_to_config(fractions) |
Warm-start serialisation. |
create_online_view() |
A KnobSpace filtered to runtime-modifiable knobs only (for ONLINE tuning mode). |
The full file lives at src/knobs/knob_space.py — every method has a Google-style docstring.
Knob tiers and data sources¶
Location: src/knobs/knob_loader.py
There are four canonical tiers: minimal, core, standard, extensive. Each tier corresponds to a CSV under data/. Two layouts are supported:
data/
├── expert_defined_knobs/ # canonical, hand-curated
│ ├── minimal_knobs.csv
│ ├── core_knobs.csv
│ ├── standard_knobs.csv
│ └── extensive_knobs.csv
└── data_driven_knobs/ # optional, derived from analysis
└── {workload}/ # e.g. oltp_read_write
├── minimal_knobs.csv
├── core_knobs.csv
├── standard_knobs.csv
├── extensive_knobs.csv
└── data_driven_tiers.json
get_knob_space(tier, workload=None, source="expert") picks the right CSV. When source="data_driven" and workload is set, the loader prefers data/data_driven_knobs/{workload}/{tier}_knobs.csv — these are the tier CSVs produced by src/analysis/tier_generator.py from fANOVA+TreeSHAP importance (see KNOB_IMPORTANCE_ANALYSIS.md).
CSV format constants live in knob_loader.py:
The CSV columns mirror KnobDefinition fields (name, vartype, min_value, max_value, scale, default, unit, enumvals, category, restart_required, hardware_relative, tuning_metadata). The tuning_metadata JSON column carries fields not present in pg_settings — see src/knobs/knob_metadata.py.
Knob policy file¶
data/knob_policy.json selects which knobs are eligible for tuning at all (vs frozen for safety) regardless of tier. The policy is loaded by src/knobs/policy.py and applied as a filter at CSV-load time. See AUTOTUNING_KNOB_POLICY.md for the reasoning behind each frozen knob.
Hardware-relative fractional encoding¶
For knobs flagged hardware_relative=True in metadata (RAM-dependent knobs like shared_buffers, work_mem; CPU-dependent like max_parallel_workers; disk-dependent like random_page_cost), the bounds in the CSV are not absolute — they are fractions of the worker's available resources.
At session start, the tuner calls detect_worker_resources() to get a WorkerResources slice for each worker (host total ÷ parallel-worker count, less an 80 % budget headroom). Then:
rewrites each hardware-relative knob's min_value and max_value to absolute integers/floats appropriate for that worker.
This is what makes warm-start across hardware work: a best_config.json produced on a 2 GB container can be re-resolved against an 8 GB container without OOM, because the stored configuration is fractional and gets converted to absolute values against the new resource ceiling.
Details and the list of all hardware-relative knobs: HARDWARE_AWARE_NORMALIZATION.md.
Sampling, perturbation, and dependency repair¶
Sampling¶
sample_random_config(seed) samples each knob independently. Numeric knobs use either linear or log sampling per KnobScale. sample_diverse_configs(n, seed) uses a Latin Hypercube design to seed the initial population with better space coverage than independent sampling.
Perturbation (explore)¶
perturb_config(config, factors=(0.8, 1.2)) is the explore step from the PBT paper. For numeric knobs it multiplies by U(factors[0], factors[1]) and clamps to bounds. Booleans flip with a configurable probability. Enums probabilistically jump to a neighbour. The implementation correctly handles log-scale knobs (perturbation in log space, not linear space) so a +20% move on shared_buffers does what you expect.
Memory-budget repair¶
repair_config_dependencies(config, worker_id, budget_ram_bytes) runs the aggregate memory check before a config ever reaches PostgreSQL:
If total_bytes exceeds the worker's RAM budget, every memory knob is scaled down by the same multiplier budget / total. This preserves the ratios the perturbation chose while bringing total memory back under the budget. Without this, the explore step could easily produce configurations that fail to start (shared_buffers alone larger than container RAM).
Implementation: src/knobs/knob_space.py:_repair_memory_budget.
Warm-start serialisation¶
config_to_fractions(config) and fractions_to_config(fractions) are the round-trip used by --warm-start. Hardware-relative knobs are serialised as fractions of their resolved bounds; absolute knobs are kept as-is. The resulting JSON survives transport across hardware.
The full warm-start flow lives in src/tuners/pbt/cli.py and is described in HARDWARE_AWARE_NORMALIZATION.md §5. Notably, since commit 858d482 the warm-start serialiser now writes the full knob space, not just the best configuration — this lets the loader gracefully drop knobs that no longer exist in the target tier and LHS-fill new ones.
KnobApplicator¶
Location: src/utils/applicator.py
KnobApplicator applies a dict[str, Any] of knob values to a live PostgreSQL instance. It is the only component that talks to PostgreSQL's configuration surface.
ApplicatorConfig¶
@dataclass
class ApplicatorConfig:
persist: bool = True # ALTER SYSTEM (persisted) vs SET (session)
auto_reload: bool = True # pg_reload_conf() for sighup knobs
validate: bool = True # check against pg_settings before applying
dry_run: bool = False
rollback_on_error: bool = True
allow_restart_params: bool = True
apply(knob_config) -> ApplicationResult¶
The flow:
- Connect — establishes a connection (cached if pooled).
- Load parameter info — single
SELECT … FROM pg_settings WHERE name IN (…)for every knob inknob_config. - Validate each knob against
pg_settings.vartype/min_val/max_val/enumvals/ context. - Apply in a single transaction.
persist=True⇒ALTER SYSTEM SET, elseSET. - Reload via
pg_reload_conf()if anysighupknob changed andauto_reload=True. - Commit or rollback — atomic. On any per-knob failure with
rollback_on_error=True, the whole transaction rolls back via psycopg2 — there is no per-knob undo path.
Return value:
@dataclass
class ApplicationResult:
success: bool
applied: dict[str, Any]
failed: dict[str, str]
restart_required: set[str]
applied_count: int
failed_count: int
message: str
restart_required is populated when a postmaster-context knob was applied (via ALTER SYSTEM) but PostgreSQL has not yet been restarted — the orchestrator uses this to decide whether to trigger a restart at barrier B3.
Context manager¶
KnobApplicator is a context manager; __enter__ connects and __exit__ disconnects unconditionally, including under exception. The orchestrator uses both the context-manager form for short-lived operations and the explicit connect() / disconnect() form when it needs to keep a connection alive across multiple barrier sub-steps.
Verifying applied config¶
verify(expected_config) -> VerificationResult reads back the actually applied values from PostgreSQL via current_setting(name) for each knob and returns:
@dataclass
class VerificationResult:
matches: dict[str, bool]
db_config: dict[str, Any] # the quantised values PostgreSQL is using
Two problems make this method non-trivial:
- Quantisation — PostgreSQL rounds many values to internal block boundaries (e.g.
shared_buffersto the nearest 8 kB page). A suggested 134217729 becomes 134217728 internally. Withoutverify()the optimiser believes its raw suggestion is what's running. - Unit conversion —
pg_settings.settingis a raw number plus a separateunitcolumn.current_setting()returns PostgreSQL's normalised typed value directly, avoiding manual unit math.
The orchestrator calls verify() at barrier B5 and merges db_config back into the worker's knob_config before scoring. This makes session JSON faithful to what the database actually ran. The BO baseline does the same to keep its surrogate model gradients honest — see BO_BASELINE.md §Quantization & Read-Back Parity.
Two-layer validation¶
Validation happens twice for a reason:
| Layer | Component | Purpose | Failure mode |
|---|---|---|---|
| Design-time | KnobSpace.validate_config() |
Keep PBT inside reasonable tuning bounds. | Reject + clamp + log. |
| Runtime | KnobApplicator._validate_parameter() |
Catch anything PostgreSQL would reject (context, type, hard min/max, enum). | Rollback whole apply. |
The design-time range is always a strict subset of the PostgreSQL-allowed range. The design-time bounds are where we want PBT to explore; the runtime bounds are where PostgreSQL allows. Two checks let us tighten exploration without losing PostgreSQL-level safety.
Design decisions¶
1. KnobSpace owns hardware resolution¶
resolve_hardware_ranges lives on KnobSpace, not on a separate hardware layer, because every consumer of KnobSpace (PBT, BO baseline, warm-start) needs the same resolved view. Putting it on the space avoids the surface area for "I forgot to resolve and got OOMed" bugs.
2. CSV-tiered loading instead of code-defined sets¶
Tiers were once hard-coded lists in Python. They are now CSVs because:
- analysis (
src/analysis/tier_generator.py) can write new tiers without code edits, - expert-defined and data-driven tiers can coexist (
data/expert_defined_knobs/vsdata/data_driven_knobs/{workload}/), - the same file is the source of truth for the BO baseline and any tooling that has to enumerate the search space.
3. Transaction-level rollback only¶
KnobApplicator does not implement a per-knob undo log. PostgreSQL's transaction semantics already guarantee atomicity on ALTER SYSTEM SET. A custom undo would duplicate that guarantee and add a failure mode.
4. verify() returns the read-back, the caller merges¶
The caller decides whether the quantised values replace the suggested values. PBT merges (so lineage tracking is honest); BO merges (so surrogate gradients are honest). A future analyzer that wants to diff suggested-vs-applied can leave the suggestion intact and use verification.db_config separately.
5. Knob policy is a filter, not a fork¶
data/knob_policy.json filters which knobs are eligible for tuning. It is applied after CSV load, so the tier CSVs can list every plausible knob and the policy decides which are safe to tune in a given environment. This avoids forking the CSVs per policy.
Related documentation¶
- Hardware-Aware Normalization — fractional encoding,
WorkerResources, warm-start across hardware. - PostgreSQL Connection and Knobs —
pg_settingsretrieval,PostgreSQLKnobRetriever, knob policy. - Autotuning Knob Policy — per-knob tuning rationale and safety classification.
- Knob Importance Analysis — how
data_driven_knobs/CSVs are generated. - Workload Orchestrator — how the applicator is driven during an evaluation.
- PBT Core Components — how
KnobSpaceis consumed by the worker and population.
File locations¶
KnobSpace,KnobDefinition,KnobType,KnobScale: src/knobs/knob_space.py- Tier CSV loader: src/knobs/knob_loader.py
KnobApplicator,ApplicatorConfig,ApplicationResult,VerificationResult: src/utils/applicator.py- Knob metadata overlay: src/knobs/knob_metadata.py
- Knob policy filter: src/knobs/policy.py
- Tier CSVs: data/expert_defined_knobs/, data/data_driven_knobs/
- Tests: tests/unit/knobs/, tests/unit/config/test_hardware_normalization.py