deploy: update Space from deploy_preflight --push
Browse files- .codeboarding/logs/wrapper-server.log +10 -0
- app.py +21 -18
- learn/finetune/SERVING.md +37 -0
- learn/finetune/gguf_pipeline_modal.py +9 -9
.codeboarding/logs/wrapper-server.log
CHANGED
|
@@ -112,3 +112,13 @@ INFO: 127.0.0.1:44498 - "GET /health HTTP/1.1" 200 OK
|
|
| 112 |
[stderr] 2026-06-14 04:55:32 INFO [watchfiles.main:308] 1 change detected
|
| 113 |
[stderr] 2026-06-14 04:55:32 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 114 |
[stderr] 2026-06-14 04:55:32 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
[stderr] 2026-06-14 04:55:32 INFO [watchfiles.main:308] 1 change detected
|
| 113 |
[stderr] 2026-06-14 04:55:32 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 114 |
[stderr] 2026-06-14 04:55:32 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 115 |
+
[stderr] 2026-06-14 04:56:51 INFO [watchfiles.main:308] 1 change detected
|
| 116 |
+
[stderr] 2026-06-14 04:56:52 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 117 |
+
[stderr] 2026-06-14 04:56:52 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 118 |
+
[stderr] 2026-06-14 04:57:07 INFO [watchfiles.main:308] 1 change detected
|
| 119 |
+
[stderr] 2026-06-14 04:57:07 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 120 |
+
[stderr] 2026-06-14 04:57:07 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
| 121 |
+
[stderr] 2026-06-14 04:57:18 INFO [watchfiles.main:308] 1 change detected
|
| 122 |
+
[stderr] 2026-06-14 04:57:18 INFO [watchfiles.main:308] 2 changes detected
|
| 123 |
+
[stderr] 2026-06-14 04:57:18 WARNING [codeboarding_pro.analysis_controller:506] Skipping incremental update: full analysis artifacts not ready yet
|
| 124 |
+
[stderr] 2026-06-14 04:57:18 INFO [codeboarding_pro.analysis_controller:314] No cached analysis results; skipping health report
|
app.py
CHANGED
|
@@ -90,6 +90,7 @@ MODEL_OPTIONS = [
|
|
| 90 |
"LoRA v2 (Standard E4B)",
|
| 91 |
"Base (Gemma 4 E4B)",
|
| 92 |
"Modal API (remote)",
|
|
|
|
| 93 |
]
|
| 94 |
|
| 95 |
MODEL_LORA_MAP = {
|
|
@@ -250,18 +251,17 @@ def warm_up_cb() -> str:
|
|
| 250 |
return f"<div class='ce-sub' style='font-size:12px;'>MODEL · {llm.warm_up()}</div>"
|
| 251 |
|
| 252 |
|
| 253 |
-
def
|
| 254 |
-
"""Model switcher
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
"See learn/finetune.</div>")
|
| 265 |
|
| 266 |
|
| 267 |
# ── simulated environment (this is a sim lab — conditions are generated, overridable) ──
|
|
@@ -540,14 +540,18 @@ def build() -> gr.Blocks:
|
|
| 540 |
gr.HTML(command_bar(llm.backend_status()))
|
| 541 |
# header row: model switcher + warm-up + live status (studio-16/18)
|
| 542 |
with gr.Row(elem_id="ce-modelswitch"):
|
| 543 |
-
|
| 544 |
-
|
|
|
|
| 545 |
warm_btn = gr.Button("WARM UP MODEL", elem_classes=["ce-pillbtn"], scale=0)
|
| 546 |
model_status = gr.HTML(_status_html())
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
|
| 548 |
state = gr.State()
|
| 549 |
part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})
|
| 550 |
-
model_choice_state = gr.State("LoRA v3 (QAT E4B)") # hidden, UI agent will replace with dropdown
|
| 551 |
|
| 552 |
with gr.Tabs() as tabs:
|
| 553 |
# ───────────────────────── STUDIO · define the job ───────────────────────
|
|
@@ -712,7 +716,7 @@ def build() -> gr.Blocks:
|
|
| 712 |
|
| 713 |
# Model warm-up + switcher
|
| 714 |
warm_btn.click(warm_up_pending, None, [model_status]).then(warm_up_cb, None, [model_status])
|
| 715 |
-
|
| 716 |
|
| 717 |
# Simulated environment: roll on load, re-roll on demand, keep readout + footer in sync.
|
| 718 |
sensor_outs = [temp, humidity, bed_position, sensors_readout]
|
|
@@ -731,10 +735,9 @@ def build() -> gr.Blocks:
|
|
| 731 |
build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
|
| 732 |
gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer,
|
| 733 |
build_loader, build_results]
|
| 734 |
-
# NOTE: model_choice dropdown will be added by UI agent. Currently defaults to "LoRA v3 (QAT E4B)".
|
| 735 |
run_btn.click(build_start, [part], build_start_outs).then(
|
| 736 |
None, None, None, js=_SCROLL_TOP).then(
|
| 737 |
-
build_job, [part, material, description, temp, humidity, bed_position,
|
| 738 |
build_outs).then(job_readout, [state], [p_job])
|
| 739 |
vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
|
| 740 |
read_toggle.change(toggle_read, [read_toggle, state],
|
|
|
|
| 90 |
"LoRA v2 (Standard E4B)",
|
| 91 |
"Base (Gemma 4 E4B)",
|
| 92 |
"Modal API (remote)",
|
| 93 |
+
# Future: "Ollama v3 (local GGUF)", "Ollama v2 (local GGUF)" — once GGUF files are pushed to Ollama.com
|
| 94 |
]
|
| 95 |
|
| 96 |
MODEL_LORA_MAP = {
|
|
|
|
| 251 |
return f"<div class='ce-sub' style='font-size:12px;'>MODEL · {llm.warm_up()}</div>"
|
| 252 |
|
| 253 |
|
| 254 |
+
def select_model(choice: str) -> str:
|
| 255 |
+
"""Model switcher (backend by learn/finetune via _apply_model_choice). Applies the
|
| 256 |
+
chosen backend, then reports the model actually serving. The LoRA v2/v3 adapters serve
|
| 257 |
+
through ZeroGPU on the Space (and fall back gracefully if the GPU/deps aren't present);
|
| 258 |
+
Base is the stock Gemma 4 E4B; Modal API is the remote endpoint."""
|
| 259 |
+
choice = choice or MODEL_OPTIONS[0]
|
| 260 |
+
try:
|
| 261 |
+
_apply_model_choice(choice)
|
| 262 |
+
except Exception:
|
| 263 |
+
pass
|
| 264 |
+
return f"<div class='ce-sub' style='font-size:12px;'>MODEL · {choice} · {llm.backend_status()}</div>"
|
|
|
|
| 265 |
|
| 266 |
|
| 267 |
# ── simulated environment (this is a sim lab — conditions are generated, overridable) ──
|
|
|
|
| 540 |
gr.HTML(command_bar(llm.backend_status()))
|
| 541 |
# header row: model switcher + warm-up + live status (studio-16/18)
|
| 542 |
with gr.Row(elem_id="ce-modelswitch"):
|
| 543 |
+
model_select = gr.Dropdown(MODEL_OPTIONS, value=MODEL_OPTIONS[0],
|
| 544 |
+
show_label=False, container=False,
|
| 545 |
+
elem_classes=["ce-modeldd"], scale=0)
|
| 546 |
warm_btn = gr.Button("WARM UP MODEL", elem_classes=["ce-pillbtn"], scale=0)
|
| 547 |
model_status = gr.HTML(_status_html())
|
| 548 |
+
gr.HTML("<div class='ce-sub' style='font-size:11px;'>Model · the LoRA v2/v3 adapters serve "
|
| 549 |
+
"via ZeroGPU on the Space; Base is the stock Gemma 4 E4B; Modal API is the remote "
|
| 550 |
+
"endpoint. Local users: pull the LoRA from HF Hub "
|
| 551 |
+
"(kylebrodeur/microfactory-node-lora-v2 · -v3-qat) or via ollama.</div>")
|
| 552 |
|
| 553 |
state = gr.State()
|
| 554 |
part = gr.State({"geometry": None, "mesh": None, "label": None, "read": None})
|
|
|
|
| 555 |
|
| 556 |
with gr.Tabs() as tabs:
|
| 557 |
# ───────────────────────── STUDIO · define the job ───────────────────────
|
|
|
|
| 716 |
|
| 717 |
# Model warm-up + switcher
|
| 718 |
warm_btn.click(warm_up_pending, None, [model_status]).then(warm_up_cb, None, [model_status])
|
| 719 |
+
model_select.change(select_model, [model_select], [model_status])
|
| 720 |
|
| 721 |
# Simulated environment: roll on load, re-roll on demand, keep readout + footer in sync.
|
| 722 |
sensor_outs = [temp, humidity, bed_position, sensors_readout]
|
|
|
|
| 735 |
build_outs = [backend, precedent, reasoning, risks, settings_html, spine_notes,
|
| 736 |
gcode_html, confirm_btn, node_cards, state, vprint, vp_slider, vp_layer,
|
| 737 |
build_loader, build_results]
|
|
|
|
| 738 |
run_btn.click(build_start, [part], build_start_outs).then(
|
| 739 |
None, None, None, js=_SCROLL_TOP).then(
|
| 740 |
+
build_job, [part, material, description, temp, humidity, bed_position, model_select],
|
| 741 |
build_outs).then(job_readout, [state], [p_job])
|
| 742 |
vp_slider.change(scrub_layer, [vp_slider, part], [vp_layer])
|
| 743 |
read_toggle.change(toggle_read, [read_toggle, state],
|
learn/finetune/SERVING.md
CHANGED
|
@@ -60,6 +60,43 @@ Path B (Ollama ADAPTER command) because:
|
|
| 60 |
- `convert_lora_to_gguf.py` compatibility with Gemma 4 not tested
|
| 61 |
- Merge→GGUF is the most battle-tested path
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
---
|
| 64 |
|
| 65 |
## 2. Modal Model Hosting — Implemented
|
|
|
|
| 60 |
- `convert_lora_to_gguf.py` compatibility with Gemma 4 not tested
|
| 61 |
- Merge→GGUF is the most battle-tested path
|
| 62 |
|
| 63 |
+
### Quantization
|
| 64 |
+
|
| 65 |
+
The GGUF pipeline uses `--outtype q4_k_m` by default:
|
| 66 |
+
- **Does NOT reduce parameter count**: 8B params stay 8B. Quantization reduces
|
| 67 |
+
weight precision from 16-bit (bf16) to 4-bit per weight.
|
| 68 |
+
- **File size**: ~5-6GB for 8B model (vs ~16GB for bf16 safetensors)
|
| 69 |
+
- **Quality**: q4_k_m is the recommended balance of size vs quality
|
| 70 |
+
- **v3 advantage**: QAT-trained model retains more quality after quantization
|
| 71 |
+
because it was trained with simulated quantization during fine-tuning
|
| 72 |
+
|
| 73 |
+
Other quantization options (pass `--outtype` to override):
|
| 74 |
+
| Format | Size (8B) | Quality | Use Case |
|
| 75 |
+
|--------|-----------|---------|----------|
|
| 76 |
+
| q8_0 | ~8.5GB | Near-lossless | Max quality, local GPU |
|
| 77 |
+
| q4_k_m | ~5.5GB | Good | Balanced (default) |
|
| 78 |
+
| q4_k_s | ~5.0GB | Slightly lower | Tight storage |
|
| 79 |
+
| q4_0 | ~4.5GB | Lower | QAT-optimized (Google's QAT Q4_0 target) |
|
| 80 |
+
|
| 81 |
+
### Parallel GGUF runs
|
| 82 |
+
Both tracks can run simultaneously — the pipeline uses separate Modal functions
|
| 83 |
+
(merge on GPU, convert on CPU) and writes distinct output filenames via `--name`:
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
# Terminal 1 — v2:
|
| 87 |
+
modal run learn/finetune/gguf_pipeline_modal.py \
|
| 88 |
+
--adapter kylebrodeur/microfactory-node-lora-v2 \
|
| 89 |
+
--name microfactory-node-v2
|
| 90 |
+
|
| 91 |
+
# Terminal 2 — v3 (simultaneously):
|
| 92 |
+
modal run learn/finetune/gguf_pipeline_modal.py \
|
| 93 |
+
--base google/gemma-4-E4B-it-qat-q4_0-unquantized \
|
| 94 |
+
--adapter kylebrodeur/microfactory-node-lora-v3-qat \
|
| 95 |
+
--name microfactory-node-v3-qat
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Download both: `modal volume get microfactory-node-finetune gguf/ --force`
|
| 99 |
+
|
| 100 |
---
|
| 101 |
|
| 102 |
## 2. Modal Model Hosting — Implemented
|
learn/finetune/gguf_pipeline_modal.py
CHANGED
|
@@ -87,11 +87,11 @@ if modal is not None:
|
|
| 87 |
@app.function(image=cpu_image, timeout=3600,
|
| 88 |
volumes={"/out": vol},
|
| 89 |
secrets=[modal.Secret.from_name("chief-engineer-secrets")])
|
| 90 |
-
def convert_to_gguf(merged_path: str, outtype: str = "q4_k_m") -> str:
|
| 91 |
"""Convert merged HF model to GGUF using llama.cpp."""
|
| 92 |
import subprocess
|
| 93 |
|
| 94 |
-
out_name = "
|
| 95 |
out_path = f"/out/gguf/{out_name}"
|
| 96 |
os.makedirs("/out/gguf", exist_ok=True)
|
| 97 |
|
|
@@ -115,14 +115,14 @@ if modal is not None:
|
|
| 115 |
|
| 116 |
@app.local_entrypoint()
|
| 117 |
def main(base: str = BASE_MODEL, adapter: str = "",
|
| 118 |
-
outtype: str = "q4_k_m"):
|
| 119 |
if not adapter:
|
| 120 |
print("ERROR: --adapter required. Example:")
|
| 121 |
-
print(" modal run learn/finetune/gguf_pipeline_modal.py --adapter kylebrodeur/microfactory-node-lora-v2")
|
| 122 |
return
|
| 123 |
|
| 124 |
print(f"=== GGUF Pipeline: {adapter} ===")
|
| 125 |
-
print(f"Base: {base} | Outtype: {outtype}")
|
| 126 |
|
| 127 |
# Step 1: Merge on GPU
|
| 128 |
print("\n--- Step 1: Merge LoRA (GPU) ---")
|
|
@@ -131,14 +131,14 @@ if modal is not None:
|
|
| 131 |
|
| 132 |
# Step 2: Convert to GGUF on CPU
|
| 133 |
print("\n--- Step 2: Convert to GGUF (CPU) ---")
|
| 134 |
-
gguf_path = convert_to_gguf.remote(merged_path, outtype)
|
| 135 |
print(f"\n=== PIPELINE COMPLETE ===")
|
| 136 |
print(f"GGUF file: {gguf_path}")
|
| 137 |
print(f"\nDownload:")
|
| 138 |
print(f" modal volume get microfactory-node-finetune gguf/ --force")
|
| 139 |
print(f"\nOllama import:")
|
| 140 |
print(f" cat > Modelfile << 'EOF'")
|
| 141 |
-
print(f" FROM ./
|
| 142 |
print(f' TEMPLATE """{{{{ if .System }}}}<start_of_turn>system')
|
| 143 |
print(f' {{{{ .System }}}}<end_of_turn>')
|
| 144 |
print(f' {{{{ end }}}}<start_of_turn>user')
|
|
@@ -148,8 +148,8 @@ if modal is not None:
|
|
| 148 |
print(f' PARAMETER stop "<start_of_turn>user"')
|
| 149 |
print(f' PARAMETER stop "<end_of_turn>"')
|
| 150 |
print(f' EOF')
|
| 151 |
-
print(f" ollama create
|
| 152 |
-
print(f" ollama run
|
| 153 |
|
| 154 |
|
| 155 |
if __name__ == "__main__":
|
|
|
|
| 87 |
@app.function(image=cpu_image, timeout=3600,
|
| 88 |
volumes={"/out": vol},
|
| 89 |
secrets=[modal.Secret.from_name("chief-engineer-secrets")])
|
| 90 |
+
def convert_to_gguf(merged_path: str, outtype: str = "q4_k_m", name: str = "microfactory-node") -> str:
|
| 91 |
"""Convert merged HF model to GGUF using llama.cpp."""
|
| 92 |
import subprocess
|
| 93 |
|
| 94 |
+
out_name = f"{name}.gguf"
|
| 95 |
out_path = f"/out/gguf/{out_name}"
|
| 96 |
os.makedirs("/out/gguf", exist_ok=True)
|
| 97 |
|
|
|
|
| 115 |
|
| 116 |
@app.local_entrypoint()
|
| 117 |
def main(base: str = BASE_MODEL, adapter: str = "",
|
| 118 |
+
outtype: str = "q4_k_m", name: str = "microfactory-node"):
|
| 119 |
if not adapter:
|
| 120 |
print("ERROR: --adapter required. Example:")
|
| 121 |
+
print(" modal run learn/finetune/gguf_pipeline_modal.py --adapter kylebrodeur/microfactory-node-lora-v2 --name microfactory-node-v2")
|
| 122 |
return
|
| 123 |
|
| 124 |
print(f"=== GGUF Pipeline: {adapter} ===")
|
| 125 |
+
print(f"Base: {base} | Outtype: {outtype} | Name: {name}")
|
| 126 |
|
| 127 |
# Step 1: Merge on GPU
|
| 128 |
print("\n--- Step 1: Merge LoRA (GPU) ---")
|
|
|
|
| 131 |
|
| 132 |
# Step 2: Convert to GGUF on CPU
|
| 133 |
print("\n--- Step 2: Convert to GGUF (CPU) ---")
|
| 134 |
+
gguf_path = convert_to_gguf.remote(merged_path, outtype, name)
|
| 135 |
print(f"\n=== PIPELINE COMPLETE ===")
|
| 136 |
print(f"GGUF file: {gguf_path}")
|
| 137 |
print(f"\nDownload:")
|
| 138 |
print(f" modal volume get microfactory-node-finetune gguf/ --force")
|
| 139 |
print(f"\nOllama import:")
|
| 140 |
print(f" cat > Modelfile << 'EOF'")
|
| 141 |
+
print(f" FROM ./{name}.gguf")
|
| 142 |
print(f' TEMPLATE """{{{{ if .System }}}}<start_of_turn>system')
|
| 143 |
print(f' {{{{ .System }}}}<end_of_turn>')
|
| 144 |
print(f' {{{{ end }}}}<start_of_turn>user')
|
|
|
|
| 148 |
print(f' PARAMETER stop "<start_of_turn>user"')
|
| 149 |
print(f' PARAMETER stop "<end_of_turn>"')
|
| 150 |
print(f' EOF')
|
| 151 |
+
print(f" ollama create {name} -f Modelfile")
|
| 152 |
+
print(f" ollama run {name}")
|
| 153 |
|
| 154 |
|
| 155 |
if __name__ == "__main__":
|