crprotocol 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crp/__init__.py +126 -0
- crp/__main__.py +8 -0
- crp/_typing.py +27 -0
- crp/_version.py +5 -0
- crp/adapters.py +31 -0
- crp/advanced/__init__.py +40 -0
- crp/advanced/auto_ingest.py +400 -0
- crp/advanced/cqs.py +235 -0
- crp/advanced/cross_window.py +477 -0
- crp/advanced/curator.py +265 -0
- crp/advanced/feedback.py +146 -0
- crp/advanced/hierarchical.py +211 -0
- crp/advanced/meta_learning.py +401 -0
- crp/advanced/parallel.py +98 -0
- crp/advanced/review_cycle.py +329 -0
- crp/advanced/scale_mode.py +129 -0
- crp/advanced/source_grounding.py +207 -0
- crp/ckf/__init__.py +35 -0
- crp/ckf/community.py +377 -0
- crp/ckf/fabric.py +445 -0
- crp/ckf/gc.py +175 -0
- crp/ckf/graph_walk.py +87 -0
- crp/ckf/merge.py +133 -0
- crp/ckf/pattern_query.py +122 -0
- crp/ckf/pubsub.py +128 -0
- crp/ckf/semantic.py +207 -0
- crp/cli/__init__.py +7 -0
- crp/cli/main.py +329 -0
- crp/cli/sidecar.py +929 -0
- crp/cli/startup.py +272 -0
- crp/continuation/__init__.py +103 -0
- crp/continuation/completion.py +348 -0
- crp/continuation/degradation.py +157 -0
- crp/continuation/document_map.py +160 -0
- crp/continuation/flow.py +109 -0
- crp/continuation/gap.py +419 -0
- crp/continuation/manager.py +484 -0
- crp/continuation/quality_monitor.py +179 -0
- crp/continuation/stitch.py +419 -0
- crp/continuation/trigger.py +142 -0
- crp/continuation/voice.py +157 -0
- crp/core/__init__.py +69 -0
- crp/core/batch.py +77 -0
- crp/core/circuit_breaker.py +116 -0
- crp/core/config.py +377 -0
- crp/core/context_tools.py +540 -0
- crp/core/dispatch_router.py +3977 -0
- crp/core/errors.py +128 -0
- crp/core/extraction_facade.py +384 -0
- crp/core/facilitator.py +713 -0
- crp/core/idempotency.py +215 -0
- crp/core/orchestrator.py +1435 -0
- crp/core/relay_strategies.py +613 -0
- crp/core/security_manager.py +140 -0
- crp/core/session.py +134 -0
- crp/core/task_intent.py +36 -0
- crp/core/window.py +363 -0
- crp/envelope/__init__.py +30 -0
- crp/envelope/builder.py +288 -0
- crp/envelope/decomposer.py +236 -0
- crp/envelope/formatter.py +168 -0
- crp/envelope/packer.py +211 -0
- crp/envelope/reranker.py +209 -0
- crp/envelope/scoring.py +310 -0
- crp/extraction/__init__.py +45 -0
- crp/extraction/complexity.py +96 -0
- crp/extraction/contradiction.py +132 -0
- crp/extraction/pipeline.py +360 -0
- crp/extraction/quality_gate.py +237 -0
- crp/extraction/stage1_regex.py +173 -0
- crp/extraction/stage2_statistical.py +244 -0
- crp/extraction/stage3_gliner.py +210 -0
- crp/extraction/stage4_uie.py +183 -0
- crp/extraction/stage5_discourse.py +175 -0
- crp/extraction/stage6_llm.py +178 -0
- crp/extraction/structured_output.py +219 -0
- crp/extraction/types.py +299 -0
- crp/license_guard.py +722 -0
- crp/observability/__init__.py +30 -0
- crp/observability/audit.py +118 -0
- crp/observability/events.py +233 -0
- crp/observability/metrics.py +264 -0
- crp/observability/quality.py +135 -0
- crp/observability/structured_logging.py +81 -0
- crp/observability/telemetry.py +117 -0
- crp/provenance/__init__.py +314 -0
- crp/provenance/_embeddings.py +97 -0
- crp/provenance/_types.py +378 -0
- crp/provenance/attribution_scorer.py +252 -0
- crp/provenance/claim_detector.py +229 -0
- crp/provenance/contradiction_detector.py +243 -0
- crp/provenance/distortion_detector.py +397 -0
- crp/provenance/entailment_verifier.py +358 -0
- crp/provenance/fabrication_detector.py +203 -0
- crp/provenance/hallucination_scorer.py +320 -0
- crp/provenance/omission_analyzer.py +106 -0
- crp/provenance/provenance_chain.py +205 -0
- crp/provenance/report_generator.py +440 -0
- crp/providers/__init__.py +43 -0
- crp/providers/anthropic.py +270 -0
- crp/providers/base.py +135 -0
- crp/providers/custom.py +63 -0
- crp/providers/diagnostic.py +251 -0
- crp/providers/llamacpp.py +224 -0
- crp/providers/manager.py +139 -0
- crp/providers/ollama.py +243 -0
- crp/providers/openai.py +628 -0
- crp/providers/tokenizers.py +48 -0
- crp/py.typed +0 -0
- crp/resources/__init__.py +53 -0
- crp/resources/adaptive_allocator.py +525 -0
- crp/resources/cost_model.py +388 -0
- crp/resources/overhead_manager.py +217 -0
- crp/resources/resource_manager.py +262 -0
- crp/schemas/__init__.py +20 -0
- crp/schemas/cost-estimate.json +33 -0
- crp/schemas/crp-error.json +43 -0
- crp/schemas/envelope-preview.json +40 -0
- crp/schemas/persisted-state-header.json +27 -0
- crp/schemas/quality-report.json +94 -0
- crp/schemas/session-handle.json +33 -0
- crp/schemas/session-status.json +57 -0
- crp/schemas/stream-event.json +18 -0
- crp/schemas/task-intent.json +42 -0
- crp/security/__init__.py +93 -0
- crp/security/audit_trail.py +392 -0
- crp/security/binding.py +192 -0
- crp/security/compliance.py +813 -0
- crp/security/consent.py +593 -0
- crp/security/embedding_defense.py +161 -0
- crp/security/encryption.py +202 -0
- crp/security/injection.py +335 -0
- crp/security/integrity.py +267 -0
- crp/security/privacy.py +662 -0
- crp/security/quarantine.py +249 -0
- crp/security/rbac.py +221 -0
- crp/security/validation.py +164 -0
- crp/state/__init__.py +31 -0
- crp/state/cold_storage.py +258 -0
- crp/state/compaction.py +263 -0
- crp/state/critical_state.py +104 -0
- crp/state/event_log.py +313 -0
- crp/state/fact.py +189 -0
- crp/state/serialization.py +189 -0
- crp/state/session_cleanup.py +77 -0
- crp/state/snapshot.py +290 -0
- crp/state/warm_store.py +346 -0
- crprotocol-2.0.0.dist-info/METADATA +1295 -0
- crprotocol-2.0.0.dist-info/RECORD +153 -0
- crprotocol-2.0.0.dist-info/WHEEL +4 -0
- crprotocol-2.0.0.dist-info/entry_points.txt +2 -0
- crprotocol-2.0.0.dist-info/licenses/LICENSE.md +170 -0
- crprotocol-2.0.0.dist-info/licenses/NOTICE +18 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Resource management — allocation, monitoring, model registry, cost model."""
|
|
4
|
+
|
|
5
|
+
from crp.resources.adaptive_allocator import (
|
|
6
|
+
AdaptiveAllocator,
|
|
7
|
+
EnvelopeProfile,
|
|
8
|
+
ExtractionProfile,
|
|
9
|
+
PromptEfficiency,
|
|
10
|
+
WindowOverheadRecord,
|
|
11
|
+
detect_hardware,
|
|
12
|
+
)
|
|
13
|
+
from crp.resources.cost_model import (
|
|
14
|
+
KNOWN_PRICING,
|
|
15
|
+
BudgetWarning,
|
|
16
|
+
BudgetWarningLevel,
|
|
17
|
+
CostModel,
|
|
18
|
+
OverheadBudget,
|
|
19
|
+
OverheadDecision,
|
|
20
|
+
ProviderPricing,
|
|
21
|
+
WindowCost,
|
|
22
|
+
)
|
|
23
|
+
from crp.resources.overhead_manager import (
|
|
24
|
+
PROTECTED_INTELLIGENCE,
|
|
25
|
+
SHEDDING_CASCADE,
|
|
26
|
+
OverheadBudgetManager,
|
|
27
|
+
)
|
|
28
|
+
from crp.resources.resource_manager import (
|
|
29
|
+
MODEL_ESTIMATES,
|
|
30
|
+
ResourceManager,
|
|
31
|
+
ResourceSnapshot,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"AdaptiveAllocator",
|
|
36
|
+
"CostModel",
|
|
37
|
+
"EnvelopeProfile",
|
|
38
|
+
"ExtractionProfile",
|
|
39
|
+
"MODEL_ESTIMATES",
|
|
40
|
+
"OverheadBudget",
|
|
41
|
+
"OverheadDecision",
|
|
42
|
+
"OverheadBudgetManager",
|
|
43
|
+
"ProviderPricing",
|
|
44
|
+
"ResourceManager",
|
|
45
|
+
"ResourceSnapshot",
|
|
46
|
+
"WindowCost",
|
|
47
|
+
"WindowOverheadRecord",
|
|
48
|
+
"BudgetWarning",
|
|
49
|
+
"BudgetWarningLevel",
|
|
50
|
+
"KNOWN_PRICING",
|
|
51
|
+
"SHEDDING_CASCADE",
|
|
52
|
+
"detect_hardware",
|
|
53
|
+
]
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# Copyright © 2025 Constantinos Vidiniotis. All rights reserved.
|
|
2
|
+
# Licensed under Elastic License 2.0 — see LICENSE.md for details.
|
|
3
|
+
"""Adaptive resource allocator — efficient CRP pipeline without cutting intelligence.
|
|
4
|
+
|
|
5
|
+
Integrates ResourceManager (memory pressure) with OverheadBudgetManager
|
|
6
|
+
(feature shedding) to dynamically adapt CRP's processing pipeline based
|
|
7
|
+
on real-time performance measurements.
|
|
8
|
+
|
|
9
|
+
Design philosophy:
|
|
10
|
+
CRP is an EFFICIENCY protocol, not a speed protocol. ML extraction
|
|
11
|
+
stages (GLiNER, UIE, Discourse) are core intelligence and are NEVER
|
|
12
|
+
disabled. Under pressure, the system *throttles throughput* (fewer
|
|
13
|
+
facts per stage, larger batches, prompt caching) rather than cutting
|
|
14
|
+
analytical capability.
|
|
15
|
+
|
|
16
|
+
Design goals:
|
|
17
|
+
1. Keep CRP overhead below a configurable cap (default 15% of wall time)
|
|
18
|
+
2. Shed optional optimization features under pressure (community detection,
|
|
19
|
+
cross-encoder reranking) while protecting ML intelligence
|
|
20
|
+
3. Throttle throughput gracefully: reduce fact counts, increase batches
|
|
21
|
+
4. Provide prompt-level efficiency hints (caching, dedup, connection reuse)
|
|
22
|
+
5. Track per-window overhead history and adapt thresholds
|
|
23
|
+
6. Provide hardware-aware defaults (CPU count, available RAM)
|
|
24
|
+
|
|
25
|
+
Efficiency strategies (applied in order of priority):
|
|
26
|
+
Phase 1 — Prompt efficiency (caching, deduplication, connection reuse)
|
|
27
|
+
Phase 2 — Feature shedding (community_detection, cross_encoder only)
|
|
28
|
+
Phase 3 — Throughput throttling (reduce facts per stage, larger batches)
|
|
29
|
+
Phase 4 — Model lifecycle (idle models freed after timeout)
|
|
30
|
+
Phase 5 — GC sweep (Python garbage collection under memory pressure)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import time
|
|
38
|
+
import threading
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
|
|
41
|
+
from crp.resources.overhead_manager import OverheadBudgetManager, SHEDDING_CASCADE, PROTECTED_INTELLIGENCE
|
|
42
|
+
from crp.resources.resource_manager import ResourceManager
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
# Hardware detection
|
|
49
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
def detect_hardware() -> dict[str, int | float]:
|
|
52
|
+
"""Detect available hardware resources (best-effort)."""
|
|
53
|
+
cpu_count = os.cpu_count() or 1
|
|
54
|
+
try:
|
|
55
|
+
if os.name == "nt":
|
|
56
|
+
import ctypes
|
|
57
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
58
|
+
|
|
59
|
+
class MEMORYSTATUSEX(ctypes.Structure):
|
|
60
|
+
_fields_ = [
|
|
61
|
+
("dwLength", ctypes.c_ulong),
|
|
62
|
+
("dwMemoryLoad", ctypes.c_ulong),
|
|
63
|
+
("ullTotalPhys", ctypes.c_ulonglong),
|
|
64
|
+
("ullAvailPhys", ctypes.c_ulonglong),
|
|
65
|
+
("ullTotalPageFile", ctypes.c_ulonglong),
|
|
66
|
+
("ullAvailPageFile", ctypes.c_ulonglong),
|
|
67
|
+
("ullTotalVirtual", ctypes.c_ulonglong),
|
|
68
|
+
("ullAvailVirtual", ctypes.c_ulonglong),
|
|
69
|
+
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
mem = MEMORYSTATUSEX()
|
|
73
|
+
mem.dwLength = ctypes.sizeof(mem)
|
|
74
|
+
kernel32.GlobalMemoryStatusEx(ctypes.byref(mem))
|
|
75
|
+
total_ram_mb = mem.ullTotalPhys / (1024 * 1024)
|
|
76
|
+
available_ram_mb = mem.ullAvailPhys / (1024 * 1024)
|
|
77
|
+
else:
|
|
78
|
+
with open("/proc/meminfo") as f:
|
|
79
|
+
info = {}
|
|
80
|
+
for line in f:
|
|
81
|
+
parts = line.split()
|
|
82
|
+
if len(parts) >= 2:
|
|
83
|
+
info[parts[0].rstrip(":")] = int(parts[1])
|
|
84
|
+
total_ram_mb = info.get("MemTotal", 0) / 1024
|
|
85
|
+
available_ram_mb = info.get("MemAvailable", info.get("MemFree", 0)) / 1024
|
|
86
|
+
except Exception:
|
|
87
|
+
total_ram_mb = 0.0
|
|
88
|
+
available_ram_mb = 0.0
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"cpu_count": cpu_count,
|
|
92
|
+
"total_ram_mb": round(total_ram_mb),
|
|
93
|
+
"available_ram_mb": round(available_ram_mb),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
# Overhead history tracker
|
|
99
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class WindowOverheadRecord:
|
|
103
|
+
"""Per-window overhead measurement."""
|
|
104
|
+
window_index: int = 0
|
|
105
|
+
total_ms: float = 0.0
|
|
106
|
+
llm_ms: float = 0.0
|
|
107
|
+
overhead_ms: float = 0.0
|
|
108
|
+
overhead_pct: float = 0.0
|
|
109
|
+
envelope_ms: float = 0.0
|
|
110
|
+
extraction_ms: float = 0.0
|
|
111
|
+
features_shed: list[str] = field(default_factory=list)
|
|
112
|
+
stages_skipped: list[int] = field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
# Extraction profile — what stages to run
|
|
117
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class ExtractionProfile:
|
|
121
|
+
"""Recommended extraction configuration based on resource state."""
|
|
122
|
+
enable_stage_3: bool = True # GLiNER NER
|
|
123
|
+
enable_stage_4: bool = True # UIE relation extraction
|
|
124
|
+
enable_stage_5: bool = True # Discourse analysis
|
|
125
|
+
enable_stage_6: bool = False # LLM-assisted (always off by default)
|
|
126
|
+
max_facts_per_stage: int = 200
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class EnvelopeProfile:
|
|
131
|
+
"""Recommended envelope configuration based on resource state."""
|
|
132
|
+
enable_cross_encoder: bool = True
|
|
133
|
+
enable_ckf: bool = True
|
|
134
|
+
max_packing_facts: int = 500
|
|
135
|
+
embedding_batch_size: int = 32
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class PromptEfficiency:
|
|
140
|
+
"""LLM-side efficiency recommendations based on pipeline state.
|
|
141
|
+
|
|
142
|
+
These hints allow providers to optimize token usage, connection
|
|
143
|
+
management, and prompt caching — reducing cost and latency without
|
|
144
|
+
cutting any intelligence features.
|
|
145
|
+
"""
|
|
146
|
+
deduplicate_facts: bool = True
|
|
147
|
+
cache_system_prompt: bool = True
|
|
148
|
+
compress_envelope: bool = False
|
|
149
|
+
reuse_connection: bool = True
|
|
150
|
+
estimated_cache_hit_pct: float = 0.0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Throughput levels — graceful throttling without cutting intelligence
|
|
154
|
+
THROUGHPUT_NORMAL = "normal"
|
|
155
|
+
THROUGHPUT_THROTTLED = "throttled"
|
|
156
|
+
THROUGHPUT_CONSTRAINED = "constrained"
|
|
157
|
+
|
|
158
|
+
# Facts-per-stage at each throughput level
|
|
159
|
+
_THROUGHPUT_FACTS: dict[str, int] = {
|
|
160
|
+
THROUGHPUT_NORMAL: 200,
|
|
161
|
+
THROUGHPUT_THROTTLED: 150,
|
|
162
|
+
THROUGHPUT_CONSTRAINED: 100,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Embedding batch sizes at each throughput level
|
|
166
|
+
_THROUGHPUT_BATCH: dict[str, int] = {
|
|
167
|
+
THROUGHPUT_NORMAL: 32,
|
|
168
|
+
THROUGHPUT_THROTTLED: 48,
|
|
169
|
+
THROUGHPUT_CONSTRAINED: 64,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Max packing facts at each throughput level
|
|
173
|
+
_THROUGHPUT_PACKING: dict[str, int] = {
|
|
174
|
+
THROUGHPUT_NORMAL: 500,
|
|
175
|
+
THROUGHPUT_THROTTLED: 350,
|
|
176
|
+
THROUGHPUT_CONSTRAINED: 200,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
181
|
+
# Adaptive Allocator
|
|
182
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class AdaptiveAllocator:
|
|
186
|
+
"""Dynamically tunes CRP pipeline based on real-time overhead and pressure.
|
|
187
|
+
|
|
188
|
+
The allocator observes per-window overhead ratios and resource pressure,
|
|
189
|
+
then adjusts throughput parameters and prompt efficiency hints to keep
|
|
190
|
+
overhead below the configured cap.
|
|
191
|
+
|
|
192
|
+
Core principle: ML extraction stages (GLiNER, UIE, Discourse) are
|
|
193
|
+
NEVER disabled — they are the protocol's core intelligence. Under
|
|
194
|
+
pressure, the system reduces fact counts and increases batch sizes
|
|
195
|
+
(graceful throttling) rather than cutting analytical capability.
|
|
196
|
+
|
|
197
|
+
Usage::
|
|
198
|
+
|
|
199
|
+
alloc = AdaptiveAllocator(
|
|
200
|
+
resource_manager=rm,
|
|
201
|
+
overhead_manager=om,
|
|
202
|
+
overhead_cap_pct=15.0,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Before each dispatch — get recommended profiles
|
|
206
|
+
ext_profile = alloc.extraction_profile()
|
|
207
|
+
env_profile = alloc.envelope_profile()
|
|
208
|
+
efficiency = alloc.prompt_efficiency()
|
|
209
|
+
|
|
210
|
+
# After each dispatch — record overhead
|
|
211
|
+
alloc.record_window(total_ms=1200, llm_ms=1050,
|
|
212
|
+
envelope_ms=80, extraction_ms=50)
|
|
213
|
+
|
|
214
|
+
# Cleanup check
|
|
215
|
+
if alloc.should_unload_models():
|
|
216
|
+
for model in alloc.idle_models():
|
|
217
|
+
unload(model)
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
# Overhead smoothing: EWMA decay factor (0.3 = recent windows weighted more)
|
|
221
|
+
_EWMA_ALPHA = 0.3
|
|
222
|
+
|
|
223
|
+
# How many consecutive windows over cap before constrained throughput
|
|
224
|
+
_CONSECUTIVE_OVER_LIMIT = 3
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
resource_manager: ResourceManager,
|
|
229
|
+
overhead_manager: OverheadBudgetManager,
|
|
230
|
+
*,
|
|
231
|
+
overhead_cap_pct: float = 15.0,
|
|
232
|
+
idle_model_timeout_s: float = 300.0,
|
|
233
|
+
max_history: int = 50,
|
|
234
|
+
) -> None:
|
|
235
|
+
self._rm = resource_manager
|
|
236
|
+
self._om = overhead_manager
|
|
237
|
+
self._cap = overhead_cap_pct
|
|
238
|
+
self._idle_timeout = idle_model_timeout_s
|
|
239
|
+
self._lock = threading.Lock()
|
|
240
|
+
|
|
241
|
+
# Overhead history
|
|
242
|
+
self._history: list[WindowOverheadRecord] = []
|
|
243
|
+
self._max_history = max_history
|
|
244
|
+
self._window_count = 0
|
|
245
|
+
|
|
246
|
+
# EWMA smoothed overhead
|
|
247
|
+
self._ewma_overhead_pct: float = 0.0
|
|
248
|
+
|
|
249
|
+
# Consecutive windows over cap
|
|
250
|
+
self._consecutive_over: int = 0
|
|
251
|
+
|
|
252
|
+
# Throughput level — graceful throttling (never disables ML stages)
|
|
253
|
+
self._throughput_level: str = THROUGHPUT_NORMAL
|
|
254
|
+
|
|
255
|
+
# Hardware snapshot
|
|
256
|
+
self._hardware = detect_hardware()
|
|
257
|
+
|
|
258
|
+
logger.info(
|
|
259
|
+
"AdaptiveAllocator initialized: cap=%.1f%%, hardware=%s",
|
|
260
|
+
overhead_cap_pct, self._hardware,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# -- Record overhead per window ----------------------------------------
|
|
264
|
+
|
|
265
|
+
def record_window(
|
|
266
|
+
self,
|
|
267
|
+
total_ms: float,
|
|
268
|
+
llm_ms: float,
|
|
269
|
+
*,
|
|
270
|
+
envelope_ms: float = 0.0,
|
|
271
|
+
extraction_ms: float = 0.0,
|
|
272
|
+
) -> WindowOverheadRecord:
|
|
273
|
+
"""Record overhead from a completed window and adapt."""
|
|
274
|
+
overhead_ms = total_ms - llm_ms
|
|
275
|
+
overhead_pct = (overhead_ms / total_ms * 100) if total_ms > 0 else 0.0
|
|
276
|
+
|
|
277
|
+
with self._lock:
|
|
278
|
+
self._window_count += 1
|
|
279
|
+
|
|
280
|
+
# Update EWMA
|
|
281
|
+
if self._window_count == 1:
|
|
282
|
+
self._ewma_overhead_pct = overhead_pct
|
|
283
|
+
else:
|
|
284
|
+
self._ewma_overhead_pct = (
|
|
285
|
+
self._EWMA_ALPHA * overhead_pct
|
|
286
|
+
+ (1 - self._EWMA_ALPHA) * self._ewma_overhead_pct
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Track consecutive over-cap windows
|
|
290
|
+
if overhead_pct > self._cap:
|
|
291
|
+
self._consecutive_over += 1
|
|
292
|
+
else:
|
|
293
|
+
self._consecutive_over = 0
|
|
294
|
+
|
|
295
|
+
# Build record
|
|
296
|
+
record = WindowOverheadRecord(
|
|
297
|
+
window_index=self._window_count,
|
|
298
|
+
total_ms=round(total_ms, 1),
|
|
299
|
+
llm_ms=round(llm_ms, 1),
|
|
300
|
+
overhead_ms=round(overhead_ms, 1),
|
|
301
|
+
overhead_pct=round(overhead_pct, 1),
|
|
302
|
+
envelope_ms=round(envelope_ms, 1),
|
|
303
|
+
extraction_ms=round(extraction_ms, 1),
|
|
304
|
+
features_shed=[
|
|
305
|
+
f for f in SHEDDING_CASCADE
|
|
306
|
+
if f not in PROTECTED_INTELLIGENCE
|
|
307
|
+
and not self._om.is_feature_enabled(f)
|
|
308
|
+
],
|
|
309
|
+
stages_skipped=[], # ML stages are never skipped
|
|
310
|
+
)
|
|
311
|
+
self._history.append(record)
|
|
312
|
+
if len(self._history) > self._max_history:
|
|
313
|
+
self._history.pop(0)
|
|
314
|
+
|
|
315
|
+
# Update overhead manager
|
|
316
|
+
self._om.update_overhead(overhead_pct)
|
|
317
|
+
|
|
318
|
+
# Adapt throughput level (never disables ML stages)
|
|
319
|
+
self._adapt_throughput(overhead_pct)
|
|
320
|
+
|
|
321
|
+
return record
|
|
322
|
+
|
|
323
|
+
# -- Adaptive throughput scheduling ------------------------------------
|
|
324
|
+
|
|
325
|
+
def _adapt_throughput(self, overhead_pct: float) -> None:
|
|
326
|
+
"""Adjust throughput level based on overhead trend.
|
|
327
|
+
|
|
328
|
+
Unlike stage disabling, this NEVER cuts ML intelligence features.
|
|
329
|
+
Instead it reduces fact counts and increases batch sizes to keep
|
|
330
|
+
overhead under cap while maintaining the full extraction pipeline.
|
|
331
|
+
"""
|
|
332
|
+
if self._consecutive_over >= self._CONSECUTIVE_OVER_LIMIT:
|
|
333
|
+
# Sustained over-cap: constrain throughput
|
|
334
|
+
if self._throughput_level != THROUGHPUT_CONSTRAINED:
|
|
335
|
+
self._throughput_level = THROUGHPUT_CONSTRAINED
|
|
336
|
+
logger.info(
|
|
337
|
+
"Adaptive: throughput → constrained "
|
|
338
|
+
"(consecutive over cap: %d, ewma=%.1f%%)",
|
|
339
|
+
self._consecutive_over, self._ewma_overhead_pct,
|
|
340
|
+
)
|
|
341
|
+
elif overhead_pct > self._cap:
|
|
342
|
+
# Single window over cap: throttle
|
|
343
|
+
if self._throughput_level == THROUGHPUT_NORMAL:
|
|
344
|
+
self._throughput_level = THROUGHPUT_THROTTLED
|
|
345
|
+
logger.info(
|
|
346
|
+
"Adaptive: throughput → throttled "
|
|
347
|
+
"(overhead=%.1f%% > cap=%.1f%%)",
|
|
348
|
+
overhead_pct, self._cap,
|
|
349
|
+
)
|
|
350
|
+
elif self._ewma_overhead_pct < self._cap * 0.7:
|
|
351
|
+
# Well under budget: restore throughput one step
|
|
352
|
+
if self._throughput_level == THROUGHPUT_CONSTRAINED:
|
|
353
|
+
self._throughput_level = THROUGHPUT_THROTTLED
|
|
354
|
+
logger.info(
|
|
355
|
+
"Adaptive: throughput → throttled (restoring, ewma=%.1f%%)",
|
|
356
|
+
self._ewma_overhead_pct,
|
|
357
|
+
)
|
|
358
|
+
elif self._throughput_level == THROUGHPUT_THROTTLED:
|
|
359
|
+
self._throughput_level = THROUGHPUT_NORMAL
|
|
360
|
+
logger.info(
|
|
361
|
+
"Adaptive: throughput → normal (restored, ewma=%.1f%%)",
|
|
362
|
+
self._ewma_overhead_pct,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# -- Profile generation ------------------------------------------------
|
|
366
|
+
|
|
367
|
+
def extraction_profile(self) -> ExtractionProfile:
|
|
368
|
+
"""Get recommended extraction profile based on current state.
|
|
369
|
+
|
|
370
|
+
ML stages (3=GLiNER, 4=UIE, 5=Discourse) are ALWAYS enabled.
|
|
371
|
+
Under pressure, throughput is throttled via max_facts_per_stage.
|
|
372
|
+
"""
|
|
373
|
+
with self._lock:
|
|
374
|
+
pressure = self._rm.snapshot().pressure_level
|
|
375
|
+
|
|
376
|
+
# ML stages are ALWAYS enabled — they are core intelligence
|
|
377
|
+
profile = ExtractionProfile(
|
|
378
|
+
enable_stage_3=True, # GLiNER NER — always on
|
|
379
|
+
enable_stage_4=True, # UIE relation extraction — always on
|
|
380
|
+
enable_stage_5=True, # Discourse analysis — always on
|
|
381
|
+
enable_stage_6=False, # LLM-assisted — off by default
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Throttle throughput based on pressure + throughput level
|
|
385
|
+
if pressure in ("high", "critical"):
|
|
386
|
+
profile.max_facts_per_stage = _THROUGHPUT_FACTS[THROUGHPUT_CONSTRAINED]
|
|
387
|
+
elif pressure == "medium":
|
|
388
|
+
profile.max_facts_per_stage = _THROUGHPUT_FACTS[THROUGHPUT_THROTTLED]
|
|
389
|
+
else:
|
|
390
|
+
profile.max_facts_per_stage = _THROUGHPUT_FACTS[self._throughput_level]
|
|
391
|
+
|
|
392
|
+
return profile
|
|
393
|
+
|
|
394
|
+
def envelope_profile(self) -> EnvelopeProfile:
|
|
395
|
+
"""Get recommended envelope profile based on current state.
|
|
396
|
+
|
|
397
|
+
CKF (Contextual Knowledge Fusion) is ALWAYS enabled — it is
|
|
398
|
+
core to CRP's knowledge retrieval. Under pressure, batch sizes
|
|
399
|
+
increase and packing limits decrease to reduce overhead.
|
|
400
|
+
"""
|
|
401
|
+
with self._lock:
|
|
402
|
+
pressure = self._rm.snapshot().pressure_level
|
|
403
|
+
|
|
404
|
+
profile = EnvelopeProfile(
|
|
405
|
+
enable_cross_encoder=self._om.is_feature_enabled("cross_encoder"),
|
|
406
|
+
enable_ckf=True, # CKF is always enabled — core intelligence
|
|
407
|
+
max_packing_facts=_THROUGHPUT_PACKING[self._throughput_level],
|
|
408
|
+
embedding_batch_size=_THROUGHPUT_BATCH[self._throughput_level],
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Pressure overrides: tighter limits but CKF stays on
|
|
412
|
+
if pressure in ("high", "critical"):
|
|
413
|
+
profile.max_packing_facts = _THROUGHPUT_PACKING[THROUGHPUT_CONSTRAINED]
|
|
414
|
+
profile.embedding_batch_size = _THROUGHPUT_BATCH[THROUGHPUT_CONSTRAINED]
|
|
415
|
+
elif pressure == "medium":
|
|
416
|
+
profile.max_packing_facts = _THROUGHPUT_PACKING[THROUGHPUT_THROTTLED]
|
|
417
|
+
profile.embedding_batch_size = _THROUGHPUT_BATCH[THROUGHPUT_THROTTLED]
|
|
418
|
+
|
|
419
|
+
# If cross_encoder was shed by OverheadBudgetManager, respect that
|
|
420
|
+
if not self._om.is_feature_enabled("cross_encoder"):
|
|
421
|
+
profile.enable_cross_encoder = False
|
|
422
|
+
|
|
423
|
+
return profile
|
|
424
|
+
|
|
425
|
+
# -- Model lifecycle decisions -----------------------------------------
|
|
426
|
+
|
|
427
|
+
def should_unload_models(self) -> bool:
|
|
428
|
+
"""Check if idle models should be unloaded."""
|
|
429
|
+
pressure = self._rm.snapshot().pressure_level
|
|
430
|
+
if pressure in ("high", "critical"):
|
|
431
|
+
return True
|
|
432
|
+
idle = self._rm.get_idle_models(self._idle_timeout)
|
|
433
|
+
return len(idle) > 0
|
|
434
|
+
|
|
435
|
+
def idle_models(self) -> list[str]:
|
|
436
|
+
"""Get list of models eligible for unloading."""
|
|
437
|
+
pressure = self._rm.snapshot().pressure_level
|
|
438
|
+
if pressure == "critical":
|
|
439
|
+
# Under critical pressure, use shorter timeout
|
|
440
|
+
return self._rm.get_idle_models(self._idle_timeout / 3)
|
|
441
|
+
return self._rm.get_idle_models(self._idle_timeout)
|
|
442
|
+
|
|
443
|
+
def should_run_gc(self) -> bool:
|
|
444
|
+
"""Check if garbage collection should run."""
|
|
445
|
+
pressure = self._rm.snapshot().pressure_level
|
|
446
|
+
return pressure in ("medium", "high", "critical")
|
|
447
|
+
|
|
448
|
+
# -- Overhead queries --------------------------------------------------
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
def ewma_overhead_pct(self) -> float:
|
|
452
|
+
return self._ewma_overhead_pct
|
|
453
|
+
|
|
454
|
+
@property
|
|
455
|
+
def window_count(self) -> int:
|
|
456
|
+
return self._window_count
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def consecutive_over_cap(self) -> int:
|
|
460
|
+
return self._consecutive_over
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def disabled_stages(self) -> set[int]:
|
|
464
|
+
"""ML stages are never disabled — always returns empty set."""
|
|
465
|
+
return set()
|
|
466
|
+
|
|
467
|
+
@property
|
|
468
|
+
def throughput_level(self) -> str:
|
|
469
|
+
"""Current throughput level: normal, throttled, or constrained."""
|
|
470
|
+
return self._throughput_level
|
|
471
|
+
|
|
472
|
+
@property
|
|
473
|
+
def hardware(self) -> dict[str, int | float]:
|
|
474
|
+
return dict(self._hardware)
|
|
475
|
+
|
|
476
|
+
def overhead_trend(self, last_n: int = 5) -> list[float]:
|
|
477
|
+
"""Return recent overhead percentages."""
|
|
478
|
+
with self._lock:
|
|
479
|
+
return [r.overhead_pct for r in self._history[-last_n:]]
|
|
480
|
+
|
|
481
|
+
# -- Prompt efficiency -------------------------------------------------
|
|
482
|
+
|
|
483
|
+
def prompt_efficiency(self) -> PromptEfficiency:
|
|
484
|
+
"""Get LLM prompt efficiency recommendations.
|
|
485
|
+
|
|
486
|
+
After 2+ windows the system prompt is stable and should be cached.
|
|
487
|
+
Under constrained throughput, envelope compression is recommended.
|
|
488
|
+
"""
|
|
489
|
+
with self._lock:
|
|
490
|
+
cache_enabled = self._window_count >= 2
|
|
491
|
+
compress = self._throughput_level == THROUGHPUT_CONSTRAINED
|
|
492
|
+
cache_pct = min(90.0, self._window_count * 15.0) if cache_enabled else 0.0
|
|
493
|
+
|
|
494
|
+
return PromptEfficiency(
|
|
495
|
+
deduplicate_facts=True,
|
|
496
|
+
cache_system_prompt=cache_enabled,
|
|
497
|
+
compress_envelope=compress,
|
|
498
|
+
reuse_connection=True,
|
|
499
|
+
estimated_cache_hit_pct=round(cache_pct, 1),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# -- Summary -----------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
def summary(self) -> dict:
|
|
505
|
+
"""Full allocator state for diagnostics."""
|
|
506
|
+
with self._lock:
|
|
507
|
+
return {
|
|
508
|
+
"overhead_cap_pct": self._cap,
|
|
509
|
+
"ewma_overhead_pct": round(self._ewma_overhead_pct, 1),
|
|
510
|
+
"window_count": self._window_count,
|
|
511
|
+
"consecutive_over_cap": self._consecutive_over,
|
|
512
|
+
"throughput_level": self._throughput_level,
|
|
513
|
+
"disabled_stages": [], # ML stages are never disabled
|
|
514
|
+
"hardware": self._hardware,
|
|
515
|
+
"feature_shedding": self._om.enabled_features,
|
|
516
|
+
"resource_pressure": self._rm.snapshot().pressure_level,
|
|
517
|
+
"overhead_trend": [r.overhead_pct for r in self._history[-10:]],
|
|
518
|
+
"prompt_efficiency": {
|
|
519
|
+
"cache_system_prompt": self._window_count >= 2,
|
|
520
|
+
"estimated_cache_hit_pct": round(
|
|
521
|
+
min(90.0, self._window_count * 15.0)
|
|
522
|
+
if self._window_count >= 2 else 0.0, 1,
|
|
523
|
+
),
|
|
524
|
+
},
|
|
525
|
+
}
|