Pin the keyword models to one thread each and ship them (V-487)
The gate loaded and worked on workpc and took mavwaked from 68% of one core to 335%. onnxruntime sizes its intra-op pool to every core and spins between runs, which an always-on gate scoring three graphs twelve times a second provokes for the whole day. One thread per session brings it to 81%, so the keyword costs about 13% of a core, and each graph still finishes well inside its 80ms. The unit now passes the three -wake- flags and the models sit beside silero_vad.onnx in ~/.local/share/maven/models. The threshold is left at the binary's default so there is one place to change it.
This commit is contained in:
@@ -71,15 +71,30 @@ func newWakeModels(melPath, embedPath, headPath, libPath string) (*wakeModels, e
|
|||||||
return nil, fmt.Errorf("wake word: onnx runtime: %w", err)
|
return nil, fmt.Errorf("wake word: onnx runtime: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// One thread per session, not the default of every core. Measured on
|
||||||
|
// workpc: the default took mavwaked from 68% of one core to 335% of
|
||||||
|
// three, for three graphs that each run in well under 80ms single
|
||||||
|
// threaded. An always-on gate that eats a quarter of the workstation is
|
||||||
|
// not a gate he will leave running.
|
||||||
|
opts, err := ort.NewSessionOptions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wake word: session options: %w", err)
|
||||||
|
}
|
||||||
|
defer opts.Destroy()
|
||||||
|
if err := opts.SetIntraOpNumThreads(1); err != nil {
|
||||||
|
return nil, fmt.Errorf("wake word: intra-op threads: %w", err)
|
||||||
|
}
|
||||||
|
if err := opts.SetInterOpNumThreads(1); err != nil {
|
||||||
|
return nil, fmt.Errorf("wake word: inter-op threads: %w", err)
|
||||||
|
}
|
||||||
open := func(p string, in, out []string) (*ort.DynamicAdvancedSession, error) {
|
open := func(p string, in, out []string) (*ort.DynamicAdvancedSession, error) {
|
||||||
s, err := ort.NewDynamicAdvancedSession(p, in, out, nil)
|
s, err := ort.NewDynamicAdvancedSession(p, in, out, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("wake word: load %s: %w", p, err)
|
return nil, fmt.Errorf("wake word: load %s: %w", p, err)
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
m := &wakeModels{}
|
m := &wakeModels{}
|
||||||
var err error
|
|
||||||
if m.mel, err = open(melPath, []string{"input"}, []string{"output"}); err != nil {
|
if m.mel, err = open(melPath, []string{"input"}, []string{"output"}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-6
@@ -4,11 +4,22 @@
|
|||||||
# user unit because it needs his ALSA session and his ssh agent, and because
|
# user unit because it needs his ALSA session and his ssh agent, and because
|
||||||
# it should stop when he logs out.
|
# it should stop when he logs out.
|
||||||
#
|
#
|
||||||
# THERE IS NO WAKE WORD YET (V-487 stage two). Anything spoken near the fifine
|
# The keyword is "Мэйвен" and the three -wake- flags are what require it
|
||||||
# becomes a turn. What makes that safe rather than expensive is voiceSender:
|
# (V-487 stage two). Without them anything spoken near the fifine becomes a
|
||||||
# it sends Surface=SurfaceVoice, which caps every command at L0, so no
|
# turn, which voiceSender makes safe rather than expensive: it sends
|
||||||
# accidental trigger runs a destructive act. It does not stop her answering
|
# Surface=SurfaceVoice, capping every command at L0. That does not stop her
|
||||||
# out loud, so this unit is his to stop when the room is not his alone.
|
# answering out loud, which is the whole reason the keyword exists.
|
||||||
|
#
|
||||||
|
# The threshold is 0.999 and it is the binary's default, so it is not passed.
|
||||||
|
# It came from 65 minutes of held-out Russian speech through this same binary:
|
||||||
|
# 0.9 false wakes an hour against 2.8 at 0.99, for one lost render out of 126
|
||||||
|
# (docs/evals/2026-08-09-wake-word.md). If the room proves noisier than the
|
||||||
|
# corpus, read the scores out of this unit's journal and pass -wake-threshold.
|
||||||
|
# Do not lower it by guessing.
|
||||||
|
#
|
||||||
|
# A keyword shorter than 1.32s can be heard too late to be used, because the
|
||||||
|
# head scores 1.28s of audio and the VAD has closed the utterance by then.
|
||||||
|
# "Мэйвен, <request>" is unaffected. A bare "Мэйвен" is the case that fails.
|
||||||
#
|
#
|
||||||
# -vad-model is passed on purpose. Silero answers "is this frame speech" where
|
# -vad-model is passed on purpose. Silero answers "is this frame speech" where
|
||||||
# the energy floor answers "is this frame loud". It declines white noise at
|
# the energy floor answers "is this frame loud". It declines white noise at
|
||||||
@@ -24,7 +35,7 @@
|
|||||||
# systemctl --user enable --now mavwaked.service
|
# systemctl --user enable --now mavwaked.service
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Maven always-on listening (VAD, no wake word yet)
|
Description=Maven always-on listening (silero VAD, "Мэйвен" keyword)
|
||||||
# The tunnel is the only path to mavend and the only thing authenticating it.
|
# The tunnel is the only path to mavend and the only thing authenticating it.
|
||||||
Requires=maven-voice-tunnel.service
|
Requires=maven-voice-tunnel.service
|
||||||
After=maven-voice-tunnel.service
|
After=maven-voice-tunnel.service
|
||||||
@@ -45,6 +56,9 @@ ExecStart=%h/.local/bin/mavwaked \
|
|||||||
-addr 127.0.0.1:9100 \
|
-addr 127.0.0.1:9100 \
|
||||||
-lang ru \
|
-lang ru \
|
||||||
-vad-model %h/.local/share/maven/models/silero_vad.onnx \
|
-vad-model %h/.local/share/maven/models/silero_vad.onnx \
|
||||||
|
-wake-model %h/.local/share/maven/models/maven_wakeword.onnx \
|
||||||
|
-wake-mel %h/.local/share/maven/models/melspectrogram.onnx \
|
||||||
|
-wake-embed %h/.local/share/maven/models/embedding_model.onnx \
|
||||||
-onnx-lib %h/.local/lib/libonnxruntime.so
|
-onnx-lib %h/.local/lib/libonnxruntime.so
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|||||||
+13
-1
@@ -170,7 +170,19 @@ The device is `plughw:0,0` and not `hw:0,0`. The fifine offers 2 channels at
|
|||||||
44100 or 48000 and nothing else, and mavwaked asks arecord for 16kHz mono. Bare
|
44100 or 48000 and nothing else, and mavwaked asks arecord for 16kHz mono. Bare
|
||||||
`hw` dies on "Channels count non available" before a frame is read.
|
`hw` dies on "Channels count non available" before a frame is read.
|
||||||
|
|
||||||
There is no wake word yet (V-487 stage two), so the loop runs open.
|
The three `-wake-` flags require the keyword "Мэйвен" (V-487 stage two). Drop
|
||||||
|
them and the loop runs open, which is what it did before. The threshold is the
|
||||||
|
binary's default of 0.999 and is not passed. Over 65 minutes of held-out
|
||||||
|
Russian speech it woke her 0.9 times an hour against 2.8 at 0.99. That cost one
|
||||||
|
lost render out of 126 (`docs/evals/2026-08-09-wake-word.md`).
|
||||||
|
|
||||||
|
The three sessions are pinned to one thread each. onnxruntime otherwise sizes
|
||||||
|
its pool to every core and spins between runs, which took mavwaked from 68% of
|
||||||
|
one core to 335%. With the cap it sits at 81%, so the gate costs about 13%.
|
||||||
|
|
||||||
|
A keyword shorter than 1.32s can be heard too late to be used. The head scores
|
||||||
|
1.28s of audio, and the VAD has closed the utterance by then.
|
||||||
|
"Мэйвен, <request>" is unaffected. A bare "Мэйвен" is the case that fails.
|
||||||
|
|
||||||
mavwaked connects at startup and holds the conn, so a nudge routed to voice
|
mavwaked connects at startup and holds the conn, so a nudge routed to voice
|
||||||
reaches the speaker before he has said anything (V-671). It used to connect
|
reaches the speaker before he has said anything (V-671). It used to connect
|
||||||
|
|||||||
@@ -94,6 +94,14 @@ the head the whole request to peak during. A bare "Мэйвен" with nothing af
|
|||||||
it is the case that fails. One fix would hold an ignored utterance for a grace
|
it is the case that fails. One fix would hold an ignored utterance for a grace
|
||||||
period and ship it if the keyword lands late. It is not built.
|
period and ship it if the keyword lands late. It is not built.
|
||||||
|
|
||||||
|
## What it costs the workstation
|
||||||
|
|
||||||
|
Under systemd on workpc, mavwaked sat at 335% of a core with the gate on and
|
||||||
|
68% with only silero. onnxruntime sizes its thread pool to every core and spins
|
||||||
|
between runs, and this gate runs three graphs twelve times a second. Pinning
|
||||||
|
all three sessions to one thread brought it to 81%, so the keyword costs about
|
||||||
|
13% of one core. The three graphs each finish in well under 80ms that way.
|
||||||
|
|
||||||
## What was not measured
|
## What was not measured
|
||||||
|
|
||||||
No room recordings. Every negative above is a clean corpus clip. This gate
|
No room recordings. Every negative above is a clean corpus clip. This gate
|
||||||
|
|||||||
Reference in New Issue
Block a user