@trillboards/edge-sdk 0.2.60 → 0.2.63
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.
- package/deploy/litertlm/test_trillboards_litertlm_shim.py +132 -0
- package/deploy/litertlm/trillboards-litertlm-shim.pth +1 -0
- package/deploy/litertlm/trillboards_litertlm_shim.py +214 -0
- package/deploy/macos/bench-local-vlm.sh +8 -4
- package/deploy/macos/build-macos-pkg.sh +4 -0
- package/deploy/macos/scripts/setup-litertlm.sh +210 -0
- package/deploy/macos/test-setup-litertlm-reinstall.sh +59 -1
- package/deploy/systemd/setup-litertlm.sh +218 -10
- package/deploy/windows/build-windows.ps1 +4 -0
- package/deploy/windows/register-litertlm-task.ps1 +22 -0
- package/deploy/windows/setup-litertlm.ps1 +228 -0
- package/deploy/windows/trillboards-edge.cmd +19 -0
- package/dist/CloudSensingManager.d.ts +2 -0
- package/dist/CloudSensingManager.d.ts.map +1 -1
- package/dist/EdgeAgent.js +1 -1
- package/dist/LiteRtVlmProvider.d.ts +74 -7
- package/dist/LiteRtVlmProvider.d.ts.map +1 -1
- package/dist/LiteRtVlmProvider.js.map +1 -1
- package/dist/analyzeCostObserver.d.ts +96 -0
- package/dist/analyzeCostObserver.d.ts.map +1 -0
- package/dist/analyzeCostObserver.js +130 -0
- package/dist/analyzeCostObserver.js.map +1 -0
- package/dist/connectorCadence.d.ts +120 -1
- package/dist/connectorCadence.d.ts.map +1 -1
- package/dist/connectorCadence.js +127 -2
- package/dist/connectorCadence.js.map +1 -1
- package/dist/deployedCadence.d.ts +13 -1
- package/dist/deployedCadence.d.ts.map +1 -1
- package/dist/deployedCadence.js +11 -2
- package/dist/deployedCadence.js.map +1 -1
- package/dist/deployedCadenceSource.d.ts +10 -0
- package/dist/deployedCadenceSource.d.ts.map +1 -1
- package/dist/deployedCadenceSource.js +11 -0
- package/dist/deployedCadenceSource.js.map +1 -1
- package/dist/diagnose.d.ts +1 -1
- package/dist/diagnose.js +1 -1
- package/dist/localInferenceSchema.d.ts +8 -3
- package/dist/localInferenceSchema.d.ts.map +1 -1
- package/dist/localInferenceSchema.js +8 -3
- package/dist/localInferenceSchema.js.map +1 -1
- package/dist/localSensing.d.ts +22 -1
- package/dist/localSensing.d.ts.map +1 -1
- package/dist/localSensing.js +36 -2
- package/dist/localSensing.js.map +1 -1
- package/dist/localVlmBench.d.ts +82 -0
- package/dist/localVlmBench.d.ts.map +1 -1
- package/dist/localVlmBench.js +88 -0
- package/dist/localVlmBench.js.map +1 -1
- package/dist/sensingRunMode.d.ts.map +1 -1
- package/dist/sensingRunMode.js +21 -2
- package/dist/sensingRunMode.js.map +1 -1
- package/package.json +1 -1
|
@@ -36,8 +36,18 @@ fi
|
|
|
36
36
|
exit 0
|
|
37
37
|
EOF
|
|
38
38
|
|
|
39
|
+
# Fake curl models BOTH endpoints the setup script uses:
|
|
40
|
+
# GET /v1/models -> the catalogue body `local_ai_healthy` greps
|
|
41
|
+
# POST /v1/chat/completions -> the vision probe, which asks for `%{http_code}`
|
|
42
|
+
# TRILLBOARDS_TEST_VISION_PROBE_CODE lets a test drive the GPU-vision fallback.
|
|
39
43
|
cat >"$FAKE_BIN/curl" <<'EOF'
|
|
40
44
|
#!/bin/bash
|
|
45
|
+
for arg in "$@"; do
|
|
46
|
+
if [ "$arg" = "-w" ]; then
|
|
47
|
+
printf '%s' "${TRILLBOARDS_TEST_VISION_PROBE_CODE:-200}"
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
41
51
|
printf '{"data":[{"id":"gemma-4-E2B-it.litertlm"}]}\n'
|
|
42
52
|
EOF
|
|
43
53
|
|
|
@@ -103,7 +113,55 @@ if ! OUTPUT="$(/bin/bash "$SETUP" 2>&1)"; then
|
|
|
103
113
|
fi
|
|
104
114
|
|
|
105
115
|
[ "$(cat "$TRILLBOARDS_TEST_STATE_DIR/bootstrap-attempts")" = "2" ]
|
|
116
|
+
# A host whose GPU vision encoder answers keeps it: config.json must carry
|
|
117
|
+
# vision_backend=gpu and must NOT have been rewritten by the fallback.
|
|
118
|
+
grep -Fq '"vision_backend":"gpu"' "$LITERT_LM_DIR/config.json"
|
|
119
|
+
grep -Fq '"speculative_decoding":true' "$LITERT_LM_DIR/config.json"
|
|
120
|
+
grep -Fq 'local AI healthy with the GPU vision encoder' <<<"$OUTPUT"
|
|
106
121
|
grep -Fq 'Bootstrap failed: 37: Operation already in progress' \
|
|
107
122
|
"$HOME/Library/Logs/Trillboards/litertlm-setup.log"
|
|
108
123
|
grep -Fq 'local AI is installed and healthy' <<<"$OUTPUT"
|
|
109
|
-
|
|
124
|
+
|
|
125
|
+
# -- Scenario 2: a host whose GPU vision encoder does NOT answer --------------
|
|
126
|
+
#
|
|
127
|
+
# This is the case the cascade exists for. Pinning the vision encoder to the GPU
|
|
128
|
+
# is the one setting here that can make a host WORSE than it was, and there is no
|
|
129
|
+
# demotion path for it: `recordGpuDemotion` re-posts the BARE model id, which
|
|
130
|
+
# moves the MAIN backend and leaves vision_backend untouched. So a box whose GPU
|
|
131
|
+
# will not bind must end this script serving on the CPU encoder, not dead.
|
|
132
|
+
#
|
|
133
|
+
# `local_ai_healthy` cannot catch it -- /v1/models answers 200 without ever
|
|
134
|
+
# building an engine. Only a real image inference can, which is why the probe
|
|
135
|
+
# POSTs one and why this test drives its status code.
|
|
136
|
+
printf 'loaded\n' >"$TRILLBOARDS_TEST_STATE_DIR/service-state"
|
|
137
|
+
printf '0\n' >"$TRILLBOARDS_TEST_STATE_DIR/bootstrap-attempts"
|
|
138
|
+
export TRILLBOARDS_TEST_VISION_PROBE_CODE=500
|
|
139
|
+
|
|
140
|
+
if ! OUTPUT2="$(/bin/bash "$SETUP" 2>&1)"; then
|
|
141
|
+
printf '%s\n' "$OUTPUT2" >&2
|
|
142
|
+
printf 'FAIL: setup exited non-zero when the GPU vision probe failed; it must fall back, not fail\n' >&2
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
grep -Fq 'falling back to the CPU encoder' <<<"$OUTPUT2"
|
|
147
|
+
grep -Fq 'local AI healthy on the CPU vision encoder' <<<"$OUTPUT2"
|
|
148
|
+
grep -Fq 'local AI is installed and healthy' <<<"$OUTPUT2"
|
|
149
|
+
|
|
150
|
+
# The fallback must REMOVE vision_backend and KEEP speculative_decoding: the
|
|
151
|
+
# latter is delegate independent and byte-identical to greedy, so it is still
|
|
152
|
+
# correct on a CPU-vision host and is the only speedup that host keeps.
|
|
153
|
+
if grep -Fq 'vision_backend' "$LITERT_LM_DIR/config.json"; then
|
|
154
|
+
printf 'FAIL: fallback left vision_backend in config.json\n' >&2
|
|
155
|
+
cat "$LITERT_LM_DIR/config.json" >&2
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
158
|
+
grep -Fq '"speculative_decoding":true' "$LITERT_LM_DIR/config.json"
|
|
159
|
+
|
|
160
|
+
# 2 bootstraps for the first start (attempt 1 hits the 37 race), + 1 for the
|
|
161
|
+
# restart the fallback performs.
|
|
162
|
+
[ "$(cat "$TRILLBOARDS_TEST_STATE_DIR/bootstrap-attempts")" = "3" ]
|
|
163
|
+
|
|
164
|
+
unset TRILLBOARDS_TEST_VISION_PROBE_CODE
|
|
165
|
+
|
|
166
|
+
printf 'macOS LiteRT-LM reinstall race + GPU-vision fallback regressions passed\n'
|
|
167
|
+
|
|
@@ -69,6 +69,182 @@ log() {
|
|
|
69
69
|
printf '[trillboards setup-litertlm] %s\n' "$*"
|
|
70
70
|
}
|
|
71
71
|
fail() { log "ERROR: $*"; exit 1; }
|
|
72
|
+
# -- Local-inference tuning (config.json) ------------------------------------
|
|
73
|
+
#
|
|
74
|
+
# `litert-lm serve` takes NO flags for any of this: `serve` accepts only
|
|
75
|
+
# --host/--port/--api/--cors-origin/--verbose. Every engine knob it honours comes
|
|
76
|
+
# from `$LITERT_LM_DIR/config.json`, which `litert_lm_cli.config.get_model_config`
|
|
77
|
+
# merges per model id. So this file IS the tuning surface for everything it can
|
|
78
|
+
# express, and the serve binary stays stock: we pass it no flags and we modify
|
|
79
|
+
# none of its files.
|
|
80
|
+
#
|
|
81
|
+
# ONE knob is NOT expressible there, and it is the largest one left. Every
|
|
82
|
+
# request the serve handles gets `ConstrainedDecodingConfig(enable=True, ...)`
|
|
83
|
+
# built for it unconditionally -- `enable` is True even when the caller sent no
|
|
84
|
+
# `response_format` and the generation is therefore unconstrained -- so the
|
|
85
|
+
# native conversation-create builds constraint machinery for a grammar nothing
|
|
86
|
+
# will use, once per frame. `config_schema.json` on the pinned 0.16.0 has 17
|
|
87
|
+
# keys (backend, cpu_thread_count, audio_backend, vision_backend, cache,
|
|
88
|
+
# max_num_tokens, temperature, top_p, top_k, seed, gpu_decode_steps_per_sync,
|
|
89
|
+
# speculative_decoding, thinking, thinking_budget, activation_data_type, plus
|
|
90
|
+
# the `default`/`models` containers) and NONE of them reaches it; no request
|
|
91
|
+
# field reaches it either, and both 0.15.0 and 0.16.1 construct it identically,
|
|
92
|
+
# so a version move does not reach it. That is why install_litertlm_shim below
|
|
93
|
+
# exists -- see it for the measurement and for why it is not a patch.
|
|
94
|
+
#
|
|
95
|
+
# Two settings, both MEASURED on an M4 Max (12P+4E/48G), litert-lm 0.16.0, Gemma
|
|
96
|
+
# 4 E2B, delegate asserted GPU from the native log, 16 warm-ups before timing,
|
|
97
|
+
# arms interleaved sample-by-sample, operator/SOP field set + the two committed
|
|
98
|
+
# CCTV fixtures:
|
|
99
|
+
#
|
|
100
|
+
# vision_backend=gpu The vision encoder ran on the CPU because NOTHING ASKED
|
|
101
|
+
# IT NOT TO -- `Engine(vision_backend=...)` defaults to
|
|
102
|
+
# None and the 0.13.1 HTTP shim hardcoded Backend.CPU()
|
|
103
|
+
# (upstream b/515805503). It is not a hardware limit and
|
|
104
|
+
# not a model constraint; GPU vision is Google's own
|
|
105
|
+
# reference config and CPU is their FALLBACK tier.
|
|
106
|
+
# Image encode 0.804 s -> 0.175 s p50, i.e. 76% of a SOP
|
|
107
|
+
# frame removed.
|
|
108
|
+
# speculative_decoding 1.433x on the decode-heavy audience schema, and the
|
|
109
|
+
# generations are BYTE-IDENTICAL to greedy (12/12 same
|
|
110
|
+
# text), which is the property that makes it safe: it is
|
|
111
|
+
# a speed change, not an answer change.
|
|
112
|
+
#
|
|
113
|
+
# Together, in-process: 1.054 s -> 0.385 s per SOP frame (2.765x paired, min
|
|
114
|
+
# 2.560 / max 2.919 over 16 rounds); 3,416 -> 9,295 analyses/hour. Through this
|
|
115
|
+
# stock serve over HTTP: 1752.5 ms -> 1005.8 ms p50 (1.742x), 12/12 schema-valid
|
|
116
|
+
# on BOTH arms. Accuracy is not traded for it: people_present 16/16 on both arms,
|
|
117
|
+
# person_count 8/16 on both, and where they differ the GPU encoder is CLOSER to
|
|
118
|
+
# the hand label (4 vs 3 against a truth of 5).
|
|
119
|
+
#
|
|
120
|
+
# WHY THE VERIFY BELOW EXISTS. Pinning the vision encoder to the GPU is the one
|
|
121
|
+
# setting here that can make a host WORSE than it is today: a box whose GPU will
|
|
122
|
+
# not bind fails engine init, and unlike the main backend there is no demotion
|
|
123
|
+
# path for it -- `LiteRtVlmProvider.recordGpuDemotion` re-posts the BARE model
|
|
124
|
+
# id, which changes the main backend and leaves this config untouched. So a
|
|
125
|
+
# blind write would brick sensing on exactly the weak hosts that need it most.
|
|
126
|
+
# We therefore do what Google's own cascade does: try GPU vision, prove it with
|
|
127
|
+
# one real image inference, and fall back to the CPU default if it does not
|
|
128
|
+
# answer.
|
|
129
|
+
CONFIG_JSON="$LITERT_DIR/config.json"
|
|
130
|
+
|
|
131
|
+
# A 223-byte 32x32 grey JPEG. Small enough to inline, real enough that decoding
|
|
132
|
+
# it exercises the vision encoder we are trying to prove.
|
|
133
|
+
LITERTLM_PROBE_JPEG_B64="/9j/4AAQSkZJRgABAgAAAQABAAD//gAPTGF2YzYzLjEuMTAxAP/bAEMACD4+ST5JVVVVVVVVZF1kaGhoZGRkZGhoaHBwcIODg3BwcGhocHB8fIODj5OPh4eDh5OTm5uburqystnZ4P/////EAEoAAQAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAgACADASIAAhEAAxEA/9oADAMBAAIRAxEAPwAAAAAAAAD/2Q=="
|
|
134
|
+
|
|
135
|
+
# Write config.json. $1 = "gpu" pins the vision encoder; empty leaves it on the
|
|
136
|
+
# CPU default. speculative_decoding is written EITHER WAY -- it is delegate
|
|
137
|
+
# independent and byte-identical, so it survives the fallback.
|
|
138
|
+
write_litertlm_config() {
|
|
139
|
+
_vision="$1"
|
|
140
|
+
mkdir -p "$LITERT_DIR" 2>/dev/null || true
|
|
141
|
+
if [ -n "$_vision" ]; then
|
|
142
|
+
printf '{"models":{"%s":{"vision_backend":"%s","speculative_decoding":true}}}\n' \
|
|
143
|
+
"$MODEL_ID" "$_vision" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
144
|
+
else
|
|
145
|
+
printf '{"models":{"%s":{"speculative_decoding":true}}}\n' \
|
|
146
|
+
"$MODEL_ID" > "$CONFIG_JSON.tmp" 2>/dev/null || return 1
|
|
147
|
+
fi
|
|
148
|
+
mv -f "$CONFIG_JSON.tmp" "$CONFIG_JSON" 2>/dev/null || return 1
|
|
149
|
+
log "wrote $CONFIG_JSON (vision_backend=${_vision:-cpu-default}, speculative_decoding=true)"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# -- The one knob config.json cannot express -----------------------------------
|
|
153
|
+
#
|
|
154
|
+
# `litert-lm serve` builds `ConstrainedDecodingConfig(enable=True, provider=...)`
|
|
155
|
+
# for EVERY request, at both openai_handler.py construction sites, with `enable`
|
|
156
|
+
# hardcoded True -- so a request that carried no `response_format`, which is
|
|
157
|
+
# every request the connector sends, still makes the native conversation-create
|
|
158
|
+
# build constraint machinery for a grammar nothing will use. It is rebuilt per
|
|
159
|
+
# request because the serve creates one conversation per request.
|
|
160
|
+
#
|
|
161
|
+
# MEASURED (M4 Max 16c/48G, litert-lm 0.16.0, Gemma 4 E2B, GPU/Metal, vision_backend
|
|
162
|
+
# =gpu + speculative_decoding=true, operator/SOP field set, the two committed CCTV
|
|
163
|
+
# fixtures, arms alternated sample-by-sample with the leading arm rotated each
|
|
164
|
+
# round, load average 22-25):
|
|
165
|
+
#
|
|
166
|
+
# in-process, one engine, n=10 pairs
|
|
167
|
+
# arm create_conversation send_message total
|
|
168
|
+
# stock 0.717 s 0.560 s 1.275 s
|
|
169
|
+
# without it 0.001 s 0.603 s 0.604 s
|
|
170
|
+
# paired 2.194x (min 2.040, max 2.284); generations BYTE-IDENTICAL 10/10;
|
|
171
|
+
# prefill 493 / decode 58 on both arms
|
|
172
|
+
#
|
|
173
|
+
# through two stock serves on two ports, interleaved, n=16 pairs
|
|
174
|
+
# 1.309 s -> 0.592 s p50 (p95 3.664 -> 0.965), paired 2.42x (min 2.174, max
|
|
175
|
+
# 4.657), 16/16 schema-valid and 16/16 byte-identical on both arms
|
|
176
|
+
#
|
|
177
|
+
# bench-local-vlm.mjs --arms gpu-sop, 16 warm-ups, n=8, back to back
|
|
178
|
+
# 1.32 s/frame (2,727 analyses/hour) -> 0.72 s/frame (5,000/hour), 8/8
|
|
179
|
+
# schema-valid and 83.3% field accuracy on BOTH arms
|
|
180
|
+
#
|
|
181
|
+
# A speed change, not an answer change -- the same bar speculative_decoding had
|
|
182
|
+
# to clear. The RATIO is itself load-dependent (3.734x at 0.0% CPU idle vs 2.42x
|
|
183
|
+
# at load 25, because the removed work is CPU-bound and the retained work is
|
|
184
|
+
# GPU-bound), so quote the near-idle figure.
|
|
185
|
+
#
|
|
186
|
+
# We do NOT patch upstream. Two files are ADDED to the litert-lm tool venv's
|
|
187
|
+
# site-packages: our shim, and a one-line .pth that imports it. Every upstream
|
|
188
|
+
# file stays byte-identical, no launch definition changes on any platform (the
|
|
189
|
+
# systemd unit's ExecStart is untouched), and a serve started by hand gets the
|
|
190
|
+
# fix too. The shim is lazy --
|
|
191
|
+
# it only registers an import hook, so `litert-lm list`/`--version`, which this
|
|
192
|
+
# script runs as health checks, do not gain a dylib load. It fails OPEN: any
|
|
193
|
+
# failure leaves stock behaviour and prints one line to the serve's own log, and
|
|
194
|
+
# `TRILLBOARDS_LITERTLM_SHIM=0` disables it without uninstalling anything.
|
|
195
|
+
#
|
|
196
|
+
# Runs UNCONDITIONALLY, never inside the install/repair branch: `uv tool install
|
|
197
|
+
# --force` rebuilds the venv and takes both files with it, and the healthy-CLI
|
|
198
|
+
# path skips that branch entirely -- so gating this behind it is exactly how a
|
|
199
|
+
# repaired-and-healthy machine would keep the slow serve while reporting fine.
|
|
200
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd || echo .)"
|
|
201
|
+
find_litertlm_shim() {
|
|
202
|
+
for _c in "$SCRIPT_DIR/trillboards_litertlm_shim.py" \
|
|
203
|
+
"$SCRIPT_DIR/../litertlm/trillboards_litertlm_shim.py" \
|
|
204
|
+
"$SCRIPT_DIR/../../litertlm/trillboards_litertlm_shim.py"; do
|
|
205
|
+
[ -f "$_c" ] && { printf '%s\n' "$_c"; return 0; }
|
|
206
|
+
done
|
|
207
|
+
return 1
|
|
208
|
+
}
|
|
209
|
+
install_litertlm_shim() {
|
|
210
|
+
_shim="$(find_litertlm_shim)" || { log "WARN: litert-lm shim source not found; serve keeps stock (slower) conversation setup"; return 1; }
|
|
211
|
+
_tooldir="$("$UV" tool dir 2>/dev/null | tr -d '\r')"
|
|
212
|
+
[ -n "$_tooldir" ] || { log "WARN: could not resolve \`uv tool dir\`; skipping litert-lm shim"; return 1; }
|
|
213
|
+
_py="$_tooldir/litert-lm/bin/python"
|
|
214
|
+
[ -x "$_py" ] || { log "WARN: litert-lm venv python missing at $_py; skipping shim"; return 1; }
|
|
215
|
+
_site="$("$_py" -c 'import sysconfig;print(sysconfig.get_paths()["purelib"])' 2>/dev/null | tr -d '\r')"
|
|
216
|
+
[ -n "$_site" ] && [ -d "$_site" ] || { log "WARN: could not resolve litert-lm site-packages; skipping shim"; return 1; }
|
|
217
|
+
cp -f "$_shim" "$_site/trillboards_litertlm_shim.py" 2>/dev/null \
|
|
218
|
+
|| { log "WARN: could not copy the litert-lm shim into $_site"; return 1; }
|
|
219
|
+
printf 'import trillboards_litertlm_shim\n' > "$_site/trillboards-litertlm-shim.pth" 2>/dev/null \
|
|
220
|
+
|| { log "WARN: could not write the litert-lm shim .pth"; return 1; }
|
|
221
|
+
# Capability is established by ATTEMPTING THE OPERATION. Importing litert_lm
|
|
222
|
+
# here is the only thing that proves the wrap actually landed on the class the
|
|
223
|
+
# serve will use; a file that merely exists proves nothing.
|
|
224
|
+
if "$_py" -c 'import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,"_trillboards_litertlm_shim",False) else 1)' >>"$LOG" 2>&1; then
|
|
225
|
+
log "litert-lm fast-conversation shim active in $_site"
|
|
226
|
+
return 0
|
|
227
|
+
fi
|
|
228
|
+
log "WARN: litert-lm shim did not take effect; serve keeps stock (slower) conversation setup"
|
|
229
|
+
return 1
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# One real image inference against the running serve. A 200 means the vision
|
|
233
|
+
# encoder loaded AND ran on whatever backend config.json just asked for.
|
|
234
|
+
# Deliberately NOT a /v1/models check: that answers 200 without ever building an
|
|
235
|
+
# engine, so it cannot tell a working GPU encoder from one that fails on first
|
|
236
|
+
# use.
|
|
237
|
+
verify_litertlm_vision() {
|
|
238
|
+
_body="$LITERT_DIR/.vision-probe.json"
|
|
239
|
+
printf '{"model":"%s,gpu","messages":[{"role":"user","content":[{"type":"text","text":"Reply with the single word: ok"},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,%s"}}]}],"max_completion_tokens":8,"temperature":0}' \
|
|
240
|
+
"$MODEL_ID" "$LITERTLM_PROBE_JPEG_B64" > "$_body" 2>/dev/null || return 1
|
|
241
|
+
_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 180 \
|
|
242
|
+
-X POST "http://127.0.0.1:$SERVE_PORT/v1/chat/completions" \
|
|
243
|
+
-H 'Content-Type: application/json' --data-binary @"$_body" 2>/dev/null)
|
|
244
|
+
rm -f "$_body" 2>/dev/null || true
|
|
245
|
+
[ "$_code" = "200" ]
|
|
246
|
+
}
|
|
247
|
+
|
|
72
248
|
local_ai_healthy() {
|
|
73
249
|
curl -fsS --max-time 3 "http://127.0.0.1:$SERVE_PORT/v1/models" 2>/dev/null \
|
|
74
250
|
| grep -Fq "$MODEL_ID"
|
|
@@ -145,6 +321,8 @@ else
|
|
|
145
321
|
log "litert-lm CLI repaired at $("$LITERTLM" --version 2>/dev/null | awk '{print $NF}')"
|
|
146
322
|
fi
|
|
147
323
|
|
|
324
|
+
install_litertlm_shim || true
|
|
325
|
+
|
|
148
326
|
# 3. Model. Idempotent: skip entirely if litert-lm already has it.
|
|
149
327
|
HAVE_MODEL=0
|
|
150
328
|
if "$LITERTLM" list 2>/dev/null | grep -q "$MODEL_ID"; then
|
|
@@ -172,25 +350,55 @@ fi
|
|
|
172
350
|
|
|
173
351
|
# 4. Serve unit. Installed by install.sh (which has sudo); here we only start it
|
|
174
352
|
# when we can, so a re-run from the service user still recovers a stopped serve.
|
|
175
|
-
|
|
353
|
+
restart_litertlm_serve() {
|
|
354
|
+
command -v systemctl >/dev/null 2>&1 || return 0
|
|
176
355
|
if systemctl list-unit-files "$SERVE_UNIT.service" >/dev/null 2>&1; then
|
|
177
356
|
systemctl restart "$SERVE_UNIT" >>"$LOG" 2>&1 \
|
|
178
357
|
|| sudo -n systemctl restart "$SERVE_UNIT" >>"$LOG" 2>&1 \
|
|
179
|
-
|| log "WARN: could not restart $SERVE_UNIT
|
|
358
|
+
|| log "WARN: could not restart $SERVE_UNIT - start it with: sudo systemctl restart $SERVE_UNIT"
|
|
180
359
|
else
|
|
181
360
|
log "WARN: $SERVE_UNIT.service is not installed yet (install.sh installs it)"
|
|
182
361
|
fi
|
|
183
|
-
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
wait_for_litertlm_health() {
|
|
365
|
+
log "waiting for local AI health check on :$SERVE_PORT"
|
|
366
|
+
for _attempt in $(seq 1 60); do
|
|
367
|
+
if local_ai_healthy; then return 0; fi
|
|
368
|
+
sleep 2
|
|
369
|
+
done
|
|
370
|
+
return 1
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# Ask for GPU vision BEFORE the restart so the engine builds with it. The engine
|
|
374
|
+
# is constructed lazily on the first inference, so config.json must be on disk
|
|
375
|
+
# before that request, not merely before the next boot.
|
|
376
|
+
write_litertlm_config "gpu" || log "WARN: could not write $CONFIG_JSON - the serve will use its defaults"
|
|
377
|
+
|
|
378
|
+
restart_litertlm_serve
|
|
184
379
|
|
|
185
|
-
log "waiting for local AI health check on :$SERVE_PORT"
|
|
186
380
|
HEALTHY=0
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
sleep 2
|
|
190
|
-
done
|
|
381
|
+
if wait_for_litertlm_health; then HEALTHY=1; fi
|
|
382
|
+
|
|
191
383
|
if [ "$HEALTHY" -eq 1 ]; then
|
|
192
|
-
|
|
193
|
-
|
|
384
|
+
# The cascade. `local_ai_healthy` only proves /v1/models answers, which it does
|
|
385
|
+
# without ever building an engine - so it CANNOT see a GPU vision encoder that
|
|
386
|
+
# fails on first use. One real image inference is the only thing that can.
|
|
387
|
+
if verify_litertlm_vision; then
|
|
388
|
+
log "local AI healthy with the GPU vision encoder - the connector will run ON-DEVICE"
|
|
389
|
+
else
|
|
390
|
+
log "WARN: GPU vision encoder did not answer a probe inference - falling back to the CPU encoder"
|
|
391
|
+
write_litertlm_config "" || log "WARN: could not rewrite $CONFIG_JSON"
|
|
392
|
+
restart_litertlm_serve
|
|
393
|
+
if wait_for_litertlm_health; then
|
|
394
|
+
log "local AI healthy on the CPU vision encoder (slower, but serving)"
|
|
395
|
+
else
|
|
396
|
+
HEALTHY=0
|
|
397
|
+
fi
|
|
398
|
+
fi
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
if [ "$HEALTHY" -ne 1 ]; then
|
|
194
402
|
# NOT fatal. auto-sense waits and beacons, so an install that gets this far
|
|
195
403
|
# with the model on disk recovers by itself once the serve comes up.
|
|
196
404
|
log "WARN: Gemma is installed but the serve did not answer yet; auto-sense will"
|
|
@@ -222,6 +222,10 @@ Log "Customer setup window smoke test passed"
|
|
|
222
222
|
# model path from the customer SFX and verifies local AI before pairing; the
|
|
223
223
|
# serve task keeps `litert-lm serve :9379` alive for local-sense.
|
|
224
224
|
Copy-Item -Force "$Here\setup-litertlm.ps1" "$Payload\bin\setup-litertlm.ps1"
|
|
225
|
+
# Staged NEXT TO setup-litertlm.ps1 because that is where Install-LitertlmShim
|
|
226
|
+
# looks first. Copied into the litert-lm tool venv's site-packages at
|
|
227
|
+
# provisioning time; nothing else reads it, and no upstream file is modified.
|
|
228
|
+
Copy-Item -Force "$Here\..\litertlm\trillboards_litertlm_shim.py" "$Payload\bin\trillboards_litertlm_shim.py"
|
|
225
229
|
Copy-Item -Force "$Here\register-litertlm-task.ps1" "$Payload\bin\register-litertlm-task.ps1"
|
|
226
230
|
Copy-Item -Force "$Here\check-feeds.js" "$Payload\bin\check-feeds.js"
|
|
227
231
|
|
|
@@ -26,6 +26,28 @@ $TaskName = "TrillboardsLiteRtLm"
|
|
|
26
26
|
$UserId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
27
27
|
# litert-lm serve --host 127.0.0.1 --port <port> (loopback only -- the connector
|
|
28
28
|
# is local; never expose the model server off-box).
|
|
29
|
+
#
|
|
30
|
+
# THE SERVE'S OWN LOG IS DISCARDED ON WINDOWS, AND ONLY ON WINDOWS. A Scheduled
|
|
31
|
+
# Task has no stdout, so everything `litert-lm serve` prints goes nowhere. The
|
|
32
|
+
# other two platforms keep it: the macOS LaunchAgent sets StandardOutPath /
|
|
33
|
+
# StandardErrorPath (com.trillboards.litertlm.plist), and the unit sets
|
|
34
|
+
# StandardOutput=journal (trillboards-litertlm.service). `litert-lm serve` has no
|
|
35
|
+
# --log-file of its own -- only --verbose -- so redirection is the only route.
|
|
36
|
+
#
|
|
37
|
+
# This is not cosmetic. On 2026-08-19 windows-latest could not complete a single
|
|
38
|
+
# frame on the shipped config (run 32227929611: both provisioner probes died with
|
|
39
|
+
# connection-level errors, then two 12-minute request timeouts), and the reason is
|
|
40
|
+
# STILL unknown for exactly this reason -- the one process that knows why wrote
|
|
41
|
+
# its explanation to a handle nobody was holding.
|
|
42
|
+
#
|
|
43
|
+
# DO NOT "fix" this by wrapping the action in cmd.exe /c "... >> log 2>&1". The
|
|
44
|
+
# stop path below filters Win32_Process on Name = python.exe|litert-lm.exe|
|
|
45
|
+
# pythonw.exe, so a cmd.exe parent is invisible to it and would be orphaned on
|
|
46
|
+
# every re-register, and Task Scheduler's restart-on-failure would then be
|
|
47
|
+
# watching the wrapper instead of the server. Capturing this needs either an
|
|
48
|
+
# upstream --log-file or a purpose-built launcher that keeps the image name, and
|
|
49
|
+
# it must be proved against the S4U/boot-trigger ladder (the 0.2.17 incident)
|
|
50
|
+
# before it ships.
|
|
29
51
|
$argument = 'serve --host 127.0.0.1 --port ' + $Port
|
|
30
52
|
$Action = New-ScheduledTaskAction -Execute $LitertLmPath -Argument $argument
|
|
31
53
|
|
|
@@ -133,6 +133,105 @@ function Test-ServeHealthy {
|
|
|
133
133
|
# that sentence stays gone rather than merely de-prioritised.
|
|
134
134
|
#
|
|
135
135
|
# Never throws: a diagnostic must not be able to fail an install.
|
|
136
|
+
# -- Local-inference tuning (config.json) ------------------------------------
|
|
137
|
+
#
|
|
138
|
+
# `litert-lm serve` takes NO flags for any of this: serve accepts only
|
|
139
|
+
# --host/--port/--api/--cors-origin/--verbose. Every engine knob it honours comes
|
|
140
|
+
# from $LITERT_LM_DIR\config.json, merged per model id by
|
|
141
|
+
# litert_lm_cli.config.get_model_config. So this file IS the tuning surface for
|
|
142
|
+
# everything it can express, and the serve binary stays stock: we pass it no
|
|
143
|
+
# flags and we modify none of its files.
|
|
144
|
+
#
|
|
145
|
+
# ONE knob is NOT expressible there, and it is the largest one left. Every
|
|
146
|
+
# request the serve handles gets ConstrainedDecodingConfig(enable=True, ...)
|
|
147
|
+
# built for it unconditionally -- enable is True even when the caller sent no
|
|
148
|
+
# response_format and the generation is therefore unconstrained -- so the native
|
|
149
|
+
# conversation-create builds constraint machinery for a grammar nothing will
|
|
150
|
+
# use, once per frame. config_schema.json on the pinned 0.16.0 has 17 keys and
|
|
151
|
+
# NONE reaches it; no request field reaches it either, and 0.15.0 and 0.16.1
|
|
152
|
+
# construct it identically, so a version move does not reach it. That is why
|
|
153
|
+
# Install-LitertlmShim below exists -- see it for the measurement.
|
|
154
|
+
#
|
|
155
|
+
# MEASURED (M4 Max 12P+4E/48G, litert-lm 0.16.0, Gemma 4 E2B, delegate asserted
|
|
156
|
+
# GPU from the native log, 16 warm-ups, arms interleaved, operator/SOP field set,
|
|
157
|
+
# the two committed CCTV fixtures): image encode 0.804 s -> 0.175 s p50 and the
|
|
158
|
+
# whole SOP frame 1.054 s -> 0.385 s (2.765x paired); through the stock serve
|
|
159
|
+
# over HTTP 1752.5 ms -> 1005.8 ms p50 (1.742x), 12/12 schema-valid on both arms.
|
|
160
|
+
# The vision encoder was on the CPU because NOTHING ASKED IT NOT TO -- GPU vision
|
|
161
|
+
# is Google's reference config and CPU is their documented fallback tier.
|
|
162
|
+
# speculative_decoding is written too: 1.433x on the decode-heavy schema with
|
|
163
|
+
# BYTE-IDENTICAL generations, so it is a speed change and not an answer change.
|
|
164
|
+
$script:LitertlmProbeJpegB64 = "/9j/4AAQSkZJRgABAgAAAQABAAD//gAPTGF2YzYzLjEuMTAxAP/bAEMACD4+ST5JVVVVVVVVZF1kaGhoZGRkZGhoaHBwcIODg3BwcGhocHB8fIODj5OPh4eDh5OTm5uburqystnZ4P/////EAEoAAQAAAAAAAAAAAAAAAAAAAAABAQAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/wAARCAAgACADASIAAhEAAxEA/9oADAMBAAIRAxEAPwAAAAAAAAD/2Q=="
|
|
165
|
+
|
|
166
|
+
function Get-LitertlmConfigPath { Join-Path $LitertDir "config.json" }
|
|
167
|
+
|
|
168
|
+
# $VisionBackend = "gpu" pins the vision encoder; "" leaves it on the CPU
|
|
169
|
+
# default. speculative_decoding is written EITHER WAY -- it is delegate
|
|
170
|
+
# independent, so it survives the fallback.
|
|
171
|
+
function Write-LitertlmConfig {
|
|
172
|
+
param([string]$VisionBackend = "")
|
|
173
|
+
try {
|
|
174
|
+
if (-not (Test-Path -LiteralPath $LitertDir)) {
|
|
175
|
+
New-Item -ItemType Directory -Force -Path $LitertDir | Out-Null
|
|
176
|
+
}
|
|
177
|
+
$model = [ordered]@{}
|
|
178
|
+
if ($VisionBackend) { $model["vision_backend"] = $VisionBackend }
|
|
179
|
+
$model["speculative_decoding"] = $true
|
|
180
|
+
$cfg = [ordered]@{ models = [ordered]@{ $ModelId = $model } }
|
|
181
|
+
$path = Get-LitertlmConfigPath
|
|
182
|
+
$tmp = "$path.tmp"
|
|
183
|
+
# UTF-8 WITHOUT a BOM, written explicitly rather than via -Encoding.
|
|
184
|
+
#
|
|
185
|
+
# `Set-Content -Encoding UTF8` emits a BOM on Windows PowerShell 5.1 (it is
|
|
186
|
+
# BOM-less only on PowerShell 7+), and this script runs under whichever host
|
|
187
|
+
# the installer happens to invoke. litert-lm parses this file with a plain
|
|
188
|
+
# `json.load`, which REFUSES a BOM:
|
|
189
|
+
# Failed to parse config.json: Unexpected UTF-8 BOM (decode using utf-8-sig)
|
|
190
|
+
# That is a 500 on EVERY inference, not a degraded mode -- and because
|
|
191
|
+
# /v1/models answers 200 without building an engine, the install still looked
|
|
192
|
+
# healthy. Measured on windows-latest 2026-08-19, run 32223243603: 0 frames
|
|
193
|
+
# out of a 3-arm matrix, every arm HTTP 500. The macOS and Linux
|
|
194
|
+
# provisioners write this file with `printf` and were never affected.
|
|
195
|
+
[System.IO.File]::WriteAllText(
|
|
196
|
+
$tmp,
|
|
197
|
+
($cfg | ConvertTo-Json -Depth 6),
|
|
198
|
+
(New-Object System.Text.UTF8Encoding($false)))
|
|
199
|
+
Move-Item -LiteralPath $tmp -Destination $path -Force
|
|
200
|
+
$label = if ($VisionBackend) { $VisionBackend } else { "cpu-default" }
|
|
201
|
+
Log "wrote $path (vision_backend=$label, speculative_decoding=true)"
|
|
202
|
+
return $true
|
|
203
|
+
} catch {
|
|
204
|
+
Log "WARN: could not write litert-lm config.json: $_"
|
|
205
|
+
return $false
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# One real image inference. A 200 means the vision encoder loaded AND ran on the
|
|
210
|
+
# backend config.json just asked for. Deliberately NOT Test-ServeHealthy:
|
|
211
|
+
# /v1/models answers without ever building an engine, so it cannot tell a working
|
|
212
|
+
# GPU encoder from one that fails on first use.
|
|
213
|
+
function Test-LitertlmVision {
|
|
214
|
+
try {
|
|
215
|
+
$dataUri = "data:image/jpeg;base64," + $script:LitertlmProbeJpegB64
|
|
216
|
+
$body = @{
|
|
217
|
+
model = "$ModelId,gpu"
|
|
218
|
+
messages = @(@{ role = "user"; content = @(
|
|
219
|
+
@{ type = "text"; text = "Reply with the single word: ok" },
|
|
220
|
+
@{ type = "image_url"; image_url = @{ url = $dataUri } }
|
|
221
|
+
)})
|
|
222
|
+
max_completion_tokens = 8
|
|
223
|
+
temperature = 0
|
|
224
|
+
} | ConvertTo-Json -Depth 8
|
|
225
|
+
$resp = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/v1/chat/completions" `
|
|
226
|
+
-Method POST -ContentType "application/json" -Body $body `
|
|
227
|
+
-TimeoutSec 180 -UseBasicParsing
|
|
228
|
+
return ($resp.StatusCode -eq 200)
|
|
229
|
+
} catch {
|
|
230
|
+
Log "vision probe did not answer: $_"
|
|
231
|
+
return $false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
136
235
|
function Get-AdvertisedModelIds {
|
|
137
236
|
try {
|
|
138
237
|
$response = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/v1/models" -UseBasicParsing -TimeoutSec 5
|
|
@@ -376,6 +475,90 @@ if ((Test-LitertLmCli) -and ($installedLitertLmVersion -eq $LitertLmVersion)) {
|
|
|
376
475
|
Log "litert-lm CLI repaired successfully at $(Get-LitertLmVersion)"
|
|
377
476
|
}
|
|
378
477
|
|
|
478
|
+
# -- The one knob config.json cannot express -----------------------------------
|
|
479
|
+
#
|
|
480
|
+
# `litert-lm serve` builds ConstrainedDecodingConfig(enable=True, provider=...)
|
|
481
|
+
# for EVERY request, at both openai_handler.py construction sites, with enable
|
|
482
|
+
# hardcoded True -- so a request that carried no response_format, which is every
|
|
483
|
+
# request the connector sends, still makes the native conversation-create build
|
|
484
|
+
# constraint machinery for a grammar nothing will use. It is rebuilt per request
|
|
485
|
+
# because the serve creates one conversation per request.
|
|
486
|
+
#
|
|
487
|
+
# MEASURED (M4 Max 16c/48G, litert-lm 0.16.0, Gemma 4 E2B, GPU/Metal, vision_backend
|
|
488
|
+
# =gpu + speculative_decoding=true, operator/SOP field set, the two committed CCTV
|
|
489
|
+
# fixtures, arms alternated sample-by-sample with the leading arm rotated each
|
|
490
|
+
# round, load average 22-25):
|
|
491
|
+
#
|
|
492
|
+
# in-process, one engine, n=10 pairs
|
|
493
|
+
# arm create_conversation send_message total
|
|
494
|
+
# stock 0.717 s 0.560 s 1.275 s
|
|
495
|
+
# without it 0.001 s 0.603 s 0.604 s
|
|
496
|
+
# paired 2.194x (min 2.040, max 2.284); generations BYTE-IDENTICAL 10/10;
|
|
497
|
+
# prefill 493 / decode 58 on both arms
|
|
498
|
+
#
|
|
499
|
+
# through two stock serves on two ports, interleaved, n=16 pairs
|
|
500
|
+
# 1.309 s -> 0.592 s p50 (p95 3.664 -> 0.965), paired 2.42x (min 2.174, max
|
|
501
|
+
# 4.657), 16/16 schema-valid and 16/16 byte-identical on both arms
|
|
502
|
+
#
|
|
503
|
+
# bench-local-vlm.mjs --arms gpu-sop, 16 warm-ups, n=8, back to back
|
|
504
|
+
# 1.32 s/frame (2,727 analyses/hour) -> 0.72 s/frame (5,000/hour), 8/8
|
|
505
|
+
# schema-valid and 83.3% field accuracy on BOTH arms
|
|
506
|
+
#
|
|
507
|
+
# A speed change, not an answer change -- the same bar speculative_decoding had
|
|
508
|
+
# to clear. The RATIO is itself load-dependent (3.734x at 0.0% CPU idle vs 2.42x
|
|
509
|
+
# at load 25, because the removed work is CPU-bound and the retained work is
|
|
510
|
+
# GPU-bound), so quote the near-idle figure.
|
|
511
|
+
#
|
|
512
|
+
# We do NOT patch upstream. Two files are ADDED to the litert-lm tool venv's
|
|
513
|
+
# site-packages: our shim, and a one-line .pth that imports it. Every upstream
|
|
514
|
+
# file stays byte-identical, the Scheduled Task's action is untouched (so the
|
|
515
|
+
# model OTA's `schtasks /End; /Run /TN TrillboardsLiteRtLm` still finds the same
|
|
516
|
+
# task), and a serve started by hand gets the fix too. It fails OPEN: any
|
|
517
|
+
# failure leaves stock behaviour and prints one line to the serve's own log, and
|
|
518
|
+
# TRILLBOARDS_LITERTLM_SHIM=0 disables it without uninstalling anything.
|
|
519
|
+
#
|
|
520
|
+
# Runs UNCONDITIONALLY, never inside the install/repair branch: `uv tool install
|
|
521
|
+
# --force` rebuilds the venv and takes both files with it, while Test-LitertLmCli
|
|
522
|
+
# short-circuits that branch on a healthy CLI -- gating this behind it is exactly
|
|
523
|
+
# how a repaired-and-healthy machine would keep the slow serve and report fine.
|
|
524
|
+
# Never throws: this is a speed fix, and it must not be able to fail an install.
|
|
525
|
+
function Install-LitertlmShim {
|
|
526
|
+
param([string]$UvPath)
|
|
527
|
+
try {
|
|
528
|
+
$shim = $null
|
|
529
|
+
foreach ($c in @(
|
|
530
|
+
(Join-Path $PSScriptRoot "trillboards_litertlm_shim.py"),
|
|
531
|
+
(Join-Path $PSScriptRoot "..\litertlm\trillboards_litertlm_shim.py")
|
|
532
|
+
)) { if (Test-Path -LiteralPath $c -PathType Leaf) { $shim = $c; break } }
|
|
533
|
+
if (-not $shim) { Log "WARN: litert-lm shim source not found; serve keeps stock (slower) conversation setup"; return $false }
|
|
534
|
+
|
|
535
|
+
$toolDir = (& $UvPath tool dir 2>$null | Select-Object -First 1)
|
|
536
|
+
if ([string]::IsNullOrWhiteSpace($toolDir)) { Log "WARN: could not resolve 'uv tool dir'; skipping litert-lm shim"; return $false }
|
|
537
|
+
$py = Join-Path $toolDir.Trim() "litert-lm\Scripts\python.exe"
|
|
538
|
+
if (-not (Test-Path -LiteralPath $py -PathType Leaf)) { Log "WARN: litert-lm venv python missing at $py; skipping shim"; return $false }
|
|
539
|
+
|
|
540
|
+
$site = (& $py -c "import sysconfig;print(sysconfig.get_paths()['purelib'])" 2>$null | Select-Object -First 1)
|
|
541
|
+
if ([string]::IsNullOrWhiteSpace($site) -or -not (Test-Path -LiteralPath $site.Trim())) {
|
|
542
|
+
Log "WARN: could not resolve litert-lm site-packages; skipping shim"; return $false
|
|
543
|
+
}
|
|
544
|
+
$site = $site.Trim()
|
|
545
|
+
Copy-Item -LiteralPath $shim -Destination (Join-Path $site "trillboards_litertlm_shim.py") -Force
|
|
546
|
+
Set-Content -LiteralPath (Join-Path $site "trillboards-litertlm-shim.pth") -Value "import trillboards_litertlm_shim" -Encoding ASCII
|
|
547
|
+
|
|
548
|
+
# Capability is established by ATTEMPTING THE OPERATION. Importing litert_lm
|
|
549
|
+
# here is the only thing that proves the wrap landed on the class the serve
|
|
550
|
+
# will use; a file that merely exists proves nothing.
|
|
551
|
+
& $py -c "import litert_lm,sys; sys.exit(0 if getattr(litert_lm.Engine.create_conversation,'_trillboards_litertlm_shim',False) else 1)" *>> $LogPath
|
|
552
|
+
if ($LASTEXITCODE -eq 0) { Log "litert-lm fast-conversation shim active in $site"; return $true }
|
|
553
|
+
Log "WARN: litert-lm shim did not take effect; serve keeps stock (slower) conversation setup"
|
|
554
|
+
return $false
|
|
555
|
+
} catch {
|
|
556
|
+
Log "WARN: litert-lm shim install failed: $_"
|
|
557
|
+
return $false
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
Install-LitertlmShim -UvPath $uvCmd.Source | Out-Null
|
|
561
|
+
|
|
379
562
|
$bundledModelVerified = $false
|
|
380
563
|
if (-not [string]::IsNullOrWhiteSpace($BundledModelPath)) {
|
|
381
564
|
if (-not (Test-Path -LiteralPath $BundledModelPath -PathType Leaf)) {
|
|
@@ -475,6 +658,10 @@ if (-not (Test-Path -LiteralPath $registration)) {
|
|
|
475
658
|
exit 1
|
|
476
659
|
}
|
|
477
660
|
|
|
661
|
+
# Ask for GPU vision BEFORE the serve starts: the engine is built lazily on the
|
|
662
|
+
# first inference, so config.json must be on disk before that request.
|
|
663
|
+
Write-LitertlmConfig -VisionBackend "gpu" | Out-Null
|
|
664
|
+
|
|
478
665
|
Log "registering litert-lm serve Scheduled Task on :$Port"
|
|
479
666
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $registration -LitertLmPath $litertlmPath -Port $Port *>> $LogPath
|
|
480
667
|
if ($LASTEXITCODE -ne 0) {
|
|
@@ -495,6 +682,47 @@ if (-not $healthy) {
|
|
|
495
682
|
exit 1
|
|
496
683
|
}
|
|
497
684
|
|
|
685
|
+
# The GPU-vision cascade. Health only proves /v1/models answers, and that route
|
|
686
|
+
# never builds an engine -- so it cannot see a GPU vision encoder that fails on
|
|
687
|
+
# first USE. If the probe inference does not answer we fall back to the CPU
|
|
688
|
+
# encoder rather than leave this host unable to sense at all.
|
|
689
|
+
if (-not (Test-LitertlmVision)) {
|
|
690
|
+
Log "WARN: GPU vision encoder did not answer a probe inference; falling back to the CPU encoder"
|
|
691
|
+
Write-LitertlmConfig | Out-Null
|
|
692
|
+
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $registration -LitertLmPath $litertlmPath -Port $Port *>> $LogPath
|
|
693
|
+
$healthy = $false
|
|
694
|
+
for ($attempt = 0; $attempt -lt 60 -and -not $healthy; $attempt++) {
|
|
695
|
+
$healthy = Test-ServeHealthy
|
|
696
|
+
if (-not $healthy) { Start-Sleep -Seconds 2 }
|
|
697
|
+
}
|
|
698
|
+
if (-not $healthy) {
|
|
699
|
+
Fail-LocalAi "Gemma is installed, but the local AI service did not respond. Restart Windows, then retry Local AI."
|
|
700
|
+
Stop-SetupLock
|
|
701
|
+
exit 1
|
|
702
|
+
}
|
|
703
|
+
# RE-PROBE, with the same probe that sent us here.
|
|
704
|
+
#
|
|
705
|
+
# Test-ServeHealthy above only proves /v1/models answers, and this file's own
|
|
706
|
+
# comment six lines up says that route never builds an engine. So before this
|
|
707
|
+
# check existed, a CPU fallback that ALSO could not infer logged "healthy on
|
|
708
|
+
# the CPU vision encoder (slower, but serving)" and exited 0 on a machine where
|
|
709
|
+
# every frame 500s. That is exactly how the UTF-8 BOM above shipped as a GREEN
|
|
710
|
+
# install: the cascade detected the failure, downgraded a tier, and then
|
|
711
|
+
# verified the downgrade with a check that cannot fail for this reason.
|
|
712
|
+
#
|
|
713
|
+
# A CPU fallback that cannot infer is not a slower tier. It is a broken
|
|
714
|
+
# install, and the operator has to hear so at setup time rather than discover
|
|
715
|
+
# it as silence from a camera weeks later.
|
|
716
|
+
if (-not (Test-LitertlmVision)) {
|
|
717
|
+
Fail-LocalAi "Gemma is installed and the local AI service is answering, but it could not analyze a test image on either the GPU or the CPU vision encoder. See $LogPath."
|
|
718
|
+
Stop-SetupLock
|
|
719
|
+
exit 1
|
|
720
|
+
}
|
|
721
|
+
Log "local AI healthy on the CPU vision encoder (slower, but serving)"
|
|
722
|
+
} else {
|
|
723
|
+
Log "local AI healthy with the GPU vision encoder"
|
|
724
|
+
}
|
|
725
|
+
|
|
498
726
|
Complete-Setup
|
|
499
727
|
Stop-SetupLock
|
|
500
728
|
exit 0
|
|
@@ -19,6 +19,25 @@ if /I "%~1"=="auto-sense" (
|
|
|
19
19
|
if errorlevel 1 set "NEEDS_LOCAL_AI=1"
|
|
20
20
|
curl.exe --fail --silent --max-time 2 http://127.0.0.1:9379/v1/models >nul 2>&1
|
|
21
21
|
if errorlevel 1 set "NEEDS_LOCAL_AI=1"
|
|
22
|
+
REM A CONFIG litert-lm CANNOT PARSE IS INVISIBLE TO EVERY PROBE ABOVE. The
|
|
23
|
+
REM model is installed, the task exists, and /v1/models answers 200 -- that
|
|
24
|
+
REM route never builds an engine -- while EVERY inference returns 500. That is
|
|
25
|
+
REM exactly what shipped on 2026-08-19: setup-litertlm.ps1 wrote config.json
|
|
26
|
+
REM with `Set-Content -Encoding UTF8`, which is BOM-ful on Windows PowerShell
|
|
27
|
+
REM 5.1, and litert-lm parses the file with a plain json.load that refuses a
|
|
28
|
+
REM BOM. Without this check the repaired provisioner would land by auto-update
|
|
29
|
+
REM and never re-run, so the fix would reach NEW INSTALLS ONLY and every
|
|
30
|
+
REM already-broken box would stay broken forever while reporting healthy.
|
|
31
|
+
REM
|
|
32
|
+
REM Byte comparison, not a parse, and the BOM test comes FIRST: PowerShell's
|
|
33
|
+
REM ConvertFrom-Json happily accepts a BOM, so a parse alone would call the
|
|
34
|
+
REM broken file good. Self-clearing -- once the provisioner rewrites the file
|
|
35
|
+
REM without a BOM this never fires again -- and it fails toward REPAIR, which
|
|
36
|
+
REM is the safe direction for a box that cannot sense.
|
|
37
|
+
if exist "!LITERT_ROOT!\config.json" (
|
|
38
|
+
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "try { $p = '!LITERT_ROOT!\config.json'; $b = [System.IO.File]::ReadAllBytes($p); if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) { exit 1 }; $t = [System.Text.Encoding]::UTF8.GetString($b).Trim(); if ($t.Length -gt 0) { [void]($t | ConvertFrom-Json) }; exit 0 } catch { exit 1 }" >nul 2>&1
|
|
39
|
+
if errorlevel 1 set "NEEDS_LOCAL_AI=1"
|
|
40
|
+
)
|
|
22
41
|
REM Launch the repair through the wscript host, NOT `start`. cmd's `start`
|
|
23
42
|
REM allocates a NEW console for the child before PowerShell can hide itself,
|
|
24
43
|
REM so the customer saw a black window on exactly the reboot that needs a
|