pal-code 0.1.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.
- pal/__init__.py +29 -0
- pal/__main__.py +3 -0
- pal/access_manager/__init__.py +16 -0
- pal/access_manager/components/__init__.py +14 -0
- pal/access_manager/components/access_control.py +41 -0
- pal/access_manager/components/base.py +39 -0
- pal/access_manager/components/intervention.py +31 -0
- pal/access_manager/components/privilege_registry.py +30 -0
- pal/access_manager/pipeline.py +68 -0
- pal/access_manager/utils/__init__.py +2 -0
- pal/agent_constraints/__init__.py +3 -0
- pal/agent_constraints/components/AGENT_OUTPUT.md +13 -0
- pal/agent_constraints/components/__init__.py +3 -0
- pal/agent_constraints/components/constraints.py +22 -0
- pal/agent_constraints/pipeline.py +22 -0
- pal/agent_constraints/utils/__init__.py +2 -0
- pal/agents/__init__.py +12 -0
- pal/agents/coding.py +14 -0
- pal/agents/docs.py +15 -0
- pal/chat/__init__.py +4 -0
- pal/chat/demo.py +231 -0
- pal/chat/providers.py +23 -0
- pal/chat/service.py +591 -0
- pal/chat/services/__init__.py +26 -0
- pal/chat/services/base.py +142 -0
- pal/chat/services/data.py +142 -0
- pal/chat/services/media.py +123 -0
- pal/chat/services/web.py +60 -0
- pal/chat/sessions.py +108 -0
- pal/chat/tools.py +446 -0
- pal/chat/uploads.py +81 -0
- pal/cli.py +502 -0
- pal/compression/__init__.py +12 -0
- pal/compression/components/__init__.py +64 -0
- pal/compression/components/adverbs.py +20 -0
- pal/compression/components/hedges.py +54 -0
- pal/compression/components/interjections.py +20 -0
- pal/compression/components/minifier.py +56 -0
- pal/compression/components/output_compressor.py +13 -0
- pal/compression/components/pleonasms.py +54 -0
- pal/compression/components/punctuations.py +36 -0
- pal/compression/components/stopwords.py +62 -0
- pal/compression/history.py +346 -0
- pal/compression/pipeline.py +58 -0
- pal/compression/utils/__init__.py +30 -0
- pal/compression/utils/negations.py +18 -0
- pal/compression/utils/nltk_setup.py +20 -0
- pal/compression/utils/pronouns.py +27 -0
- pal/compression/utils/proselint_setup.py +9 -0
- pal/compression/utils/spacy_setup.py +22 -0
- pal/config/default.yaml +163 -0
- pal/config.py +504 -0
- pal/context_manager/__init__.py +7 -0
- pal/context_manager/chat_context.py +117 -0
- pal/context_manager/components/__init__.py +5 -0
- pal/context_manager/components/assembler.py +75 -0
- pal/context_manager/components/context_cache.py +45 -0
- pal/context_manager/components/kv_bridge.py +16 -0
- pal/context_manager/components/pager.py +48 -0
- pal/context_manager/components/simple_context_manager.py +56 -0
- pal/context_manager/components/turns.py +88 -0
- pal/context_manager/pipeline.py +27 -0
- pal/context_manager/utils/__init__.py +18 -0
- pal/context_manager/utils/hashing.py +5 -0
- pal/context_manager/utils/relevance.py +59 -0
- pal/context_manager/utils/storage.py +37 -0
- pal/documents/__init__.py +4 -0
- pal/documents/components/__init__.py +0 -0
- pal/documents/components/chunker.py +79 -0
- pal/documents/components/index.py +155 -0
- pal/documents/utils/__init__.py +1 -0
- pal/errors.py +40 -0
- pal/input_converters/__init__.py +8 -0
- pal/input_converters/components/__init__.py +0 -0
- pal/input_converters/components/base.py +38 -0
- pal/input_converters/components/pandoc_converter.py +87 -0
- pal/input_converters/components/pdf_converter.py +46 -0
- pal/input_converters/components/pptx_converter.py +52 -0
- pal/input_converters/components/xlsx_converter.py +38 -0
- pal/input_converters/pipeline.py +72 -0
- pal/input_converters/utils/__init__.py +1 -0
- pal/input_converters/workspace.py +56 -0
- pal/input_savers/INPUT_RULES.md +2 -0
- pal/input_savers/__init__.py +5 -0
- pal/input_savers/components/__init__.py +0 -0
- pal/input_savers/components/base.py +40 -0
- pal/input_savers/components/methods.py +41 -0
- pal/input_savers/components/verifier.py +39 -0
- pal/input_savers/pipeline.py +154 -0
- pal/input_savers/utils/__init__.py +0 -0
- pal/input_savers/utils/guards.py +94 -0
- pal/knowledge_graph/__init__.py +10 -0
- pal/knowledge_graph/components/__init__.py +0 -0
- pal/knowledge_graph/components/base.py +50 -0
- pal/knowledge_graph/components/builder.py +54 -0
- pal/knowledge_graph/components/retriever.py +100 -0
- pal/knowledge_graph/components/workspace.py +116 -0
- pal/knowledge_graph/pipeline.py +215 -0
- pal/knowledge_graph/utils/__init__.py +1 -0
- pal/latency_tracker/__init__.py +3 -0
- pal/latency_tracker/pipeline.py +86 -0
- pal/llm_scheduler/__init__.py +11 -0
- pal/llm_scheduler/api.py +137 -0
- pal/llm_scheduler/code.py +28 -0
- pal/llm_scheduler/core.py +92 -0
- pal/llm_scheduler/local.py +215 -0
- pal/llm_scheduler/pipeline.py +42 -0
- pal/llm_scheduler/types.py +92 -0
- pal/llm_wrapper/__init__.py +34 -0
- pal/llm_wrapper/adapter.py +316 -0
- pal/llm_wrapper/components/__init__.py +23 -0
- pal/llm_wrapper/components/adapter_types.py +90 -0
- pal/llm_wrapper/components/base.py +195 -0
- pal/llm_wrapper/components/features.py +104 -0
- pal/llm_wrapper/components/litellm_wrapper.py +215 -0
- pal/llm_wrapper/components/openrouter_wrapper.py +114 -0
- pal/llm_wrapper/modes.py +73 -0
- pal/llm_wrapper/pipeline.py +61 -0
- pal/llm_wrapper/utils/__init__.py +4 -0
- pal/llm_wrapper/utils/registry.py +49 -0
- pal/llm_wrapper/utils/token_counting.py +10 -0
- pal/local_llm/__init__.py +5 -0
- pal/local_llm/components/__init__.py +0 -0
- pal/local_llm/components/huggingface_local.py +217 -0
- pal/local_llm/components/local_llm.py +60 -0
- pal/logging_setup.py +18 -0
- pal/memory/__init__.py +7 -0
- pal/memory/components/__init__.py +6 -0
- pal/memory/components/lru_k.py +56 -0
- pal/memory/components/manager.py +98 -0
- pal/memory/components/note.py +54 -0
- pal/memory/components/storage_manager.py +60 -0
- pal/memory/pipeline.py +122 -0
- pal/memory/utils/__init__.py +3 -0
- pal/memory/utils/keyword_overlap.py +3 -0
- pal/memory_tracker/__init__.py +3 -0
- pal/memory_tracker/components/__init__.py +0 -0
- pal/memory_tracker/components/process.py +56 -0
- pal/memory_tracker/pipeline.py +44 -0
- pal/metrics/__init__.py +4 -0
- pal/metrics/components/__init__.py +4 -0
- pal/metrics/components/collector.py +140 -0
- pal/metrics/components/exporter.py +28 -0
- pal/metrics/pipeline.py +49 -0
- pal/metrics/utils/__init__.py +2 -0
- pal/notes_manager/__init__.py +5 -0
- pal/notes_manager/components/__init__.py +0 -0
- pal/notes_manager/components/privacy.py +14 -0
- pal/notes_manager/components/recall.py +31 -0
- pal/notes_manager/components/tidy.py +28 -0
- pal/notes_manager/pipeline.py +138 -0
- pal/orchestrator/__init__.py +26 -0
- pal/orchestrator/components/__init__.py +26 -0
- pal/orchestrator/components/agent.py +105 -0
- pal/orchestrator/components/cgroup_manager.py +209 -0
- pal/orchestrator/components/context_interrupt.py +220 -0
- pal/orchestrator/components/limits.py +9 -0
- pal/orchestrator/components/scheduler.py +69 -0
- pal/orchestrator/components/syscall_scheduler.py +200 -0
- pal/orchestrator/pipeline.py +228 -0
- pal/orchestrator/utils/__init__.py +4 -0
- pal/orchestrator/utils/device.py +164 -0
- pal/orchestrator/utils/protocol.py +32 -0
- pal/os_kernel/__init__.py +5 -0
- pal/os_kernel/filesystem.py +80 -0
- pal/os_kernel/network.py +146 -0
- pal/os_kernel/pipeline.py +175 -0
- pal/os_kernel/sysinfo.py +36 -0
- pal/paths.py +59 -0
- pal/pipeline.py +177 -0
- pal/py.typed +0 -0
- pal/redact.py +19 -0
- pal/sdk/__init__.py +33 -0
- pal/sdk/adapters.py +43 -0
- pal/sdk/agent.py +99 -0
- pal/sdk/components/__init__.py +14 -0
- pal/sdk/components/autogen_adapter.py +65 -0
- pal/sdk/components/autogen_client.py +98 -0
- pal/sdk/components/framework_adapter_base.py +69 -0
- pal/sdk/components/metagpt_adapter.py +46 -0
- pal/sdk/components/open_interpreter_adapter.py +47 -0
- pal/sdk/components/tool_catalog.py +44 -0
- pal/sdk/os_api.py +84 -0
- pal/sdk/patterns.py +129 -0
- pal/sdk/pipeline.py +240 -0
- pal/sdk/queries.py +68 -0
- pal/sdk/savers.py +88 -0
- pal/sdk/utils/__init__.py +2 -0
- pal/storage_manager/__init__.py +17 -0
- pal/storage_manager/components/__init__.py +19 -0
- pal/storage_manager/components/base.py +50 -0
- pal/storage_manager/components/local_file_backend.py +84 -0
- pal/storage_manager/components/storage_queue.py +91 -0
- pal/storage_manager/components/vector_store_backend.py +122 -0
- pal/storage_manager/pipeline.py +153 -0
- pal/storage_manager/utils/__init__.py +4 -0
- pal/storage_manager/utils/memory_adapter.py +60 -0
- pal/storage_manager/utils/sdk.py +93 -0
- pal/studio/__init__.py +0 -0
- pal/studio/builder.py +166 -0
- pal/studio/catalog.py +90 -0
- pal/studio/checks.py +129 -0
- pal/studio/clients.py +6 -0
- pal/studio/editor.py +137 -0
- pal/studio/files.py +6 -0
- pal/studio/models.py +6 -0
- pal/studio/plan.py +149 -0
- pal/studio/planner.py +205 -0
- pal/studio/policy.py +69 -0
- pal/studio/routes.py +254 -0
- pal/studio/runs.py +210 -0
- pal/task_router/__init__.py +11 -0
- pal/task_router/components/__init__.py +0 -0
- pal/task_router/components/answerer.py +37 -0
- pal/task_router/components/base.py +34 -0
- pal/task_router/components/classifier.py +40 -0
- pal/task_router/components/gates.py +23 -0
- pal/task_router/pipeline.py +61 -0
- pal/task_router/utils/__init__.py +1 -0
- pal/terminal/__init__.py +1 -0
- pal/terminal/backend.py +141 -0
- pal/terminal/bench.py +173 -0
- pal/terminal/cli.py +826 -0
- pal/terminal/clients.py +170 -0
- pal/terminal/commands.py +401 -0
- pal/terminal/decide.py +242 -0
- pal/terminal/files.py +55 -0
- pal/terminal/flow_cli.py +439 -0
- pal/terminal/hardware.py +199 -0
- pal/terminal/history.py +88 -0
- pal/terminal/keys.py +83 -0
- pal/terminal/local_models.py +261 -0
- pal/terminal/logo.py +117 -0
- pal/terminal/models.py +99 -0
- pal/terminal/policy.py +54 -0
- pal/terminal/project.py +413 -0
- pal/terminal/review.py +65 -0
- pal/terminal/security.py +239 -0
- pal/terminal/session.py +503 -0
- pal/terminal/setup.py +354 -0
- pal/terminal/syntax.py +98 -0
- pal/terminal/theme.py +37 -0
- pal/terminal/ui.py +647 -0
- pal/terminal/voice.py +137 -0
- pal/terminal/workflow.py +624 -0
- pal/token_tracker/__init__.py +3 -0
- pal/token_tracker/pipeline.py +54 -0
- pal/tokens.py +21 -0
- pal/tool_catalog.py +107 -0
- pal/tool_manager/__init__.py +17 -0
- pal/tool_manager/builtin.py +82 -0
- pal/tool_manager/components/__init__.py +19 -0
- pal/tool_manager/components/base.py +53 -0
- pal/tool_manager/components/conflict_resolver.py +27 -0
- pal/tool_manager/components/param_validator.py +41 -0
- pal/tool_manager/components/tool_call_queue.py +111 -0
- pal/tool_manager/components/tool_registry.py +60 -0
- pal/tool_manager/pipeline.py +80 -0
- pal/tool_manager/utils/__init__.py +3 -0
- pal/web/__init__.py +3 -0
- pal/web/app.py +276 -0
- pal/web/static/editor.js +219 -0
- pal/web/static/fonts/LICENSE-Inter.txt +3 -0
- pal/web/static/fonts/inter-400.woff2 +0 -0
- pal/web/static/fonts/inter-500.woff2 +0 -0
- pal/web/static/fonts/inter-600.woff2 +0 -0
- pal/web/static/fonts/inter-700.woff2 +0 -0
- pal/web/static/inter.css +5 -0
- pal/web/static/pal-logo-256.png +0 -0
- pal/web/static/pal-logo-32.png +0 -0
- pal/web/static/pal-logo-64.png +0 -0
- pal/web/static/studio.css +306 -0
- pal/web/static/studio.js +533 -0
- pal/web/static/voice.js +76 -0
- pal/web/templates/index.html +368 -0
- pal/web/templates/studio.html +120 -0
- pal_code-0.1.0.dist-info/METADATA +379 -0
- pal_code-0.1.0.dist-info/RECORD +282 -0
- pal_code-0.1.0.dist-info/WHEEL +5 -0
- pal_code-0.1.0.dist-info/entry_points.txt +2 -0
- pal_code-0.1.0.dist-info/licenses/LICENSE +21 -0
- pal_code-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context interrupt mechanism for Round Robin scheduling (AIOS section
|
|
3
|
+
3.3 / detailed in their Appendix A.3, which this module is the
|
|
4
|
+
counterpart of here).
|
|
5
|
+
|
|
6
|
+
Long-running LLM inference is exactly the kind of system call that can
|
|
7
|
+
monopolize a shared resource -- one slow generation blocks everything
|
|
8
|
+
queued behind it unless something can pause it. FIFO scheduling doesn't
|
|
9
|
+
need that (it runs each request to completion by design); Round Robin
|
|
10
|
+
does, since giving every request a bounded time slice only works if a
|
|
11
|
+
request that isn't done yet can be suspended and picked back up later
|
|
12
|
+
without losing the work already done.
|
|
13
|
+
|
|
14
|
+
Two snapshot/restore approaches, matching what's actually available
|
|
15
|
+
per core:
|
|
16
|
+
|
|
17
|
+
- **Text-based** (`_run_slice_text` / `ContextSnapshot(mode="text")`):
|
|
18
|
+
for cores with no logits access -- every cloud API and every local
|
|
19
|
+
server this project talks to (Anthropic, Gemini, OpenAI-compatible,
|
|
20
|
+
Ollama). None of them return raw per-step logits over HTTP, so the
|
|
21
|
+
only state available to snapshot is the decoded text generated so
|
|
22
|
+
far. Resumption re-injects that text as a partial assistant turn plus
|
|
23
|
+
a short "continue exactly where you left off" instruction, and lets
|
|
24
|
+
the model carry on from there.
|
|
25
|
+
|
|
26
|
+
- **Logits-based** (`_run_slice_logits` / `ContextSnapshot(mode="logits")`):
|
|
27
|
+
for cores with direct logits access -- currently only
|
|
28
|
+
HuggingFaceLocalLLM, the in-process local model. Generation is
|
|
29
|
+
driven one token at a time via manual beam-search stepping
|
|
30
|
+
(HuggingFaceLocalLLM.beam_search_step), and the snapshot is the
|
|
31
|
+
actual search-tree state (every beam's token sequence + cumulative
|
|
32
|
+
score, see BeamSearchState in huggingface_local.py) rather than
|
|
33
|
+
just its decoded text. Resuming continues the search from that exact
|
|
34
|
+
state -- no re-tokenization, no risk of the model repeating or
|
|
35
|
+
drifting from where it left off the way a text-based "please
|
|
36
|
+
continue" prompt can.
|
|
37
|
+
|
|
38
|
+
Both are request-level approximations of "pause an in-flight
|
|
39
|
+
computation" -- neither literally suspends a running matrix
|
|
40
|
+
multiplication. What differs between them is how much of the model's
|
|
41
|
+
own state survives the pause: text-based keeps only what came out the
|
|
42
|
+
other end; logits-based keeps the search state that produced it.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import time
|
|
47
|
+
from dataclasses import dataclass
|
|
48
|
+
from typing import Any, Optional
|
|
49
|
+
|
|
50
|
+
from pal.llm_wrapper.components.base import LLMSyscall, LLMWrapper
|
|
51
|
+
|
|
52
|
+
_CONTINUE_INSTRUCTION = (
|
|
53
|
+
"[Continue your previous reply exactly where it left off. Do not "
|
|
54
|
+
"repeat any earlier part of it, do not restart, and do not "
|
|
55
|
+
"acknowledge this instruction.]"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ContextSnapshot:
|
|
61
|
+
"""The suspended state of one interrupted generation.
|
|
62
|
+
|
|
63
|
+
mode="text": text_so_far is the only state -- decoding must
|
|
64
|
+
restart from a continuation prompt built around it.
|
|
65
|
+
mode="logits": beam_state carries the actual search-tree state
|
|
66
|
+
(an opaque, core-specific object -- BeamSearchState for
|
|
67
|
+
HuggingFaceLocalLLM) that resumption steps forward directly,
|
|
68
|
+
with text_so_far kept alongside purely for human-readable
|
|
69
|
+
inspection/logging, not used to drive resumption.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
mode: str # "text" | "logits"
|
|
73
|
+
text_so_far: str = ""
|
|
74
|
+
beam_state: Any = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class InterruptResult:
|
|
79
|
+
complete: bool
|
|
80
|
+
text_so_far: str
|
|
81
|
+
elapsed: float
|
|
82
|
+
snapshot: ContextSnapshot
|
|
83
|
+
# True when text_so_far is the FULL cumulative text generated across
|
|
84
|
+
# every slice so far (logits mode -- beam decoding naturally tracks
|
|
85
|
+
# whole sequences). False when it's only this slice's delta, to be
|
|
86
|
+
# appended to whatever came before (text mode -- each streamed
|
|
87
|
+
# continuation only yields its own new chunk).
|
|
88
|
+
cumulative: bool = False
|
|
89
|
+
chunks_yielded: int = 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ContextInterruptController:
|
|
93
|
+
"""Runs one LLMSyscall against one core for at most `quantum`
|
|
94
|
+
seconds, choosing the text-based or logits-based approach based on
|
|
95
|
+
the core's capabilities(), and returns either a complete result or
|
|
96
|
+
a ContextSnapshot the scheduler can use to resume later."""
|
|
97
|
+
|
|
98
|
+
def run_slice(
|
|
99
|
+
self,
|
|
100
|
+
core: LLMWrapper,
|
|
101
|
+
syscall: LLMSyscall,
|
|
102
|
+
quantum: float,
|
|
103
|
+
resume_snapshot: Optional[ContextSnapshot] = None,
|
|
104
|
+
) -> InterruptResult:
|
|
105
|
+
use_logits = (resume_snapshot is not None and resume_snapshot.mode == "logits") or (
|
|
106
|
+
resume_snapshot is None
|
|
107
|
+
and core.capabilities().supports_logits_snapshot
|
|
108
|
+
and hasattr(core, "init_beam_search")
|
|
109
|
+
and syscall.params.get("use_logits_snapshot", True)
|
|
110
|
+
)
|
|
111
|
+
if use_logits:
|
|
112
|
+
return self._run_slice_logits(core, syscall, quantum, resume_snapshot)
|
|
113
|
+
return self._run_slice_text(core, syscall, quantum)
|
|
114
|
+
|
|
115
|
+
# -- text-based: closed-source / no-logits-access cores --------------------
|
|
116
|
+
|
|
117
|
+
def _run_slice_text(self, core: LLMWrapper, syscall: LLMSyscall, quantum: float) -> InterruptResult:
|
|
118
|
+
text_parts: list[str] = []
|
|
119
|
+
t0 = time.time()
|
|
120
|
+
chunks = 0
|
|
121
|
+
|
|
122
|
+
for chunk in core.stream_chat(syscall):
|
|
123
|
+
text_parts.append(chunk)
|
|
124
|
+
chunks += 1
|
|
125
|
+
elapsed = time.time() - t0
|
|
126
|
+
if elapsed >= quantum:
|
|
127
|
+
# Time slice exhausted mid-generation -- stop pulling
|
|
128
|
+
# from the stream here. Cores without real incremental
|
|
129
|
+
# streaming (LLMWrapper.stream_chat's default fallback,
|
|
130
|
+
# which yields the whole response as one chunk) will
|
|
131
|
+
# already have finished by the time the first chunk
|
|
132
|
+
# arrives, so this branch only actually interrupts
|
|
133
|
+
# something for cores with true token-level streaming.
|
|
134
|
+
text_so_far = "".join(text_parts)
|
|
135
|
+
return InterruptResult(
|
|
136
|
+
complete=False,
|
|
137
|
+
text_so_far=text_so_far,
|
|
138
|
+
elapsed=elapsed,
|
|
139
|
+
snapshot=ContextSnapshot(mode="text", text_so_far=text_so_far),
|
|
140
|
+
cumulative=False,
|
|
141
|
+
chunks_yielded=chunks,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
text_so_far = "".join(text_parts)
|
|
145
|
+
return InterruptResult(
|
|
146
|
+
complete=True,
|
|
147
|
+
text_so_far=text_so_far,
|
|
148
|
+
elapsed=time.time() - t0,
|
|
149
|
+
snapshot=ContextSnapshot(mode="text", text_so_far=text_so_far),
|
|
150
|
+
cumulative=False,
|
|
151
|
+
chunks_yielded=chunks,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def build_continuation(self, syscall: LLMSyscall, partial: InterruptResult) -> LLMSyscall:
|
|
155
|
+
"""Text-based resumption: folds the partial output back into the
|
|
156
|
+
message list as prior context and returns a new LLMSyscall ready
|
|
157
|
+
to be re-queued. Only meaningful for mode="text" results -- the
|
|
158
|
+
scheduler doesn't call this for logits-mode interrupts, since
|
|
159
|
+
those resume from `beam_state` directly instead of rebuilding
|
|
160
|
+
the prompt."""
|
|
161
|
+
continued_messages = list(syscall.messages) + [
|
|
162
|
+
{"role": "assistant", "content": partial.text_so_far},
|
|
163
|
+
{"role": "user", "content": _CONTINUE_INSTRUCTION},
|
|
164
|
+
]
|
|
165
|
+
return LLMSyscall(
|
|
166
|
+
messages=continued_messages,
|
|
167
|
+
model=syscall.model,
|
|
168
|
+
temperature=syscall.temperature,
|
|
169
|
+
max_tokens=syscall.max_tokens,
|
|
170
|
+
stream=syscall.stream,
|
|
171
|
+
params=syscall.params,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# -- logits-based: direct logits access, search-tree state -----------------
|
|
175
|
+
|
|
176
|
+
def _run_slice_logits(
|
|
177
|
+
self,
|
|
178
|
+
core: LLMWrapper,
|
|
179
|
+
syscall: LLMSyscall,
|
|
180
|
+
quantum: float,
|
|
181
|
+
resume_snapshot: Optional[ContextSnapshot],
|
|
182
|
+
) -> InterruptResult:
|
|
183
|
+
t0 = time.time()
|
|
184
|
+
beam_width = syscall.params.get("beam_width", 1)
|
|
185
|
+
max_new_tokens = syscall.max_tokens or 512
|
|
186
|
+
|
|
187
|
+
state = resume_snapshot.beam_state if resume_snapshot is not None else core.init_beam_search(
|
|
188
|
+
syscall, beam_width=beam_width
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
steps_this_slice = 0
|
|
192
|
+
while not state.finished:
|
|
193
|
+
state = core.beam_search_step(state, max_new_tokens=max_new_tokens)
|
|
194
|
+
steps_this_slice += 1
|
|
195
|
+
elapsed = time.time() - t0
|
|
196
|
+
|
|
197
|
+
if elapsed >= quantum and not state.finished:
|
|
198
|
+
# Suspended mid-generation -- snapshot the intermediate
|
|
199
|
+
# search-tree state (not just its decoded text) so
|
|
200
|
+
# resumption continues the beam search exactly where
|
|
201
|
+
# this step left off.
|
|
202
|
+
decoded = core.decode_beam_search(state)
|
|
203
|
+
return InterruptResult(
|
|
204
|
+
complete=False,
|
|
205
|
+
text_so_far=decoded,
|
|
206
|
+
elapsed=elapsed,
|
|
207
|
+
snapshot=ContextSnapshot(mode="logits", text_so_far=decoded, beam_state=state),
|
|
208
|
+
cumulative=True,
|
|
209
|
+
chunks_yielded=steps_this_slice,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
final_text = core.decode_beam_search(state)
|
|
213
|
+
return InterruptResult(
|
|
214
|
+
complete=True,
|
|
215
|
+
text_so_far=final_text,
|
|
216
|
+
elapsed=time.time() - t0,
|
|
217
|
+
snapshot=ContextSnapshot(mode="logits", text_so_far=final_text, beam_state=state),
|
|
218
|
+
cumulative=True,
|
|
219
|
+
chunks_yielded=steps_this_slice,
|
|
220
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pal.orchestrator.components.cgroup_manager import ResourceLimits
|
|
2
|
+
|
|
3
|
+
# Preset resource envelopes for common agent workload sizes. cpu.max
|
|
4
|
+
# format is "<quota> <period>" in microseconds -- e.g. "50000 100000" =
|
|
5
|
+
# 50% of one CPU core.
|
|
6
|
+
|
|
7
|
+
LIGHT = ResourceLimits(memory_max="256M", cpu_max="25000 100000", pids_max="20")
|
|
8
|
+
STANDARD = ResourceLimits(memory_max="512M", cpu_max="50000 100000", pids_max="50")
|
|
9
|
+
HEAVY = ResourceLimits(memory_max="2G", cpu_max="100000 100000", pids_max="100")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from pal.orchestrator.components.agent import Agent, AgentConfig
|
|
4
|
+
from pal.orchestrator.components.cgroup_manager import CgroupManager
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ScheduledAgent:
|
|
9
|
+
config: AgentConfig
|
|
10
|
+
priority: int = 0 # higher runs first when slots are contended
|
|
11
|
+
weight: int = 100 # relative cpu.weight if backend supports it
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Scheduler:
|
|
15
|
+
"""
|
|
16
|
+
Bounds how many agents run concurrently (max_concurrent), and
|
|
17
|
+
decides which queued agents get a slot next based on priority.
|
|
18
|
+
Doesn't touch cgroup internals directly -- that stays
|
|
19
|
+
CgroupManager's job. This is purely admission control: who gets to
|
|
20
|
+
start, and in what order.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, cgroup_manager: CgroupManager, max_concurrent: int = 3):
|
|
24
|
+
self.cgroup_manager = cgroup_manager
|
|
25
|
+
self.max_concurrent = max_concurrent
|
|
26
|
+
self.running: dict[str, Agent] = {}
|
|
27
|
+
self.queue: list[ScheduledAgent] = []
|
|
28
|
+
|
|
29
|
+
def submit(self, config: AgentConfig, priority: int = 0, weight: int = 100) -> None:
|
|
30
|
+
if config.name in self.running:
|
|
31
|
+
raise ValueError(f"agent {config.name} already running")
|
|
32
|
+
if any(s.config.name == config.name for s in self.queue):
|
|
33
|
+
raise ValueError(f"agent {config.name} already queued")
|
|
34
|
+
self.queue.append(ScheduledAgent(config=config, priority=priority, weight=weight))
|
|
35
|
+
self.queue.sort(key=lambda s: s.priority, reverse=True)
|
|
36
|
+
|
|
37
|
+
def tick(self) -> list[str]:
|
|
38
|
+
"""
|
|
39
|
+
Admits as many queued agents as available slots allow, in
|
|
40
|
+
priority order. Returns the names of agents newly started this
|
|
41
|
+
tick. Call this after submit(), and again after any stop() to
|
|
42
|
+
backfill freed slots.
|
|
43
|
+
"""
|
|
44
|
+
started = []
|
|
45
|
+
while self.queue and len(self.running) < self.max_concurrent:
|
|
46
|
+
scheduled = self.queue.pop(0)
|
|
47
|
+
agent = Agent(scheduled.config, self.cgroup_manager)
|
|
48
|
+
agent.start()
|
|
49
|
+
self.running[scheduled.config.name] = agent
|
|
50
|
+
started.append(scheduled.config.name)
|
|
51
|
+
return started
|
|
52
|
+
|
|
53
|
+
def stop(self, agent_name: str) -> None:
|
|
54
|
+
agent = self.running.pop(agent_name, None)
|
|
55
|
+
if agent is None:
|
|
56
|
+
raise ValueError(f"agent {agent_name} not running")
|
|
57
|
+
agent.stop()
|
|
58
|
+
|
|
59
|
+
def stop_all(self) -> None:
|
|
60
|
+
for name in list(self.running.keys()):
|
|
61
|
+
self.stop(name)
|
|
62
|
+
|
|
63
|
+
def status(self) -> dict:
|
|
64
|
+
return {
|
|
65
|
+
"running": list(self.running.keys()),
|
|
66
|
+
"queued": [s.config.name for s in self.queue],
|
|
67
|
+
"capacity": self.max_concurrent,
|
|
68
|
+
"slots_free": self.max_concurrent - len(self.running),
|
|
69
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized system-call scheduler (AIOS section 3.3).
|
|
3
|
+
|
|
4
|
+
All LLM inference requests -- from Pipeline.run(), from orchestrator
|
|
5
|
+
Agents, from anywhere else in the system -- get submitted here as
|
|
6
|
+
SyscallRequests instead of each caller hitting an LLMWrapper directly.
|
|
7
|
+
That's the actual point of centralizing: request management
|
|
8
|
+
(queueing, ordering, time-slicing) lives in exactly one place, so
|
|
9
|
+
every other module only has to focus on producing/consuming a
|
|
10
|
+
request/result pair, not on how or when its turn comes.
|
|
11
|
+
|
|
12
|
+
Two scheduling strategies, same as AIOS:
|
|
13
|
+
- FIFO: process queued requests in arrival order, straight through to
|
|
14
|
+
completion. Simple, but a long request can make everything behind it
|
|
15
|
+
wait its full duration.
|
|
16
|
+
- Round Robin: give each queued request a bounded time slice
|
|
17
|
+
(`quantum`), then move to the next one, cycling back to unfinished
|
|
18
|
+
requests later. This is what needs the context-interrupt mechanism in
|
|
19
|
+
context_interrupt.py -- pausing an LLM generation mid-stream and
|
|
20
|
+
resuming it later without restarting from scratch.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import time
|
|
25
|
+
import uuid
|
|
26
|
+
from collections import deque
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from enum import Enum
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from pal.llm_wrapper.components.base import LLMResponse, LLMSyscall, LLMWrapper
|
|
32
|
+
from pal.orchestrator.components.context_interrupt import ContextInterruptController, ContextSnapshot
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SchedulingStrategy(str, Enum):
|
|
36
|
+
FIFO = "fifo"
|
|
37
|
+
ROUND_ROBIN = "round_robin"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SyscallStatus(str, Enum):
|
|
41
|
+
QUEUED = "queued"
|
|
42
|
+
RUNNING = "running"
|
|
43
|
+
DONE = "done"
|
|
44
|
+
ERROR = "error"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SyscallRequest:
|
|
49
|
+
"""One queued LLM inference request. `core` is whichever LLMWrapper
|
|
50
|
+
the submitting module wants this run against -- the scheduler
|
|
51
|
+
doesn't care which deployment option it is, only when it runs."""
|
|
52
|
+
|
|
53
|
+
agent_name: str
|
|
54
|
+
core: LLMWrapper
|
|
55
|
+
syscall: LLMSyscall
|
|
56
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
57
|
+
submitted_at: float = field(default_factory=time.time)
|
|
58
|
+
priority: int = 0 # FIFO ignores this; reserved for future strategies
|
|
59
|
+
|
|
60
|
+
# internal scheduling state
|
|
61
|
+
status: SyscallStatus = SyscallStatus.QUEUED
|
|
62
|
+
accumulated_text: str = ""
|
|
63
|
+
slices_run: int = 0
|
|
64
|
+
result: Optional[LLMResponse] = None
|
|
65
|
+
error: Optional[str] = None
|
|
66
|
+
# Set when a Round Robin slice is interrupted, so the next tick
|
|
67
|
+
# knows to RESUME (text-based: via the continuation baked into
|
|
68
|
+
# `syscall` already; logits-based: via snapshot.beam_state) rather
|
|
69
|
+
# than starting a fresh generation.
|
|
70
|
+
snapshot: Optional[ContextSnapshot] = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SyscallScheduler:
|
|
74
|
+
"""
|
|
75
|
+
One centralized queue for every LLM syscall in the system,
|
|
76
|
+
regardless of which module submitted it. Callers submit() a request
|
|
77
|
+
and get back an id; results are collected via get_result() once
|
|
78
|
+
processing has advanced far enough (run_fifo() / run_round_robin()
|
|
79
|
+
drive that, they aren't automatic/background by default -- call
|
|
80
|
+
tick() from whatever event loop the rest of the system already
|
|
81
|
+
has, e.g. the orchestrator's own tick()).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, strategy: SchedulingStrategy = SchedulingStrategy.FIFO, rr_quantum: float = 2.0):
|
|
85
|
+
self.strategy = strategy
|
|
86
|
+
self.rr_quantum = rr_quantum
|
|
87
|
+
self.queue: deque[SyscallRequest] = deque()
|
|
88
|
+
self.requests: dict[str, SyscallRequest] = {}
|
|
89
|
+
self._interrupt_controller = ContextInterruptController()
|
|
90
|
+
|
|
91
|
+
def submit(self, agent_name: str, core: LLMWrapper, syscall: LLMSyscall, priority: int = 0) -> str:
|
|
92
|
+
req = SyscallRequest(agent_name=agent_name, core=core, syscall=syscall, priority=priority)
|
|
93
|
+
self.requests[req.id] = req
|
|
94
|
+
self.queue.append(req)
|
|
95
|
+
return req.id
|
|
96
|
+
|
|
97
|
+
def get_result(self, request_id: str) -> SyscallRequest:
|
|
98
|
+
return self.requests[request_id]
|
|
99
|
+
|
|
100
|
+
def pending_count(self) -> int:
|
|
101
|
+
return len(self.queue)
|
|
102
|
+
|
|
103
|
+
# -- scheduling strategies -------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def run_fifo_once(self) -> Optional[SyscallRequest]:
|
|
106
|
+
"""Pops the head of the queue and runs it straight through to
|
|
107
|
+
completion. No interruption -- a long request occupies the
|
|
108
|
+
core for its full duration before the next one gets to start,
|
|
109
|
+
by design."""
|
|
110
|
+
if not self.queue:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
req = self.queue.popleft()
|
|
114
|
+
req.status = SyscallStatus.RUNNING
|
|
115
|
+
try:
|
|
116
|
+
req.result = req.core.chat(req.syscall)
|
|
117
|
+
req.accumulated_text = req.result.text
|
|
118
|
+
req.status = SyscallStatus.DONE
|
|
119
|
+
except Exception as e:
|
|
120
|
+
req.error = str(e)
|
|
121
|
+
req.status = SyscallStatus.ERROR
|
|
122
|
+
return req
|
|
123
|
+
|
|
124
|
+
def run_round_robin_tick(self) -> Optional[SyscallRequest]:
|
|
125
|
+
"""Runs the head-of-queue request for at most `rr_quantum`
|
|
126
|
+
seconds, via ContextInterruptController -- which picks the
|
|
127
|
+
text-based or logits-based snapshot approach depending on what
|
|
128
|
+
the request's core supports. If it finishes within that slice,
|
|
129
|
+
it's marked DONE and NOT re-queued. If the slice runs out
|
|
130
|
+
first:
|
|
131
|
+
- text-based: the partial output is folded into a continuation
|
|
132
|
+
syscall (build_continuation) and pushed to the back of the
|
|
133
|
+
queue.
|
|
134
|
+
- logits-based: the request's `syscall` is left UNCHANGED
|
|
135
|
+
(nothing to rebuild -- the search-tree state itself is what
|
|
136
|
+
gets resumed) and its `snapshot` is stored for the next tick
|
|
137
|
+
to resume from directly.
|
|
138
|
+
Either way, every other queued request gets its own slice
|
|
139
|
+
before this one is revisited -- that's the "round" in Round
|
|
140
|
+
Robin."""
|
|
141
|
+
if not self.queue:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
req = self.queue.popleft()
|
|
145
|
+
req.status = SyscallStatus.RUNNING
|
|
146
|
+
try:
|
|
147
|
+
partial = self._interrupt_controller.run_slice(
|
|
148
|
+
req.core, req.syscall, self.rr_quantum, resume_snapshot=req.snapshot
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
req.error = str(e)
|
|
152
|
+
req.status = SyscallStatus.ERROR
|
|
153
|
+
return req
|
|
154
|
+
|
|
155
|
+
req.slices_run += 1
|
|
156
|
+
if partial.cumulative:
|
|
157
|
+
# Logits mode: beam decoding naturally tracks the full
|
|
158
|
+
# sequence generated so far, so this IS the total, not a
|
|
159
|
+
# delta to append.
|
|
160
|
+
req.accumulated_text = partial.text_so_far
|
|
161
|
+
else:
|
|
162
|
+
# Text mode: each resumed stream only yields its own new
|
|
163
|
+
# continuation text.
|
|
164
|
+
req.accumulated_text += partial.text_so_far
|
|
165
|
+
|
|
166
|
+
if partial.complete:
|
|
167
|
+
req.result = LLMResponse(text=req.accumulated_text)
|
|
168
|
+
req.status = SyscallStatus.DONE
|
|
169
|
+
req.snapshot = None
|
|
170
|
+
else:
|
|
171
|
+
if partial.snapshot.mode == "text":
|
|
172
|
+
req.syscall = self._interrupt_controller.build_continuation(req.syscall, partial)
|
|
173
|
+
req.snapshot = None
|
|
174
|
+
else:
|
|
175
|
+
req.snapshot = partial.snapshot
|
|
176
|
+
req.status = SyscallStatus.QUEUED
|
|
177
|
+
self.queue.append(req)
|
|
178
|
+
|
|
179
|
+
return req
|
|
180
|
+
|
|
181
|
+
def tick(self) -> Optional[SyscallRequest]:
|
|
182
|
+
"""Advances the scheduler by exactly one step, using whichever
|
|
183
|
+
strategy this instance was configured with."""
|
|
184
|
+
if self.strategy == SchedulingStrategy.FIFO:
|
|
185
|
+
return self.run_fifo_once()
|
|
186
|
+
return self.run_round_robin_tick()
|
|
187
|
+
|
|
188
|
+
def drain(self, max_ticks: int = 10_000) -> list[SyscallRequest]:
|
|
189
|
+
"""Runs tick() until the queue is empty (every request reaches
|
|
190
|
+
DONE or ERROR) or max_ticks is hit, as a safety bound against
|
|
191
|
+
an unbounded RR cycle. Returns the requests in the order they
|
|
192
|
+
finished."""
|
|
193
|
+
finished = []
|
|
194
|
+
ticks = 0
|
|
195
|
+
while self.queue and ticks < max_ticks:
|
|
196
|
+
req = self.tick()
|
|
197
|
+
ticks += 1
|
|
198
|
+
if req is not None and req.status in (SyscallStatus.DONE, SyscallStatus.ERROR):
|
|
199
|
+
finished.append(req)
|
|
200
|
+
return finished
|