kylebrodeur commited on
Commit
22fe767
·
verified ·
1 Parent(s): 3b0bde6

deploy: update Space from deploy_preflight --push

Browse files
.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 pick_model(choice: str) -> str:
254
- """Model switcher. Live serves now; the trained LoRA / QAT variants are tracked
255
- in learn/finetune and are NOT swapped into the live runtime until a held-out eval
256
- earns it — surfaced honestly rather than faked (the project's honesty rule)."""
257
- if choice == "Live":
258
- return f"<div class='ce-sub' style='font-size:12px;'>MODEL · {llm.backend_status()}</div>"
259
- note = ("LoRA on the accumulated ledger" if choice == "LoRA"
260
- else "QAT (quantization-aware) variant")
261
- return ("<div class='ce-sub' style='font-size:12px;'>MODEL · "
262
- f"<span style='color:var(--ao-yellow);'>●</span> {choice} selected · {note} is "
263
- "<b>training on Modal</b>, not yet serving — Live model is active. "
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
- model_switch = gr.Radio(["Live", "LoRA", "QAT"], value="Live", show_label=False,
544
- elem_classes=["ce-seg"], scale=0)
 
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
- model_switch.change(pick_model, [model_switch], [model_status])
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, model_choice_state],
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 = "microfactory-node.gguf"
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 ./microfactory-node.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,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 microfactory-node -f Modelfile")
152
- print(f" ollama run microfactory-node")
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__":