programasweights 0.3.1__tar.gz → 0.4.0__tar.gz
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.
- {programasweights-0.3.1 → programasweights-0.4.0}/AGENTS.md +1 -1
- {programasweights-0.3.1 → programasweights-0.4.0}/PKG-INFO +2 -2
- {programasweights-0.3.1 → programasweights-0.4.0}/PYPI_README.md +1 -1
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/adr/001-llama-cpp-over-pytorch.md +1 -1
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/advanced/adrs.md +5 -4
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/case-studies/site-navigation.md +21 -21
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/guide/browser-inference.md +7 -7
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/index.md +2 -2
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/__init__.py +26 -8
- programasweights-0.4.0/programasweights/cache.py +323 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/cli.py +1 -1
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/client.py +69 -3
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/runtime_llamacpp.py +19 -6
- {programasweights-0.3.1 → programasweights-0.4.0}/pyproject.toml +1 -1
- programasweights-0.4.0/tests/test_runtime_registry_sdk.py +81 -0
- programasweights-0.3.1/programasweights/cache.py +0 -131
- {programasweights-0.3.1 → programasweights-0.4.0}/.gitignore +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/.readthedocs.yaml +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/CHANGELOG.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/LICENSE +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/README.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/adr/002-q4_0-adapter-format.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/adr/003-single-spec-field.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/adr/004-compiler-naming.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/adr/005-vllm-hidden-states.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/adr/006-email-api-key-auth.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/advanced/architecture.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/api-reference/cli.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/api-reference/python-sdk.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/api-reference/rest-api.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/architecture.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/case-studies/log-monitoring.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/case-studies/semantic-search.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/case-studies/tool-calling.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/getting-started/first-program.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/getting-started/installation.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/getting-started/naming-programs.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/guide/how-it-works.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/guide/local-inference.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/guide/writing-good-specs.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/hub/browsing-programs.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/hub/feedback-cases.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/hub/publishing-programs.md +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/docs/requirements.txt +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/examples/flask_app.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/examples/jupyter_notebook.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/examples/langchain_integration.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/examples/paw_monitor.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/examples/replace_openai.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/mkdocs.yml +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/_output.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/artifacts.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/compiler/__init__.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/compiler/dummy.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/config.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/convert_peft_to_paw.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/paw_format.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/runtime/__init__.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/runtime/interpreter.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/programasweights/runtime/interpreter_onnx.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/tests/test_offline_cache.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/tests/test_sdk.py +0 -0
- {programasweights-0.3.1 → programasweights-0.4.0}/tests/test_sdk.sh +0 -0
|
@@ -38,7 +38,7 @@ fn("I love this!") # "positive"
|
|
|
38
38
|
## Two Compilers
|
|
39
39
|
|
|
40
40
|
- **Standard** (`paw-4b-qwen3-0.6b`) — higher accuracy, 594 MB base + ~22 MB/program. Default.
|
|
41
|
-
- **Compact** (`paw-4b-gpt2`) — smaller (
|
|
41
|
+
- **Compact** (`paw-4b-gpt2`) — smaller (134 MB base + ~5 MB/program), runs in browser via WebAssembly.
|
|
42
42
|
|
|
43
43
|
## When to Use PAW
|
|
44
44
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: programasweights
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Compile natural language specifications into neural programs that run locally via llama.cpp.
|
|
5
5
|
Project-URL: Homepage, https://programasweights.com
|
|
6
6
|
Project-URL: Repository, https://github.com/programasweights/programasweights-python
|
|
@@ -71,7 +71,7 @@ fn("I love this!") # "positive"
|
|
|
71
71
|
| --------------- | --------------------- | -------------------- |
|
|
72
72
|
| Compiler name | `paw-4b-qwen3-0.6b` | `paw-4b-gpt2` |
|
|
73
73
|
| Accuracy | Higher | Lower |
|
|
74
|
-
| Base model size | 594 MB |
|
|
74
|
+
| Base model size | 594 MB | 134 MB |
|
|
75
75
|
| Program size | ~22 MB | ~5 MB |
|
|
76
76
|
| Local inference | ~0.05-0.5s per call | ~0.03-0.3s per call |
|
|
77
77
|
| Runs in browser | No | Yes (WebAssembly) |
|
|
@@ -41,7 +41,7 @@ fn("I love this!") # "positive"
|
|
|
41
41
|
| --------------- | --------------------- | -------------------- |
|
|
42
42
|
| Compiler name | `paw-4b-qwen3-0.6b` | `paw-4b-gpt2` |
|
|
43
43
|
| Accuracy | Higher | Lower |
|
|
44
|
-
| Base model size | 594 MB |
|
|
44
|
+
| Base model size | 594 MB | 134 MB |
|
|
45
45
|
| Program size | ~22 MB | ~5 MB |
|
|
46
46
|
| Local inference | ~0.05-0.5s per call | ~0.03-0.3s per call |
|
|
47
47
|
| Runs in browser | No | Yes (WebAssembly) |
|
|
@@ -17,5 +17,5 @@ Replace the PyTorch runtime with llama-cpp-python (~80MB install). Use GGUF mode
|
|
|
17
17
|
- Inference uses Metal (Mac), CPU (Linux/Windows) — no GPU required
|
|
18
18
|
- Must pre-render chat templates server-side (no transformers tokenizer on client)
|
|
19
19
|
- .paw format must change from v2 (safetensors) to v3 (GGUF adapter)
|
|
20
|
-
- Base model is downloaded once (~594 MB for
|
|
20
|
+
- Base model is downloaded once (~594 MB Q6_K for Qwen3, ~134 MB Q8_0 for GPT-2) and shared across all functions
|
|
21
21
|
- Per-function adapter download is ~23MB (Q4_0, confirmed lossless at 4096-scale eval)
|
|
@@ -14,16 +14,17 @@ Concise records of major technical choices. Full ADR files may live elsewhere in
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## ADR 002:
|
|
17
|
+
## ADR 002: Quantization levels for base models and adapters
|
|
18
18
|
|
|
19
|
-
**Decision:** Use **Q4_0** for adapters
|
|
19
|
+
**Decision:** Use **Q4_0** for adapters. For base models, use **Q6_K** for Qwen3 0.6B and **Q8_0** for GPT-2.
|
|
20
20
|
|
|
21
21
|
**Context:** Empirical evaluation on **4096** held-out examples across quantization settings informed the trade-off.
|
|
22
22
|
|
|
23
23
|
**Consequence:**
|
|
24
24
|
|
|
25
|
-
- **Q6_K base** — quality is preserved while the footprint is roughly **60% smaller** than fp16.
|
|
26
|
-
- **
|
|
25
|
+
- **Q6_K base (Qwen3 0.6B)** — quality is preserved while the footprint is roughly **60% smaller** than fp16. 4096-sample eval shows no accuracy loss vs fp16.
|
|
26
|
+
- **Q8_0 base (GPT-2)** — Q6_K caused ~3.5% accuracy loss on GPT-2 (4096-sample eval). Q8_0 closes the gap at only ~29 MB additional cost (134 MB vs 105 MB).
|
|
27
|
+
- **Q4_0 adapter** — quality loss is negligible for both models; adapter size drops to about **23 MB** (Qwen3) / **5 MB** (GPT-2) versus **78 MB** / **19 MB** at fp16.
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
@@ -21,20 +21,20 @@ Pages: /playground, /hub, /browser, /docs, /settings, /about
|
|
|
21
21
|
|
|
22
22
|
### Attempt 2: Reframe as classification
|
|
23
23
|
|
|
24
|
-
Instead of generating URLs, output a
|
|
24
|
+
Instead of generating URLs, output a short semantic label such as `playground`, `docs`, or `feedback`. The frontend maps those labels to actual destinations.
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
Classify the user's intent. Return ONLY a single
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
Classify the user's intent. Return ONLY a single label.
|
|
28
|
+
playground = Create or compile a new program
|
|
29
|
+
hub = Browse or search existing programs
|
|
30
|
+
browser = Run a program in the browser
|
|
31
31
|
...
|
|
32
|
-
|
|
32
|
+
none = None of the above
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
**Result:** Accuracy jumped from ~67% to ~88%. Classification is far more reliable than generation for routing.
|
|
36
36
|
|
|
37
|
-
**Lesson:** Reframe generation tasks as classification whenever possible. Output a label
|
|
37
|
+
**Lesson:** Reframe generation tasks as classification whenever possible. Output a short label, map it to the real value in your code.
|
|
38
38
|
|
|
39
39
|
### Attempt 3: Add Q&A support
|
|
40
40
|
|
|
@@ -71,8 +71,8 @@ validator = paw.function("my-answer-validator")
|
|
|
71
71
|
|
|
72
72
|
def handle_query(user_query: str):
|
|
73
73
|
destination = router(user_query)
|
|
74
|
-
if destination != "
|
|
75
|
-
return {"action": "navigate", "page":
|
|
74
|
+
if destination != "none":
|
|
75
|
+
return {"action": "navigate", "page": PAGE_MAP[destination]}
|
|
76
76
|
|
|
77
77
|
category = q_type(user_query)
|
|
78
78
|
if category == "yes_no":
|
|
@@ -92,36 +92,36 @@ Each program has a focused spec. For example, the page classifier:
|
|
|
92
92
|
|
|
93
93
|
```python
|
|
94
94
|
router = paw.compile("""
|
|
95
|
-
Classify the user's intent. Return ONLY a single
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
Classify the user's intent. Return ONLY a single label.
|
|
96
|
+
playground = Create or compile something new
|
|
97
|
+
hub = Browse or search existing items
|
|
98
|
+
browser = Run something in the browser
|
|
99
|
+
docs = Read documentation
|
|
100
|
+
settings = Manage account or API keys
|
|
101
|
+
none = None of the above (likely a question)
|
|
102
102
|
|
|
103
103
|
Input: how do I get started
|
|
104
|
-
Output:
|
|
104
|
+
Output: docs
|
|
105
105
|
|
|
106
106
|
Input: browse community programs
|
|
107
|
-
Output:
|
|
107
|
+
Output: hub
|
|
108
108
|
|
|
109
109
|
Input: is it free?
|
|
110
|
-
Output:
|
|
110
|
+
Output: none
|
|
111
111
|
""")
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
## Adapting this for your site
|
|
115
115
|
|
|
116
116
|
1. **List your pages** with short descriptions of what users do there
|
|
117
|
-
2. **Compile a classifier** that maps intents to
|
|
117
|
+
2. **Compile a classifier** that maps intents to short labels
|
|
118
118
|
3. **Test with 20-30 real queries** your users would type — iterate on the spec wording
|
|
119
119
|
4. If users also ask questions, add Q&A programs and a validator
|
|
120
120
|
5. Each program compiles once and is cached forever — the pipeline runs locally with no API calls
|
|
121
121
|
|
|
122
122
|
## Takeaways
|
|
123
123
|
|
|
124
|
-
- **Classification beats generation** for routing. Output a label, map it in code.
|
|
124
|
+
- **Classification beats generation** for routing. Output a short label, map it in code.
|
|
125
125
|
- **Multiple small programs beat one complex program.** When accuracy drops, split.
|
|
126
126
|
- **A validator catches failures** the other programs miss — cheap insurance.
|
|
127
127
|
- **Iterate with real queries.** Build a small test set, measure, adjust wording, repeat.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Run PAW programs directly in the browser via WebAssembly. No server, no API key, no setup.
|
|
4
4
|
|
|
5
|
-
Programs compiled with the compact interpreter (GPT-2 124M) run entirely client-side. The base model (
|
|
5
|
+
Programs compiled with the compact interpreter (GPT-2 124M) run entirely client-side. The base model (134 MB) downloads once and is cached; each program adds ~12 MB total (~5 MB adapter + ~7 MB prefix cache).
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ Programs compiled with the compact interpreter (GPT-2 124M) run entirely client-
|
|
|
12
12
|
<script type="module">
|
|
13
13
|
import paw from 'https://cdn.jsdelivr.net/npm/@programasweights/web';
|
|
14
14
|
|
|
15
|
-
const fn = await paw.function('
|
|
15
|
+
const fn = await paw.function('email-triage');
|
|
16
16
|
const result = await fn('Urgent: server is down!');
|
|
17
17
|
console.log(result); // "immediate"
|
|
18
18
|
</script>
|
|
@@ -27,7 +27,7 @@ npm install @programasweights/web
|
|
|
27
27
|
```javascript
|
|
28
28
|
import paw from '@programasweights/web';
|
|
29
29
|
|
|
30
|
-
const fn = await paw.function('
|
|
30
|
+
const fn = await paw.function('email-triage', {
|
|
31
31
|
onProgress: ({ loaded, total, stage }) => {
|
|
32
32
|
console.log(`${stage}: ${Math.round(loaded/total*100)}%`);
|
|
33
33
|
},
|
|
@@ -42,7 +42,7 @@ await fn.free();
|
|
|
42
42
|
|
|
43
43
|
## How It Works
|
|
44
44
|
|
|
45
|
-
1. **Base model** — Compact interpreter (GPT-2 124M,
|
|
45
|
+
1. **Base model** — Compact interpreter (GPT-2 124M, 134 MB) downloads from HuggingFace CDN and is cached in the browser after first load.
|
|
46
46
|
2. **LoRA adapter** — Each program is a ~5 MB Q4_0 GGUF LoRA adapter that specializes the base model for a specific task.
|
|
47
47
|
3. **Prefix cache** — A precomputed KV cache (~7 MB) eliminates the prompt prefill step, making the first inference call fast.
|
|
48
48
|
4. **Inference** — Runs via WebAssembly (llama.cpp compiled to WASM with SIMD). ~200ms per call on Chrome.
|
|
@@ -84,10 +84,10 @@ paw.configure({
|
|
|
84
84
|
|
|
85
85
|
| Component | Size | When |
|
|
86
86
|
|-----------|------|------|
|
|
87
|
-
| Base model (GPT-2
|
|
87
|
+
| Base model (GPT-2 Q8_0) | 134 MB | First program load (cached) |
|
|
88
88
|
| LoRA adapter | ~5 MB | Per program |
|
|
89
89
|
| Prefix cache | ~7 MB | Per program |
|
|
90
|
-
| **First load total** | **~
|
|
90
|
+
| **First load total** | **~146 MB** | |
|
|
91
91
|
| **Switching programs** | **~12 MB** | |
|
|
92
92
|
|
|
93
93
|
## Browser Compatibility
|
|
@@ -135,5 +135,5 @@ Without these headers, the SDK falls back to single-threaded WASM (still functio
|
|
|
135
135
|
## Limitations
|
|
136
136
|
|
|
137
137
|
- Only programs compiled with the **compact** interpreter (GPT-2 124M) are supported. Programs compiled with the standard interpreter (Qwen3 0.6B) are too large for browser inference (~594 MB base model).
|
|
138
|
-
- The
|
|
138
|
+
- The 134 MB base model download may be slow on mobile connections.
|
|
139
139
|
- Performance varies by browser (Chrome is fastest).
|
|
@@ -56,7 +56,7 @@ Each compiled program has two parts:
|
|
|
56
56
|
1. **Discrete pseudo-program** -- text instructions generated by the neural compiler
|
|
57
57
|
2. **Continuous neural adapter** -- LoRA weights (~23 MB) that tune the interpreter model
|
|
58
58
|
|
|
59
|
-
At runtime, the SDK loads a
|
|
59
|
+
At runtime, the SDK loads a quantized base model (Q6_K ~594 MB for Qwen3, Q8_0 ~134 MB for GPT-2, downloaded once) and applies the LoRA adapter. Inference runs entirely locally via llama.cpp.
|
|
60
60
|
|
|
61
61
|
## Browser Inference
|
|
62
62
|
|
|
@@ -65,7 +65,7 @@ Programs compiled with the compact interpreter (GPT-2 124M) also run directly in
|
|
|
65
65
|
```html
|
|
66
66
|
<script type="module">
|
|
67
67
|
import paw from '@programasweights/web';
|
|
68
|
-
const fn = await paw.function('
|
|
68
|
+
const fn = await paw.function('email-triage');
|
|
69
69
|
const result = await fn('Urgent: server is down!');
|
|
70
70
|
</script>
|
|
71
71
|
```
|
|
@@ -27,18 +27,19 @@ try:
|
|
|
27
27
|
from importlib.metadata import version as _meta_version
|
|
28
28
|
__version__ = _meta_version("programasweights")
|
|
29
29
|
except Exception:
|
|
30
|
-
__version__ = "0.
|
|
30
|
+
__version__ = "0.4.0"
|
|
31
31
|
|
|
32
32
|
from .config import get_api_url, get_api_key, set_api_key
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def compile(
|
|
36
36
|
spec: str,
|
|
37
|
-
compiler: str =
|
|
37
|
+
compiler: str | None = None,
|
|
38
38
|
name: str | None = None,
|
|
39
39
|
tags: list[str] | None = None,
|
|
40
40
|
public: bool = True,
|
|
41
41
|
slug: str | None = None,
|
|
42
|
+
ephemeral: bool = False,
|
|
42
43
|
):
|
|
43
44
|
"""Compile a natural language specification into a neural program.
|
|
44
45
|
|
|
@@ -48,7 +49,8 @@ def compile(
|
|
|
48
49
|
|
|
49
50
|
Args:
|
|
50
51
|
spec: Full specification text. Include examples in the text if desired.
|
|
51
|
-
compiler: Compiler model (alias or snapshot name).
|
|
52
|
+
compiler: Compiler model (alias or snapshot name). If omitted, the
|
|
53
|
+
server chooses the current default compiler.
|
|
52
54
|
name: Human-readable program name (display title for the hub).
|
|
53
55
|
tags: Tags for hub discovery.
|
|
54
56
|
public: Whether to list on the public hub.
|
|
@@ -71,7 +73,7 @@ def compile(
|
|
|
71
73
|
from ._output import status
|
|
72
74
|
status("Compiling...")
|
|
73
75
|
client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
|
|
74
|
-
result = client.compile(spec, compiler=compiler, name=name, tags=tags, public=public, slug=slug)
|
|
76
|
+
result = client.compile(spec, compiler=compiler, name=name, tags=tags, public=public, slug=slug, ephemeral=ephemeral)
|
|
75
77
|
label = f"{result.id}"
|
|
76
78
|
if result.slug:
|
|
77
79
|
ver_str = f" v{result.version}" if result.version and result.version > 1 else ""
|
|
@@ -125,6 +127,7 @@ def function(
|
|
|
125
127
|
from .cache import is_program_cached, get_program_dir, get_cached_slug, save_slug_mapping
|
|
126
128
|
from .runtime_llamacpp import PawFunction
|
|
127
129
|
|
|
130
|
+
offline = offline or os.environ.get("PAW_OFFLINE", "").strip() in ("1", "true", "yes")
|
|
128
131
|
if n_gpu_layers is None:
|
|
129
132
|
n_gpu_layers = int(os.environ.get("PAW_GPU_LAYERS", "-1"))
|
|
130
133
|
|
|
@@ -133,7 +136,7 @@ def function(
|
|
|
133
136
|
resolved_id = program_id
|
|
134
137
|
if not re.fullmatch(r"[a-f0-9]{16,64}", program_id):
|
|
135
138
|
is_pinned = bool(re.search(r"@v\d+$", program_id))
|
|
136
|
-
use_offline = offline
|
|
139
|
+
use_offline = offline
|
|
137
140
|
|
|
138
141
|
if is_pinned:
|
|
139
142
|
cached = get_cached_slug(program_id)
|
|
@@ -176,7 +179,13 @@ def function(
|
|
|
176
179
|
|
|
177
180
|
program_dir = get_program_dir(resolved_id)
|
|
178
181
|
return PawFunction(
|
|
179
|
-
program_dir,
|
|
182
|
+
program_dir,
|
|
183
|
+
n_ctx=n_ctx,
|
|
184
|
+
n_gpu_layers=n_gpu_layers,
|
|
185
|
+
verbose=verbose,
|
|
186
|
+
api_url=get_api_url(),
|
|
187
|
+
api_key=get_api_key(),
|
|
188
|
+
offline=offline,
|
|
180
189
|
)
|
|
181
190
|
|
|
182
191
|
|
|
@@ -227,7 +236,7 @@ def login(key: str | None = None):
|
|
|
227
236
|
|
|
228
237
|
def compile_and_load(
|
|
229
238
|
spec: str,
|
|
230
|
-
compiler: str =
|
|
239
|
+
compiler: str | None = None,
|
|
231
240
|
n_ctx: int = 2048,
|
|
232
241
|
n_gpu_layers: int | None = None,
|
|
233
242
|
verbose: bool = False,
|
|
@@ -240,7 +249,8 @@ def compile_and_load(
|
|
|
240
249
|
|
|
241
250
|
Args:
|
|
242
251
|
spec: Natural language specification.
|
|
243
|
-
compiler: Compiler model name.
|
|
252
|
+
compiler: Compiler model name. If omitted, the server chooses the
|
|
253
|
+
current default compiler.
|
|
244
254
|
n_ctx: Context window size for llama.cpp.
|
|
245
255
|
n_gpu_layers: GPU layers (-1 = all, 0 = CPU only).
|
|
246
256
|
verbose: Print llama.cpp debug output.
|
|
@@ -293,10 +303,18 @@ def list_programs(sort: str = "recent", per_page: int = 20, page: int = 1) -> di
|
|
|
293
303
|
return client.list_programs(sort=sort, per_page=per_page, page=page)
|
|
294
304
|
|
|
295
305
|
|
|
306
|
+
def list_compilers() -> list[dict]:
|
|
307
|
+
"""List available compilers from the server."""
|
|
308
|
+
from .client import PAWClient
|
|
309
|
+
client = PAWClient(api_url=get_api_url(), api_key=get_api_key())
|
|
310
|
+
return client.list_compilers()
|
|
311
|
+
|
|
312
|
+
|
|
296
313
|
__all__ = [
|
|
297
314
|
"compile",
|
|
298
315
|
"compile_and_load",
|
|
299
316
|
"function",
|
|
317
|
+
"list_compilers",
|
|
300
318
|
"list_programs",
|
|
301
319
|
"list_versions",
|
|
302
320
|
"login",
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local cache management for base models and compiled programs.
|
|
3
|
+
|
|
4
|
+
Cache structure:
|
|
5
|
+
~/.cache/programasweights/
|
|
6
|
+
base_models/
|
|
7
|
+
qwen3-0.6b-q6_k.gguf # ~594 MB, downloaded once
|
|
8
|
+
gpt2-q8_0.gguf # ~134 MB, downloaded once
|
|
9
|
+
programs/
|
|
10
|
+
<program_id>/
|
|
11
|
+
adapter.gguf # ~23 MB, Q4_0 LoRA
|
|
12
|
+
prompt_template.txt
|
|
13
|
+
meta.json
|
|
14
|
+
slug_cache.json # slug -> program_id mapping
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
from . import config
|
|
26
|
+
|
|
27
|
+
BASE_MODEL_URLS = {
|
|
28
|
+
"qwen3-0.6b-q6_k": "https://huggingface.co/programasweights/Qwen3-0.6B-GGUF-Q6_K/resolve/main/qwen3-0.6b-q6_k.gguf",
|
|
29
|
+
"gpt2-q8_0": "https://huggingface.co/programasweights/GPT2-GGUF-Q8_0/resolve/main/gpt2-q8_0.gguf",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
INTERPRETER_TO_GGUF = {
|
|
33
|
+
"Qwen/Qwen3-0.6B": "qwen3-0.6b-q6_k",
|
|
34
|
+
"gpt2": "gpt2-q8_0",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
LEGACY_RUNTIME_MANIFESTS = {
|
|
39
|
+
"qwen3-0.6b-q6_k": {
|
|
40
|
+
"runtime_id": "qwen3-0.6b-q6_k",
|
|
41
|
+
"manifest_version": 1,
|
|
42
|
+
"display_name": "Qwen3 0.6B (Q6_K)",
|
|
43
|
+
"interpreter": "Qwen/Qwen3-0.6B",
|
|
44
|
+
"adapter_format": "gguf_lora",
|
|
45
|
+
"local_sdk": {
|
|
46
|
+
"supported": True,
|
|
47
|
+
"base_model": {
|
|
48
|
+
"provider": "huggingface",
|
|
49
|
+
"repo": "programasweights/Qwen3-0.6B-GGUF-Q6_K",
|
|
50
|
+
"file": "qwen3-0.6b-q6_k.gguf",
|
|
51
|
+
"url": BASE_MODEL_URLS["qwen3-0.6b-q6_k"],
|
|
52
|
+
"sha256": None,
|
|
53
|
+
},
|
|
54
|
+
"n_ctx": 2048,
|
|
55
|
+
},
|
|
56
|
+
"js_sdk": {
|
|
57
|
+
"supported": False,
|
|
58
|
+
"base_model": None,
|
|
59
|
+
"prefix_cache_supported": False,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
"gpt2-q8_0": {
|
|
63
|
+
"runtime_id": "gpt2-q8_0",
|
|
64
|
+
"manifest_version": 1,
|
|
65
|
+
"display_name": "GPT-2 124M (Q8_0)",
|
|
66
|
+
"interpreter": "gpt2",
|
|
67
|
+
"adapter_format": "gguf_lora",
|
|
68
|
+
"local_sdk": {
|
|
69
|
+
"supported": True,
|
|
70
|
+
"base_model": {
|
|
71
|
+
"provider": "huggingface",
|
|
72
|
+
"repo": "programasweights/GPT2-GGUF-Q8_0",
|
|
73
|
+
"file": "gpt2-q8_0.gguf",
|
|
74
|
+
"url": BASE_MODEL_URLS["gpt2-q8_0"],
|
|
75
|
+
"sha256": None,
|
|
76
|
+
},
|
|
77
|
+
"n_ctx": 2048,
|
|
78
|
+
},
|
|
79
|
+
"js_sdk": {
|
|
80
|
+
"supported": True,
|
|
81
|
+
"base_model": {
|
|
82
|
+
"provider": "huggingface",
|
|
83
|
+
"repo": "programasweights/GPT2-GGUF-Q8_0",
|
|
84
|
+
"file": "gpt2-q8_0.gguf",
|
|
85
|
+
"url": BASE_MODEL_URLS["gpt2-q8_0"],
|
|
86
|
+
"sha256": None,
|
|
87
|
+
},
|
|
88
|
+
"prefix_cache_supported": True,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _runtime_cache_dir() -> Path:
|
|
95
|
+
d = config.get_cache_dir() / "runtimes"
|
|
96
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
return d
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _runtime_manifest_path(runtime_id: str) -> Path:
|
|
101
|
+
return _runtime_cache_dir() / f"{runtime_id}.json"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_cached_runtime_manifest(runtime_id: str) -> dict | None:
|
|
105
|
+
path = _runtime_manifest_path(runtime_id)
|
|
106
|
+
if not path.exists():
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
return json.loads(path.read_text())
|
|
110
|
+
except (json.JSONDecodeError, OSError):
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def save_runtime_manifest(runtime_manifest: dict) -> None:
|
|
115
|
+
runtime_id = runtime_manifest.get("runtime_id")
|
|
116
|
+
if not runtime_id:
|
|
117
|
+
return
|
|
118
|
+
path = _runtime_manifest_path(runtime_id)
|
|
119
|
+
path.write_text(json.dumps(runtime_manifest, indent=2))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _legacy_runtime_manifest(interpreter: str | None) -> dict | None:
|
|
123
|
+
if not interpreter:
|
|
124
|
+
return None
|
|
125
|
+
runtime_id = INTERPRETER_TO_GGUF.get(interpreter)
|
|
126
|
+
if not runtime_id:
|
|
127
|
+
return None
|
|
128
|
+
manifest = LEGACY_RUNTIME_MANIFESTS.get(runtime_id)
|
|
129
|
+
return json.loads(json.dumps(manifest)) if manifest else None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _is_runtime_manifest_complete(runtime_manifest: dict | None) -> bool:
|
|
133
|
+
if not runtime_manifest:
|
|
134
|
+
return False
|
|
135
|
+
base_model = runtime_manifest.get("local_sdk", {}).get("base_model")
|
|
136
|
+
return bool(runtime_manifest.get("runtime_id") and base_model and base_model.get("file"))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def fetch_runtime_manifest(
|
|
140
|
+
runtime_id: str,
|
|
141
|
+
api_url: str | None = None,
|
|
142
|
+
api_key: str | None = None,
|
|
143
|
+
) -> dict:
|
|
144
|
+
base_url = (api_url or config.get_api_url()).rstrip("/")
|
|
145
|
+
headers = {}
|
|
146
|
+
if api_key:
|
|
147
|
+
headers["X-API-Key"] = api_key
|
|
148
|
+
resp = httpx.get(
|
|
149
|
+
f"{base_url}/api/v1/models/runtimes/{runtime_id}",
|
|
150
|
+
headers=headers,
|
|
151
|
+
timeout=10.0,
|
|
152
|
+
)
|
|
153
|
+
resp.raise_for_status()
|
|
154
|
+
runtime_manifest = resp.json()
|
|
155
|
+
save_runtime_manifest(runtime_manifest)
|
|
156
|
+
return runtime_manifest
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def resolve_runtime_manifest(
|
|
160
|
+
program_meta: dict,
|
|
161
|
+
api_url: str | None = None,
|
|
162
|
+
api_key: str | None = None,
|
|
163
|
+
offline: bool = False,
|
|
164
|
+
) -> dict | None:
|
|
165
|
+
embedded = program_meta.get("runtime")
|
|
166
|
+
if isinstance(embedded, dict) and _is_runtime_manifest_complete(embedded):
|
|
167
|
+
save_runtime_manifest(embedded)
|
|
168
|
+
return embedded
|
|
169
|
+
|
|
170
|
+
runtime_id = program_meta.get("runtime_id")
|
|
171
|
+
if runtime_id:
|
|
172
|
+
cached = get_cached_runtime_manifest(runtime_id)
|
|
173
|
+
if _is_runtime_manifest_complete(cached):
|
|
174
|
+
return cached
|
|
175
|
+
if not offline:
|
|
176
|
+
try:
|
|
177
|
+
return fetch_runtime_manifest(runtime_id, api_url=api_url, api_key=api_key)
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
return _legacy_runtime_manifest(program_meta.get("interpreter"))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _base_model_info_from_runtime(runtime_manifest: dict) -> dict | None:
|
|
185
|
+
return runtime_manifest.get("local_sdk", {}).get("base_model")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _build_hf_url(repo: str, file_name: str) -> str:
|
|
189
|
+
return f"https://huggingface.co/{repo}/resolve/main/{file_name}"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def get_base_model_path(
|
|
193
|
+
interpreter: str = "Qwen/Qwen3-0.6B",
|
|
194
|
+
runtime_manifest: dict | None = None,
|
|
195
|
+
) -> Path:
|
|
196
|
+
"""Get the path to the base model GGUF, downloading if needed."""
|
|
197
|
+
if runtime_manifest:
|
|
198
|
+
local_sdk = runtime_manifest.get("local_sdk", {})
|
|
199
|
+
if not local_sdk.get("supported", False):
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Runtime '{runtime_manifest.get('runtime_id', interpreter)}' is not supported by the local SDK."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
base_model = _base_model_info_from_runtime(runtime_manifest)
|
|
205
|
+
if base_model:
|
|
206
|
+
file_name = base_model.get("file")
|
|
207
|
+
if not file_name:
|
|
208
|
+
raise ValueError(
|
|
209
|
+
f"Runtime '{runtime_manifest.get('runtime_id', interpreter)}' is missing a base model file."
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
gguf_path = config.get_base_models_dir() / file_name
|
|
213
|
+
if gguf_path.exists():
|
|
214
|
+
return gguf_path
|
|
215
|
+
|
|
216
|
+
url = base_model.get("url")
|
|
217
|
+
if not url and base_model.get("provider") == "huggingface":
|
|
218
|
+
url = _build_hf_url(base_model["repo"], file_name)
|
|
219
|
+
if not url:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Runtime '{runtime_manifest.get('runtime_id', interpreter)}' is missing a downloadable base model URL."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
from ._output import status
|
|
225
|
+
label = runtime_manifest.get("display_name") or runtime_manifest.get("runtime_id") or interpreter
|
|
226
|
+
status(f"Downloading interpreter {label} (one-time download)...")
|
|
227
|
+
_download_file(url, gguf_path)
|
|
228
|
+
status(f"Saved to {gguf_path}")
|
|
229
|
+
return gguf_path
|
|
230
|
+
|
|
231
|
+
gguf_name = INTERPRETER_TO_GGUF.get(interpreter)
|
|
232
|
+
if not gguf_name:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Unknown interpreter: '{interpreter}'. "
|
|
235
|
+
f"Supported: {list(INTERPRETER_TO_GGUF.keys())}"
|
|
236
|
+
)
|
|
237
|
+
gguf_path = config.get_base_models_dir() / f"{gguf_name}.gguf"
|
|
238
|
+
|
|
239
|
+
if gguf_path.exists():
|
|
240
|
+
return gguf_path
|
|
241
|
+
|
|
242
|
+
url = BASE_MODEL_URLS.get(gguf_name)
|
|
243
|
+
if not url:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Unknown interpreter model: {interpreter}. "
|
|
246
|
+
f"Available: {list(INTERPRETER_TO_GGUF.keys())}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
from ._output import status
|
|
250
|
+
status(f"Downloading interpreter {gguf_name} (one-time download)...")
|
|
251
|
+
_download_file(url, gguf_path)
|
|
252
|
+
status(f"Saved to {gguf_path}")
|
|
253
|
+
return gguf_path
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_program_dir(program_id: str) -> Path:
|
|
257
|
+
"""Get the local cache directory for a program."""
|
|
258
|
+
return config.get_programs_dir() / program_id
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def is_program_cached(program_id: str) -> bool:
|
|
262
|
+
"""Check if a program's artifacts are already cached locally."""
|
|
263
|
+
d = get_program_dir(program_id)
|
|
264
|
+
return (d / "adapter.gguf").exists() and (d / "prompt_template.txt").exists()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _download_file(url: str, dest: Path):
|
|
268
|
+
"""Download a file atomically with progress indication."""
|
|
269
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
|
271
|
+
try:
|
|
272
|
+
with httpx.stream("GET", url, follow_redirects=True, timeout=300.0) as resp:
|
|
273
|
+
resp.raise_for_status()
|
|
274
|
+
total = int(resp.headers.get("content-length", 0))
|
|
275
|
+
downloaded = 0
|
|
276
|
+
with open(tmp, "wb") as f:
|
|
277
|
+
for chunk in resp.iter_bytes(chunk_size=8192):
|
|
278
|
+
f.write(chunk)
|
|
279
|
+
downloaded += len(chunk)
|
|
280
|
+
if total > 0:
|
|
281
|
+
pct = downloaded / total * 100
|
|
282
|
+
mb = downloaded / 1024 / 1024
|
|
283
|
+
from ._output import status_inline, status_end
|
|
284
|
+
status_inline(f" {mb:.1f} MB ({pct:.0f}%)")
|
|
285
|
+
from ._output import status_end
|
|
286
|
+
status_end()
|
|
287
|
+
os.replace(str(tmp), str(dest))
|
|
288
|
+
except BaseException:
|
|
289
|
+
tmp.unlink(missing_ok=True)
|
|
290
|
+
raise
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _slug_cache_path() -> Path:
|
|
294
|
+
return config.get_cache_dir() / "slug_cache.json"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_cached_slug(slug: str) -> str | None:
|
|
298
|
+
"""Look up a slug in the local cache. Returns program_id or None."""
|
|
299
|
+
path = _slug_cache_path()
|
|
300
|
+
if not path.exists():
|
|
301
|
+
return None
|
|
302
|
+
try:
|
|
303
|
+
data = json.loads(path.read_text())
|
|
304
|
+
program_id = data.get(slug)
|
|
305
|
+
if program_id and is_program_cached(program_id):
|
|
306
|
+
return program_id
|
|
307
|
+
except (json.JSONDecodeError, OSError):
|
|
308
|
+
pass
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def save_slug_mapping(slug: str, program_id: str) -> None:
|
|
313
|
+
"""Save a slug -> program_id mapping to the local cache."""
|
|
314
|
+
path = _slug_cache_path()
|
|
315
|
+
data: dict = {}
|
|
316
|
+
if path.exists():
|
|
317
|
+
try:
|
|
318
|
+
data = json.loads(path.read_text())
|
|
319
|
+
except (json.JSONDecodeError, OSError):
|
|
320
|
+
pass
|
|
321
|
+
data[slug] = program_id
|
|
322
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
323
|
+
path.write_text(json.dumps(data))
|
|
@@ -169,7 +169,7 @@ def main():
|
|
|
169
169
|
|
|
170
170
|
p = sub.add_parser("compile", help="Compile a spec on the server")
|
|
171
171
|
p.add_argument("--spec", required=True, help="Natural language specification")
|
|
172
|
-
p.add_argument("--compiler", default=
|
|
172
|
+
p.add_argument("--compiler", default=None, help="Compiler model (omit to use the server default)")
|
|
173
173
|
p.add_argument("--slug", default=None, help="URL-safe handle (e.g. 'message-classifier')")
|
|
174
174
|
p.add_argument("--private", action="store_true", help="Make program private (not listed on hub)")
|
|
175
175
|
p.add_argument("--json", action="store_true", help="JSON output")
|
|
@@ -6,6 +6,7 @@ Handles compilation, program download, and authentication.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import json
|
|
9
10
|
import os
|
|
10
11
|
import time
|
|
11
12
|
import zipfile
|
|
@@ -25,6 +26,10 @@ class Program:
|
|
|
25
26
|
status: str
|
|
26
27
|
slug: Optional[str] = None
|
|
27
28
|
compiler_snapshot: Optional[str] = None
|
|
29
|
+
compiler_kind: Optional[str] = None
|
|
30
|
+
pseudo_program_strategy: Optional[str] = None
|
|
31
|
+
runtime_id: Optional[str] = None
|
|
32
|
+
runtime_manifest_version: Optional[int] = None
|
|
28
33
|
timings: Optional[dict] = None
|
|
29
34
|
error: Optional[str] = None
|
|
30
35
|
version: Optional[int] = None
|
|
@@ -47,11 +52,12 @@ class PAWClient:
|
|
|
47
52
|
def compile(
|
|
48
53
|
self,
|
|
49
54
|
spec: str,
|
|
50
|
-
compiler: str =
|
|
55
|
+
compiler: str | None = None,
|
|
51
56
|
name: str | None = None,
|
|
52
57
|
tags: list[str] | None = None,
|
|
53
58
|
public: bool = True,
|
|
54
59
|
slug: str | None = None,
|
|
60
|
+
ephemeral: bool = False,
|
|
55
61
|
) -> Program:
|
|
56
62
|
"""Compile a spec into a neural program on the server.
|
|
57
63
|
|
|
@@ -70,7 +76,11 @@ class PAWClient:
|
|
|
70
76
|
Raises:
|
|
71
77
|
httpx.HTTPStatusError: On API errors (422 for validation, 429 for rate limit).
|
|
72
78
|
"""
|
|
73
|
-
body: dict = {"spec": spec, "
|
|
79
|
+
body: dict = {"spec": spec, "public": public}
|
|
80
|
+
if compiler:
|
|
81
|
+
body["compiler"] = compiler
|
|
82
|
+
if ephemeral:
|
|
83
|
+
body["ephemeral"] = True
|
|
74
84
|
if name:
|
|
75
85
|
body["name"] = name
|
|
76
86
|
if tags:
|
|
@@ -92,6 +102,10 @@ class PAWClient:
|
|
|
92
102
|
status=data.get("status", "unknown"),
|
|
93
103
|
slug=data.get("slug"),
|
|
94
104
|
compiler_snapshot=data.get("compiler_snapshot"),
|
|
105
|
+
compiler_kind=data.get("compiler_kind"),
|
|
106
|
+
pseudo_program_strategy=data.get("pseudo_program_strategy"),
|
|
107
|
+
runtime_id=data.get("runtime_id"),
|
|
108
|
+
runtime_manifest_version=data.get("runtime_manifest_version"),
|
|
95
109
|
timings=data.get("timings"),
|
|
96
110
|
error=data.get("error"),
|
|
97
111
|
version=data.get("version"),
|
|
@@ -121,7 +135,7 @@ class PAWClient:
|
|
|
121
135
|
from ._output import status
|
|
122
136
|
|
|
123
137
|
status(f"Downloading program {program_id[:12]}...")
|
|
124
|
-
max_wait =
|
|
138
|
+
max_wait = 60
|
|
125
139
|
elapsed = 0
|
|
126
140
|
resp = None
|
|
127
141
|
waiting_logged = False
|
|
@@ -174,8 +188,36 @@ class PAWClient:
|
|
|
174
188
|
raise ValueError(f"Unsafe path in .paw archive: {member}")
|
|
175
189
|
zf.extractall(program_dir)
|
|
176
190
|
|
|
191
|
+
self._hydrate_runtime_manifest(program_dir)
|
|
192
|
+
|
|
177
193
|
return program_dir
|
|
178
194
|
|
|
195
|
+
def _hydrate_runtime_manifest(self, program_dir: Path) -> None:
|
|
196
|
+
meta_path = program_dir / "meta.json"
|
|
197
|
+
if not meta_path.exists():
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
meta = json.loads(meta_path.read_text())
|
|
202
|
+
except (json.JSONDecodeError, OSError):
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
runtime_id = meta.get("runtime_id")
|
|
206
|
+
runtime = meta.get("runtime")
|
|
207
|
+
if isinstance(runtime, dict) and runtime.get("runtime_id") and runtime.get("local_sdk", {}).get("base_model"):
|
|
208
|
+
return
|
|
209
|
+
if not runtime_id:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
runtime_manifest = self.get_runtime_manifest(runtime_id)
|
|
214
|
+
except Exception:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
meta["runtime"] = runtime_manifest
|
|
218
|
+
meta.setdefault("runtime_manifest_version", runtime_manifest.get("manifest_version"))
|
|
219
|
+
meta_path.write_text(json.dumps(meta, indent=2))
|
|
220
|
+
|
|
179
221
|
def get_program_meta(self, program_id: str) -> dict:
|
|
180
222
|
"""Get program metadata from the server."""
|
|
181
223
|
resp = httpx.get(
|
|
@@ -186,6 +228,30 @@ class PAWClient:
|
|
|
186
228
|
resp.raise_for_status()
|
|
187
229
|
return resp.json()
|
|
188
230
|
|
|
231
|
+
def get_runtime_manifest(self, runtime_id: str) -> dict:
|
|
232
|
+
"""Fetch a runtime manifest from the server and cache it locally."""
|
|
233
|
+
from . import cache
|
|
234
|
+
|
|
235
|
+
cached = cache.get_cached_runtime_manifest(runtime_id)
|
|
236
|
+
if cached:
|
|
237
|
+
return cached
|
|
238
|
+
|
|
239
|
+
return cache.fetch_runtime_manifest(
|
|
240
|
+
runtime_id,
|
|
241
|
+
api_url=self._api_url,
|
|
242
|
+
api_key=self._api_key,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def list_compilers(self) -> list[dict]:
|
|
246
|
+
"""List available compilers from the server."""
|
|
247
|
+
resp = httpx.get(
|
|
248
|
+
f"{self._api_url}/api/v1/models/compilers",
|
|
249
|
+
headers=self._headers(),
|
|
250
|
+
timeout=10.0,
|
|
251
|
+
)
|
|
252
|
+
resp.raise_for_status()
|
|
253
|
+
return resp.json()["compilers"]
|
|
254
|
+
|
|
189
255
|
def list_slug_versions(self, slug: str) -> dict:
|
|
190
256
|
"""List all versions of a slug. Slug format: 'username/slug-name' or bare 'slug-name'."""
|
|
191
257
|
resp = httpx.get(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
llama.cpp runtime for local inference with LoRA adapters.
|
|
3
3
|
|
|
4
|
-
Loads a base GGUF model (Q6_K) and applies a
|
|
4
|
+
Loads a base GGUF model (Q6_K for Qwen3, Q8_0 for GPT-2) and applies a
|
|
5
|
+
Q4_0 LoRA adapter per-program.
|
|
5
6
|
Uses the pre-rendered prompt template from the .paw bundle.
|
|
6
7
|
|
|
7
8
|
Prefix KV cache is saved to disk after the first call and reloaded on
|
|
@@ -33,6 +34,9 @@ class PawFunction:
|
|
|
33
34
|
n_ctx: int = 2048,
|
|
34
35
|
n_gpu_layers: int = 0,
|
|
35
36
|
verbose: bool = False,
|
|
37
|
+
api_url: str | None = None,
|
|
38
|
+
api_key: str | None = None,
|
|
39
|
+
offline: bool = False,
|
|
36
40
|
):
|
|
37
41
|
program_dir = Path(program_dir)
|
|
38
42
|
self._program_dir = program_dir
|
|
@@ -53,7 +57,17 @@ class PawFunction:
|
|
|
53
57
|
|
|
54
58
|
from ._output import status
|
|
55
59
|
interpreter = self._meta.get("interpreter", "Qwen/Qwen3-0.6B")
|
|
56
|
-
|
|
60
|
+
runtime_manifest = cache.resolve_runtime_manifest(
|
|
61
|
+
self._meta,
|
|
62
|
+
api_url=api_url,
|
|
63
|
+
api_key=api_key,
|
|
64
|
+
offline=offline,
|
|
65
|
+
)
|
|
66
|
+
if runtime_manifest:
|
|
67
|
+
self._meta["runtime"] = runtime_manifest
|
|
68
|
+
self._meta.setdefault("runtime_id", runtime_manifest.get("runtime_id"))
|
|
69
|
+
self._meta.setdefault("runtime_manifest_version", runtime_manifest.get("manifest_version"))
|
|
70
|
+
base_model_path = cache.get_base_model_path(interpreter, runtime_manifest=runtime_manifest)
|
|
57
71
|
|
|
58
72
|
status("Loading interpreter...")
|
|
59
73
|
|
|
@@ -88,7 +102,6 @@ class PawFunction:
|
|
|
88
102
|
_os.close(_old_stderr)
|
|
89
103
|
|
|
90
104
|
placeholder = "{INPUT_PLACEHOLDER}"
|
|
91
|
-
self._use_special = interpreter not in ("gpt2",)
|
|
92
105
|
|
|
93
106
|
if placeholder in self._template:
|
|
94
107
|
prefix_text = self._template.split(placeholder)[0]
|
|
@@ -99,8 +112,8 @@ class PawFunction:
|
|
|
99
112
|
|
|
100
113
|
self._prefix_tokens = self._llm.tokenize(
|
|
101
114
|
prefix_text.encode("utf-8"),
|
|
102
|
-
add_bos=
|
|
103
|
-
special=
|
|
115
|
+
add_bos=False,
|
|
116
|
+
special=True,
|
|
104
117
|
)
|
|
105
118
|
self._suffix_text = suffix_text
|
|
106
119
|
self._n_prefix = len(self._prefix_tokens)
|
|
@@ -183,7 +196,7 @@ class PawFunction:
|
|
|
183
196
|
input_tokens = self._llm.tokenize(
|
|
184
197
|
input_with_suffix.encode("utf-8"),
|
|
185
198
|
add_bos=False,
|
|
186
|
-
special=
|
|
199
|
+
special=True,
|
|
187
200
|
)
|
|
188
201
|
|
|
189
202
|
tokens_used = self._n_prefix + len(input_tokens)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "programasweights"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Compile natural language specifications into neural programs that run locally via llama.cpp."
|
|
9
9
|
readme = "PYPI_README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from programasweights import cache
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
RUNTIME = {
|
|
12
|
+
"runtime_id": "gpt2-q8_0",
|
|
13
|
+
"manifest_version": 1,
|
|
14
|
+
"display_name": "GPT-2 124M (Q8_0)",
|
|
15
|
+
"interpreter": "gpt2",
|
|
16
|
+
"adapter_format": "gguf_lora",
|
|
17
|
+
"local_sdk": {
|
|
18
|
+
"supported": True,
|
|
19
|
+
"base_model": {
|
|
20
|
+
"provider": "huggingface",
|
|
21
|
+
"repo": "programasweights/GPT2-GGUF-Q8_0",
|
|
22
|
+
"file": "gpt2-q8_0.gguf",
|
|
23
|
+
"url": "https://example.com/gpt2-q8_0.gguf",
|
|
24
|
+
"sha256": None,
|
|
25
|
+
},
|
|
26
|
+
"n_ctx": 2048,
|
|
27
|
+
},
|
|
28
|
+
"js_sdk": {
|
|
29
|
+
"supported": True,
|
|
30
|
+
"base_model": {
|
|
31
|
+
"provider": "huggingface",
|
|
32
|
+
"repo": "programasweights/GPT2-GGUF-Q8_0",
|
|
33
|
+
"file": "gpt2-q8_0.gguf",
|
|
34
|
+
"url": "https://example.com/gpt2-q8_0.gguf",
|
|
35
|
+
"sha256": None,
|
|
36
|
+
},
|
|
37
|
+
"prefix_cache_supported": True,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture(autouse=True)
|
|
43
|
+
def _cache_dir(tmp_path, monkeypatch):
|
|
44
|
+
monkeypatch.setenv("PAW_CACHE_DIR", str(tmp_path / "cache"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_resolve_runtime_manifest_prefers_embedded_manifest():
|
|
48
|
+
meta = {
|
|
49
|
+
"interpreter": "gpt2",
|
|
50
|
+
"runtime_id": "gpt2-q8_0",
|
|
51
|
+
"runtime": RUNTIME,
|
|
52
|
+
}
|
|
53
|
+
resolved = cache.resolve_runtime_manifest(meta, offline=True)
|
|
54
|
+
assert resolved["runtime_id"] == "gpt2-q8_0"
|
|
55
|
+
cached = cache.get_cached_runtime_manifest("gpt2-q8_0")
|
|
56
|
+
assert cached["runtime_id"] == "gpt2-q8_0"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_resolve_runtime_manifest_uses_legacy_fallback_when_offline():
|
|
60
|
+
meta = {
|
|
61
|
+
"interpreter": "gpt2",
|
|
62
|
+
"runtime_id": "gpt2-q8_0",
|
|
63
|
+
}
|
|
64
|
+
resolved = cache.resolve_runtime_manifest(meta, offline=True)
|
|
65
|
+
assert resolved is not None
|
|
66
|
+
assert resolved["runtime_id"] == "gpt2-q8_0"
|
|
67
|
+
assert resolved["local_sdk"]["base_model"]["file"] == "gpt2-q8_0.gguf"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_get_base_model_path_uses_runtime_manifest(tmp_path, monkeypatch):
|
|
71
|
+
target = tmp_path / "cache" / "base_models" / "gpt2-q8_0.gguf"
|
|
72
|
+
|
|
73
|
+
def fake_download(url: str, dest: Path):
|
|
74
|
+
assert url == "https://example.com/gpt2-q8_0.gguf"
|
|
75
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
dest.write_bytes(b"gguf")
|
|
77
|
+
|
|
78
|
+
monkeypatch.setattr(cache, "_download_file", fake_download)
|
|
79
|
+
path = cache.get_base_model_path("gpt2", runtime_manifest=RUNTIME)
|
|
80
|
+
assert path == target
|
|
81
|
+
assert path.read_bytes() == b"gguf"
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Local cache management for base models and compiled programs.
|
|
3
|
-
|
|
4
|
-
Cache structure:
|
|
5
|
-
~/.cache/programasweights/
|
|
6
|
-
base_models/
|
|
7
|
-
qwen3-0.6b-q6_k.gguf # ~594 MB, downloaded once
|
|
8
|
-
programs/
|
|
9
|
-
<program_id>/
|
|
10
|
-
adapter.gguf # ~23 MB, Q4_0 LoRA
|
|
11
|
-
prompt_template.txt
|
|
12
|
-
meta.json
|
|
13
|
-
slug_cache.json # slug -> program_id mapping
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import json
|
|
19
|
-
import os
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
|
|
22
|
-
import httpx
|
|
23
|
-
|
|
24
|
-
from . import config
|
|
25
|
-
|
|
26
|
-
BASE_MODEL_URLS = {
|
|
27
|
-
"qwen3-0.6b-q6_k": "https://huggingface.co/programasweights/Qwen3-0.6B-GGUF-Q6_K/resolve/main/qwen3-0.6b-q6_k.gguf",
|
|
28
|
-
"gpt2-q6_k": "https://huggingface.co/programasweights/GPT2-GGUF-Q6_K/resolve/main/gpt2-q6_k.gguf",
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
INTERPRETER_TO_GGUF = {
|
|
32
|
-
"Qwen/Qwen3-0.6B": "qwen3-0.6b-q6_k",
|
|
33
|
-
"gpt2": "gpt2-q6_k",
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def get_base_model_path(interpreter: str = "Qwen/Qwen3-0.6B") -> Path:
|
|
38
|
-
"""Get the path to the base model GGUF, downloading if needed."""
|
|
39
|
-
gguf_name = INTERPRETER_TO_GGUF.get(interpreter)
|
|
40
|
-
if not gguf_name:
|
|
41
|
-
raise ValueError(
|
|
42
|
-
f"Unknown interpreter: '{interpreter}'. "
|
|
43
|
-
f"Supported: {list(INTERPRETER_TO_GGUF.keys())}"
|
|
44
|
-
)
|
|
45
|
-
gguf_path = config.get_base_models_dir() / f"{gguf_name}.gguf"
|
|
46
|
-
|
|
47
|
-
if gguf_path.exists():
|
|
48
|
-
return gguf_path
|
|
49
|
-
|
|
50
|
-
url = BASE_MODEL_URLS.get(gguf_name)
|
|
51
|
-
if not url:
|
|
52
|
-
raise ValueError(
|
|
53
|
-
f"Unknown interpreter model: {interpreter}. "
|
|
54
|
-
f"Available: {list(INTERPRETER_TO_GGUF.keys())}"
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
from ._output import status
|
|
58
|
-
status(f"Downloading interpreter {gguf_name} (one-time download)...")
|
|
59
|
-
_download_file(url, gguf_path)
|
|
60
|
-
status(f"Saved to {gguf_path}")
|
|
61
|
-
return gguf_path
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def get_program_dir(program_id: str) -> Path:
|
|
65
|
-
"""Get the local cache directory for a program."""
|
|
66
|
-
return config.get_programs_dir() / program_id
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def is_program_cached(program_id: str) -> bool:
|
|
70
|
-
"""Check if a program's artifacts are already cached locally."""
|
|
71
|
-
d = get_program_dir(program_id)
|
|
72
|
-
return (d / "adapter.gguf").exists() and (d / "prompt_template.txt").exists()
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _download_file(url: str, dest: Path):
|
|
76
|
-
"""Download a file atomically with progress indication."""
|
|
77
|
-
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
-
tmp = dest.with_suffix(dest.suffix + ".tmp")
|
|
79
|
-
try:
|
|
80
|
-
with httpx.stream("GET", url, follow_redirects=True, timeout=300.0) as resp:
|
|
81
|
-
resp.raise_for_status()
|
|
82
|
-
total = int(resp.headers.get("content-length", 0))
|
|
83
|
-
downloaded = 0
|
|
84
|
-
with open(tmp, "wb") as f:
|
|
85
|
-
for chunk in resp.iter_bytes(chunk_size=8192):
|
|
86
|
-
f.write(chunk)
|
|
87
|
-
downloaded += len(chunk)
|
|
88
|
-
if total > 0:
|
|
89
|
-
pct = downloaded / total * 100
|
|
90
|
-
mb = downloaded / 1024 / 1024
|
|
91
|
-
from ._output import status_inline, status_end
|
|
92
|
-
status_inline(f" {mb:.1f} MB ({pct:.0f}%)")
|
|
93
|
-
from ._output import status_end
|
|
94
|
-
status_end()
|
|
95
|
-
os.replace(str(tmp), str(dest))
|
|
96
|
-
except BaseException:
|
|
97
|
-
tmp.unlink(missing_ok=True)
|
|
98
|
-
raise
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _slug_cache_path() -> Path:
|
|
102
|
-
return config.get_cache_dir() / "slug_cache.json"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def get_cached_slug(slug: str) -> str | None:
|
|
106
|
-
"""Look up a slug in the local cache. Returns program_id or None."""
|
|
107
|
-
path = _slug_cache_path()
|
|
108
|
-
if not path.exists():
|
|
109
|
-
return None
|
|
110
|
-
try:
|
|
111
|
-
data = json.loads(path.read_text())
|
|
112
|
-
program_id = data.get(slug)
|
|
113
|
-
if program_id and is_program_cached(program_id):
|
|
114
|
-
return program_id
|
|
115
|
-
except (json.JSONDecodeError, OSError):
|
|
116
|
-
pass
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def save_slug_mapping(slug: str, program_id: str) -> None:
|
|
121
|
-
"""Save a slug -> program_id mapping to the local cache."""
|
|
122
|
-
path = _slug_cache_path()
|
|
123
|
-
data: dict = {}
|
|
124
|
-
if path.exists():
|
|
125
|
-
try:
|
|
126
|
-
data = json.loads(path.read_text())
|
|
127
|
-
except (json.JSONDecodeError, OSError):
|
|
128
|
-
pass
|
|
129
|
-
data[slug] = program_id
|
|
130
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
-
path.write_text(json.dumps(data))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{programasweights-0.3.1 → programasweights-0.4.0}/programasweights/runtime/interpreter_onnx.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|