fix: document and verify provider priority leveling

This commit is contained in:
toki 2026-07-04 15:05:36 +09:00
parent 9b4c6ac644
commit b55d1a2e85
7 changed files with 223 additions and 42 deletions

View file

@ -3,7 +3,7 @@ test_env: dev
test_profile: edge-smoke
domain: edge
verification_type: smoke
last_rule_updated_at: 2026-06-24
last_rule_updated_at: 2026-07-04
---
# edge-smoke dev 테스트
@ -51,7 +51,7 @@ dev-runtime provider pool과 3-node 연결 상태를 점검할 때는 `agent-tes
- bootstrap HTTP: `http://toki-labs.com:18082`
- Edge OpenAI-compatible base URL: `http://toki-labs.com:18083/v1`
- Edge-Node TCP transport: `toki-labs.com:18084`
- model alias: `ornith:35b`
- model alias: `qwen3.6:35b`
노드 후보:
@ -61,28 +61,31 @@ dev-runtime provider pool과 3-node 연결 상태를 점검할 때는 `agent-tes
- workspace: `/Users/toki/agent-work/iop-workspace/nomadcode`
- MLX vLLM provider: `mac-mlx-vllm`
- provider endpoint: `http://127.0.0.1:8002/v1`
- served model: `mlx-community/Ornith-1.0-35B-4bit`
- served model: `mlx-community/Qwen3.6-35B-A3B-4bit`
- capacity baseline: `2`
- priority baseline: `2`
- workdir: `/Users/toki/agent-work/iop-mlx-vllm`
- runtime baseline: `vllm-mlx`, `--max-num-seqs 2`, `--max-kv-size 262144`, `--max-request-tokens 262144`, `--use-paged-cache --paged-cache-block-size 64 --max-cache-blocks 4096`
- KV policy: per-call window bound `262144`, one full context-size setting
- GX10 vLLM node: `gx10-vllm-node` / `gx10-vllm`
- SSH/user: `ssh toki@192.168.0.91`
- provider endpoint: `http://192.168.0.91:8001/v1`
- served model: `sakamakismile/Ornith-1.0-35B-NVFP4`
- served model: `nvidia/Qwen3.6-35B-A3B-NVFP4`
- capacity baseline: `4`
- runtime baseline: `vllm`, `--max-model-len 262144`, `--max-num-seqs 4`, `--gpu-memory-utilization 0.30`
- priority baseline: `0`
- runtime baseline: `vllm`, `--max-model-len 262144`, `--max-num-seqs 4`, `--gpu-memory-utilization 0.30`, `--reasoning-parser qwen3`
- workspace: `/home/toki/iop-gx10-vllm`
- OneXPlayer Lemonade node: `onexplayer-lemonade-node` / `onexplayer-lemonade`
- SSH/user: `ssh r0bin@192.168.0.59`
- 접속 기준: 현재 작업 호스트에서 직접 SSH
- provider endpoint: `http://192.168.0.59:13305/v1`
- served model: `Ornith-1.0-35B-GGUF-Q4_K_M`
- served model: `Qwen3.6-35B-A3B-MTP-GGUF`
- capacity baseline: `3`
- priority baseline: `1`
- load baseline: backend `vulkan`, ctx size `524288`, `llamacpp_args="--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"`, `save_options=true`
- workspace: `C:/Users/r0bin/iop-field`
OneXPlayer Lemonade는 `Ornith-1.0-35B-GGUF-Q4_K_M` artifact를 사용하되 runtime MTP speculative decoding은 켜지 않는다. dev long-context baseline은 `/v1/load``llamacpp_backend=vulkan`, `ctx_size=524288`, `--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024``save_options=true`로 저장한 상태다. llama.cpp backend의 `/slots`에서는 총 ctx `524288`이 slot 3개로 나뉘어 2026-07-04 기준 slot별 `n_ctx=174848`로 보인다.
OneXPlayer Lemonade는 `Qwen3.6-35B-A3B-MTP-GGUF` artifact를 사용하되 runtime MTP speculative decoding은 켜지 않는다. dev long-context baseline은 `/v1/load` `model_name=Qwen3.6-35B-A3B-MTP-GGUF`, `llamacpp_backend=vulkan`, `ctx_size=524288`, `--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024``save_options=true`로 저장한 상태다. llama.cpp backend의 `/slots`에서는 총 ctx `524288`이 slot 3개로 나뉘어 2026-07-04 기준 slot별 `n_ctx=174848`로 보인다.
OneXPlayer Lemonade Node는 원격 runner나 Edge host에서 다시 SSH하거나 proxy process로 띄우지 않는다. 현재 작업 호스트에서 OneXPlayer Windows host에 `ssh r0bin@192.168.0.59`로 직접 접속한 뒤 generated PowerShell bootstrap을 실행한다.
@ -116,10 +119,11 @@ mac-mlx-vllm provider는 mac-codex-node 소속 resource로, Edge host와 같은
- node 로컬 `[node-message]` payload 라인 목록과 edge `[node-*-message]` payload 라인 목록이 run별로 내용/순서까지 동일해야 한다.
- edge complete는 같은 run의 마지막 `[node-*-message]` 이후에만 정상이다.
- OpenAI-compatible smoke에서 `/healthz`, `/v1/models`, `/v1/responses`가 기대 상태로 응답한다.
- provider-pool dispatch는 `in_flight >= capacity`인 provider를 후보에서 제외하고, 남은 후보 중 가장 낮은 `in_flight` 레벨을 먼저 선택한다. 같은 `in_flight` 레벨 안에서는 낮은 priority 값과 rotation으로 분산한다.
- dev-runtime capacity smoke는 `/v1/responses``/v1/chat/completions` 각각에 provider capacity 총합 + 1개 동시 요청을 보내고, Control Plane status의 `provider_snapshots`에서 총 `in_flight`가 capacity 총합에 도달하며 `queued`가 1 이상 잡히는지 확인한다. 현재 `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=2`이면 endpoint별 10개 동시 요청에서 총 `in_flight=9`, `queued>=1` 관측을 기준으로 한다. 13개 동시 확장 smoke에서는 총 `in_flight=9`, `queued>=4` 관측을 기대한다.
- capacity smoke 완료 후 대상 provider의 `in_flight=0`, `queued=0` 회복을 확인한다.
- 2026-06-24 dev 13-way 확장 smoke의 관측 결과`/v1/chat/completions` 13개 요청 성공, peak `in_flight=10`, 실제 queue 대기 `3`개, 최종 회복 `in_flight=0`, `queued=0`다. 초기 peak는 `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=3`으로 채워졌고, 대기 요청은 먼저 slot이 빈 provider로 dispatch될 수 있다.
- Ornith provider-pool smoke는 thinking/reasoning 텍스트가 포함될 수 있다. 추론 출력 자체를 실패로 보지 말고 HTTP 성공, model alias, final marker 포함 여부, provider node log/run count 증가로 판정한다. 응답 전체가 특정 token과 정확히 같은지 비교하는 strict exact-match는 이 profile의 기본 판정으로 쓰지 않는다.
- 2026-06-24 dev 13-way 확장 smoke는 Mac capacity가 `3`이던 이전 관측이다. 해당 run에서`/v1/chat/completions` 13개 요청 성공, peak `in_flight=10`, 실제 queue 대기 `3`개, 최종 회복 `in_flight=0`, `queued=0` 관측되었다. 현재 Mac capacity `2` 기준 13-way 확장 smoke 기대치는 peak `in_flight=9`, queue 대기 `4`개 이상이다.
- Qwen provider-pool smoke는 thinking/reasoning 텍스트가 포함될 수 있다. 추론 출력 자체를 실패로 보지 말고 HTTP 성공, model alias, final marker 포함 여부, provider node log/run count 증가로 판정한다. 응답 전체가 특정 token과 정확히 같은지 비교하는 strict exact-match는 이 profile의 기본 판정으로 쓰지 않는다.
- bootstrap 사용자 명령은 완성된 URL과 positional token 하나만 포함한다.
## 기준 출력 예시

View file

@ -1,6 +1,6 @@
test_env: dev
profile: dev-runtime-provider-pool
last_updated_at: "2026-07-03"
last_updated_at: "2026-07-04"
source:
remote_runner:
@ -31,23 +31,106 @@ build:
node_windows_amd64: build/dev-runtime/bin/iop-node-windows-amd64.exe
model:
alias: ornith:35b
alias: qwen3.6:35b
provider_capacity_total: 9
provider_capacity_status: verified_with_control_plane_provider_snapshots_2026_07_04
context_window: 262144
default_max_tokens: 32768
min_max_tokens: 32768
default_thinking_token_budget: 8192
default_thinking_token_budget: 1024
reasoning_policy: bounded_thinking
content_completion_policy:
enforce_min_max_tokens: true
reason: prevent reasoning-only completions from exhausting caller max_tokens before final content
dispatch_policy:
capacity_gate: providers at or above capacity are excluded from dispatch candidates
selection_order: lower in_flight level wins among providers with available capacity
same_in_flight_tiebreak: lower numeric priority first, then deterministic rotation
capacity_smoke:
endpoints:
- /v1/responses
- /v1/chat/completions
concurrent_requests: capacity_plus_one
concurrent_requests: 9
expected_total_in_flight: 9
expected_min_queued: 1
expected_min_queued: 0
capacity_plus_one_concurrent_requests: 10
capacity_plus_one_expected_total_in_flight: 9
capacity_plus_one_expected_min_queued: 1
extended_concurrent_requests: 13
extended_expected_min_queued: 4
prompt_policy: long_reasoning_allowed
exact_output_match: false
latest_provider_snapshot:
observed_at: "2026-07-04"
total_capacity: 9
all_nodes_connected: true
providers:
gx10-vllm:
capacity: 4
priority: 0
in_flight: 0
queued: 0
health: healthy
served_model: nvidia/Qwen3.6-35B-A3B-NVFP4
mac-mlx-vllm:
capacity: 2
priority: 2
in_flight: 0
queued: 0
health: healthy
served_model: mlx-community/Qwen3.6-35B-A3B-4bit
onexplayer-lemonade:
capacity: 3
priority: 1
in_flight: 0
queued: 0
health: healthy
served_model: Qwen3.6-35B-A3B-MTP-GGUF
final_content_smoke:
endpoint: /v1/chat/completions
concurrent_requests: 10
current_capacity_total: 9
expected_peak_total_in_flight: 9
expected_min_queued: 1
request_max_tokens: 3072
effective_min_max_tokens: 32768
include_reasoning: false
expected_success_count: 10
expected_finish_reason: stop
expected_final_marker: true
expected_empty_content: 0
expected_iop_notice: 0
previous_observed_at: "2026-07-04"
previous_observed_capacity_total: 10
previous_observed_result:
http_ok: 10
success_count: 10
failure_count: 0
finish_reason_stop: 10
marker_missing_count: 0
empty_content_count: 0
iop_notice_count: 0
think_tag_leak_count: 0
reasoning_present_count: 0
aggregate_completion_tok_s: 62.5
total_completion_tokens: 17933
batch_wall_sec: 286.908
avg_latency_sec: 175.074
peak_total_in_flight: 10
peak_total_queued: 0
provider_peaks:
gx10-vllm:
capacity: 4
max_in_flight: 4
max_queued: 0
mac-mlx-vllm:
capacity: 3
max_in_flight: 3
max_queued: 0
onexplayer-lemonade:
capacity: 3
max_in_flight: 3
max_queued: 0
nodes:
- id: mac-codex-node
@ -63,8 +146,9 @@ nodes:
- id: mac-mlx-vllm
type: vllm-mlx
endpoint: http://127.0.0.1:8002/v1
served_model: mlx-community/Ornith-1.0-35B-4bit
served_model: mlx-community/Qwen3.6-35B-A3B-4bit
capacity: 2
priority: 2
runtime:
api_key_policy: local_bearer_from_edge_yaml
workdir: /Users/toki/agent-work/iop-mlx-vllm
@ -81,10 +165,12 @@ nodes:
max_kv_size: 262144
max_request_tokens: 262144
default_max_tokens: 32768
default_thinking_token_budget: 8192
default_thinking_token_budget: 1024
paged_cache_block_size: 64
max_cache_blocks: 4096
total_kv_tokens: 262144
tool_call_parser: qwen
reasoning_parser: qwen3
default_chat_template_kwargs:
enable_thinking: true
- id: gx10-vllm-node
@ -97,12 +183,17 @@ nodes:
id: gx10-vllm
type: vllm
endpoint: http://192.168.0.91:8001/v1
served_model: sakamakismile/Ornith-1.0-35B-NVFP4
served_model: nvidia/Qwen3.6-35B-A3B-NVFP4
capacity: 4
priority: 0
runtime:
container_name: iop-vllm-qwen36-prev-20260704063128
max_model_len: 262144
max_num_seqs: 4
gpu_memory_utilization: 0.30
default_max_tokens: 32768
default_thinking_token_budget: 1024
reasoning_parser: qwen3
default_chat_template_kwargs:
enable_thinking: true
- id: onexplayer-lemonade-node
@ -116,14 +207,18 @@ nodes:
id: onexplayer-lemonade
type: lemonade
endpoint: http://192.168.0.59:13305/v1
served_model: Ornith-1.0-35B-GGUF-Q4_K_M
served_model: Qwen3.6-35B-A3B-MTP-GGUF
capacity: 3
priority: 1
load:
endpoint: http://192.168.0.59:13305/v1/load
model_name: Qwen3.6-35B-A3B-MTP-GGUF
backend: vulkan
ctx_size: 524288
llamacpp_args: "--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"
observed_slot_n_ctx: 174848
save_options: true
mtp_runtime_policy: disabled
default_max_tokens: 32768
default_thinking_token_budget: 1024
windows_process_start: Win32_Process.Create

View file

@ -3,7 +3,7 @@ test_env: dev
test_profile: node-smoke
domain: node
verification_type: smoke
last_rule_updated_at: 2026-06-24
last_rule_updated_at: 2026-07-04
---
# node-smoke dev 테스트
@ -50,28 +50,31 @@ dev-runtime의 실제 3-node 연결을 점검할 때는 원격 runner `ssh toki@
- workspace: `/Users/toki/agent-work/iop-workspace/nomadcode`
- MLX vLLM provider: `mac-mlx-vllm`
- provider endpoint: `http://127.0.0.1:8002/v1`
- served model: `mlx-community/Ornith-1.0-35B-4bit`
- served model: `mlx-community/Qwen3.6-35B-A3B-4bit`
- capacity baseline: `2`
- priority baseline: `2`
- workdir: `/Users/toki/agent-work/iop-mlx-vllm`
- runtime baseline: `vllm-mlx`, `--max-num-seqs 2`, `--max-kv-size 262144`, `--max-request-tokens 262144`, `--use-paged-cache --paged-cache-block-size 64 --max-cache-blocks 4096`
- KV policy: per-call window bound `262144`, one full context-size setting
- GX10 vLLM node: `gx10-vllm-node` / `gx10-vllm`
- SSH/user: `ssh toki@192.168.0.91`
- provider endpoint: `http://192.168.0.91:8001/v1`
- served model: `sakamakismile/Ornith-1.0-35B-NVFP4`
- served model: `nvidia/Qwen3.6-35B-A3B-NVFP4`
- capacity baseline: `4`
- runtime baseline: `vllm`, `--max-model-len 262144`, `--max-num-seqs 4`, `--gpu-memory-utilization 0.30`
- priority baseline: `0`
- runtime baseline: `vllm`, `--max-model-len 262144`, `--max-num-seqs 4`, `--gpu-memory-utilization 0.30`, `--reasoning-parser qwen3`
- workspace: `/home/toki/iop-gx10-vllm`
- OneXPlayer Lemonade node: `onexplayer-lemonade-node` / `onexplayer-lemonade`
- SSH/user: `ssh r0bin@192.168.0.59`
- 접속 기준: 현재 작업 호스트에서 직접 SSH
- provider endpoint: `http://192.168.0.59:13305/v1`
- served model: `Ornith-1.0-35B-GGUF-Q4_K_M`
- served model: `Qwen3.6-35B-A3B-MTP-GGUF`
- capacity baseline: `3`
- priority baseline: `1`
- load baseline: backend `vulkan`, ctx size `524288`, `llamacpp_args="--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024"`, `save_options=true`
- workspace: `C:/Users/r0bin/iop-field`
OneXPlayer Lemonade는 `Ornith-1.0-35B-GGUF-Q4_K_M` artifact를 사용하되 runtime MTP speculative decoding은 끈 상태를 dev 기준으로 삼는다. Node 검증 전 `/v1/load``recipe_options`가 Vulkan, `ctx_size=524288`, `--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024`를 포함하는지 확인한다. backend `/slots`에서는 총 ctx `524288`이 slot 3개로 나뉘어 2026-07-04 기준 slot별 `n_ctx=174848`로 보인다.
OneXPlayer Lemonade는 `Qwen3.6-35B-A3B-MTP-GGUF` artifact를 사용하되 runtime MTP speculative decoding은 끈 상태를 dev 기준으로 삼는다. Node 검증 전 `/v1/load``recipe_options` `model_name=Qwen3.6-35B-A3B-MTP-GGUF`, Vulkan, `ctx_size=524288`, `--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024`를 포함하는지 확인한다. backend `/slots`에서는 총 ctx `524288`이 slot 3개로 나뉘어 2026-07-04 기준 slot별 `n_ctx=174848`로 보인다.
GX10은 Linux/ARM64 bootstrap, OneXPlayer는 Windows native PowerShell bootstrap을 기본으로 한다. OneXPlayer는 현재 작업 호스트에서 직접 접속해 세팅하며, 원격 runner나 Edge host에서 `node-onexplayer-lemonade.yaml`로 proxy 실행하지 않는다.

View file

@ -182,10 +182,12 @@ func (m *modelQueueManager) updateGroupPolicyLocked(g *modelQueueGroup, policy g
}
// findAvailableNodeLocked returns the available candidate with the lowest
// absolute in_flight count. When in_flight is equal, the candidate with the
// lower priority value wins. When both in_flight and priority are equal,
// all tied candidates remain in best[] so that the rotation logic in the
// caller picks the next slot after the last selected one.
// absolute in_flight count. Full candidates are skipped, so a provider that is
// at capacity is deferred behind providers that still have room. When in_flight
// is equal, the candidate with the lower priority value wins. When both
// in_flight and priority are equal, all tied candidates remain in best[] so
// that the rotation logic in the caller picks the next slot after the last
// selected one.
func (m *modelQueueManager) findAvailableNodeLocked(group *modelQueueGroup, candidates []candidateNode) *candidateNode {
var best []candidateNode
bestInflight := 0

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"net"
"reflect"
"sync"
"testing"
"time"
@ -497,8 +498,8 @@ func TestModelQueueUsesProviderQueuePolicy(t *testing.T) {
}
// TestModelQueueProviderInflightSelectionBeatsPriority verifies that in_flight
// count is the primary selection criterion: a candidate with lower in_flight
// is preferred even if it has a higher (worse) priority value.
// level is the primary selection criterion: a candidate with lower in_flight is
// preferred even if it has a higher (worse) priority value.
func TestModelQueueProviderInflightSelectionBeatsPriority(t *testing.T) {
entryA := &edgenode.NodeEntry{NodeID: "node-ip-a"}
entryB := &edgenode.NodeEntry{NodeID: "node-ip-b"}
@ -526,6 +527,77 @@ func TestModelQueueProviderInflightSelectionBeatsPriority(t *testing.T) {
}
}
// TestModelQueueProviderFullPriorityFallsThrough verifies that priority does
// not override capacity: a full high-priority provider is skipped and the next
// available priority tier receives the request.
func TestModelQueueProviderFullPriorityFallsThrough(t *testing.T) {
entryA := &edgenode.NodeEntry{NodeID: "node-full-a"}
entryB := &edgenode.NodeEntry{NodeID: "node-next-b"}
cands := []candidateNode{
{entry: entryA, capacity: 4, priority: 1, providerID: "prov-fast"},
{entry: entryB, capacity: 3, priority: 10, providerID: "prov-slow"},
}
m := newModelQueueManager(nil)
m.mu.Lock()
g := m.getOrCreateGroupLocked("g-full-priority", groupPolicy{})
g.inflight["node-full-a:prov-fast"] = 4
m.mu.Unlock()
sel, err := m.admit(context.Background(), "g-full-priority", "", "", cands, groupPolicy{})
if err != nil {
t.Fatalf("admit: %v", err)
}
if sel == nil || sel.providerID != "prov-slow" {
t.Fatalf("expected prov-slow after high-priority provider is full, got %v", sel)
}
}
// TestModelQueueProviderLevelingPriorityAdmissionOrder fixes the intended
// provider-pool order for differently-sized providers: fill each in_flight
// level before moving to the next level, and use priority within the same
// level. With capacities 4/3/2 and priorities 0/1/2, the first nine admits are
// gx10, onex, mac, gx10, onex, mac, gx10, onex, gx10. The tenth has no
// available candidate until a slot is released.
func TestModelQueueProviderLevelingPriorityAdmissionOrder(t *testing.T) {
cands := []candidateNode{
{entry: &edgenode.NodeEntry{NodeID: "node-gx10"}, capacity: 4, priority: 0, providerID: "gx10-vllm"},
{entry: &edgenode.NodeEntry{NodeID: "node-onex"}, capacity: 3, priority: 1, providerID: "onexplayer-lemonade"},
{entry: &edgenode.NodeEntry{NodeID: "node-mac"}, capacity: 2, priority: 2, providerID: "mac-mlx-vllm"},
}
m := newModelQueueManager(nil)
var got []string
for i := 0; i < 9; i++ {
sel, err := m.admit(context.Background(), "qwen3.6:35b", "", "", cands, groupPolicy{})
if err != nil {
t.Fatalf("admit %d: %v", i+1, err)
}
got = append(got, sel.providerID)
}
want := []string{
"gx10-vllm",
"onexplayer-lemonade",
"mac-mlx-vllm",
"gx10-vllm",
"onexplayer-lemonade",
"mac-mlx-vllm",
"gx10-vllm",
"onexplayer-lemonade",
"gx10-vllm",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("admission order mismatch:\n got: %v\nwant: %v", got, want)
}
m.mu.Lock()
g := m.getOrCreateGroupLocked("qwen3.6:35b", groupPolicy{})
if candidate := m.findAvailableNodeLocked(g, cands); candidate != nil {
t.Fatalf("expected no available candidate after filling capacity, got %s", candidate.providerID)
}
m.mu.Unlock()
}
// TestModelQueueProviderPriorityBreaksEqualInflightTie verifies that when
// in_flight counts are equal, the candidate with the lower priority value
// is selected.
@ -1501,8 +1573,8 @@ func TestResolveProviderPoolCandidatesPropagatesPriority(t *testing.T) {
{
ID: "model-prio",
Providers: map[string]string{
"prov-high-prio": "served-model-prio",
"prov-low-prio": "served-model-prio",
"prov-high-prio": "served-model-prio",
"prov-low-prio": "served-model-prio",
"prov-no-priority": "served-model-prio",
},
},

View file

@ -113,6 +113,9 @@ console:
models:
- id: "qwen3.6:35b"
display_name: "Qwen 3.6 35B"
default_max_tokens: 32768
min_max_tokens: 32768
default_thinking_token_budget: 1024
providers:
mac-mlx-vllm: "mlx-community/Qwen3.6-35B-A3B-4bit"
# Example: CLI providers (codex, codex-exec) do not serve models.
@ -174,7 +177,7 @@ nodes:
models:
- "mlx-community/Qwen3.6-35B-A3B-4bit"
health: "healthy"
capacity: 3
capacity: 2
# Legacy adapters configuration (compat override example):
# adapters:
# cli:

View file

@ -152,9 +152,11 @@ GOOS=windows GOARCH=amd64 go build -trimpath -o build/dev-runtime/bin/iop-node-w
기준 provider pool:
- model alias: `qwen3.6:35b`
- provider candidate: `gx10-vllm` on `gx10-vllm-node`, capacity `4`
- provider candidate: `onexplayer-lemonade` on `onexplayer-lemonade-node`, capacity `3`
- provider candidate: `mac-mlx-vllm` on `mac-codex-node`, capacity `3`
- provider candidate: `gx10-vllm` on `gx10-vllm-node`, capacity `4`, priority `0`
- provider candidate: `onexplayer-lemonade` on `onexplayer-lemonade-node`, capacity `3`, priority `1`
- provider candidate: `mac-mlx-vllm` on `mac-codex-node`, capacity `2`, priority `2`
Provider-pool dispatch는 capacity gate를 먼저 적용해 `in_flight >= capacity`인 provider를 후보에서 제외한다. 남은 후보 중 가장 낮은 `in_flight` 레벨을 우선 선택하며, 같은 `in_flight` 레벨 안에서 priority 숫자가 낮은 provider를 먼저 선택한다. 이 기준에서 10개 동시 요청은 정상적으로 `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=2`, queue `1` 구성이 된다.
GX10 vLLM은 `nvidia/Qwen3.6-35B-A3B-NVFP4`를 Docker container `iop-vllm-qwen36-prev-20260704063128`로 띄운다. dev 기준 endpoint는 `http://192.168.0.91:8001/v1`, runtime baseline은 `--max-model-len 262144`, `--max-num-seqs 4`, `--gpu-memory-utilization 0.30`, `--reasoning-parser qwen3`, `--default-chat-template-kwargs '{"enable_thinking":true}'`이다.
@ -168,9 +170,9 @@ curl -fsS http://192.168.0.59:13305/v1/load \
-d '{"model_name":"Qwen3.6-35B-A3B-MTP-GGUF","ctx_size":524288,"llamacpp_backend":"vulkan","llamacpp_args":"--spec-type none -np 3 -cb -fa on -b 4096 -ub 1024","save_options":true}'
```
Mac MLX provider는 Edge host 자신에서 `vllm-mlx`로 띄운다. dev 기준 endpoint는 `http://127.0.0.1:8002/v1`, served model은 `mlx-community/Qwen3.6-35B-A3B-4bit`, provider capacity는 `3`이다. 작업 경로는 `/Users/toki/agent-work/iop-mlx-vllm`, Python 환경은 `.venv`의 Homebrew Python 3.12, 모델 cache는 `hf-cache`를 사용한다. 확인된 package baseline은 `vllm-mlx 0.3.0`, `mlx 0.31.2`, `mlx-lm 0.31.3`이다.
Mac MLX provider는 Edge host 자신에서 `vllm-mlx`로 띄운다. dev 기준 endpoint는 `http://127.0.0.1:8002/v1`, served model은 `mlx-community/Qwen3.6-35B-A3B-4bit`, provider capacity는 `2`이다. 작업 경로는 `/Users/toki/agent-work/iop-mlx-vllm`, Python 환경은 `.venv`의 Homebrew Python 3.12, 모델 cache는 `hf-cache`를 사용한다. 확인된 package baseline은 `vllm-mlx 0.3.0`, `mlx 0.31.2`, `mlx-lm 0.31.3`이다.
vllm-mlx process는 `--max-num-seqs 3`, `--max-kv-size 262144`, `--max-request-tokens 262144`, `--use-paged-cache --paged-cache-block-size 64 --max-cache-blocks 4096`로 고정한다. 이 설정은 각 호출 window bound를 `262144`로 두고, provider capacity는 `3`으로 맞춘다.
vllm-mlx process는 `--max-num-seqs 2`, `--max-kv-size 262144`, `--max-request-tokens 262144`, `--use-paged-cache --paged-cache-block-size 64 --max-cache-blocks 4096`로 고정한다. 이 설정은 병렬 슬롯을 `2`로 제한하고, KV/context 예산을 full `262144` window 1개분으로 맞춘다.
```bash
cd /Users/toki/agent-work/iop-mlx-vllm
@ -181,7 +183,7 @@ nohup .venv/bin/vllm-mlx serve mlx-community/Qwen3.6-35B-A3B-4bit \
--host 127.0.0.1 \
--port 8002 \
--api-key "$IOP_MLX_API_KEY" \
--max-num-seqs 3 \
--max-num-seqs 2 \
--max-kv-size 262144 \
--max-request-tokens 262144 \
--use-paged-cache \
@ -225,12 +227,12 @@ Node host OS 재부팅은 필요하지 않다. Edge process 재시작이나 일
2. Edge host에서 `toki-labs.com:18084`의 established node TCP connection 수를 확인한다.
3. Edge process를 재시작하고 bootstrap 재실행 없이 3개 Node 연결이 회복되는지 확인한다.
4. `http://toki-labs.com:18083/healthz``/v1/models`를 확인한다.
5. capacity를 `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=3`으로 맞춘 후보 config를 refresh apply하고 Edge process가 유지되는지 확인한다. Node adapter capacity 변경이 `restart_required`로 반환되면 Edge process를 새 config로 재시작한 뒤 Node reconnect 상태를 확인한다.
5. capacity를 `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=2`로 맞춘 후보 config를 refresh apply하고 Edge process가 유지되는지 확인한다. Node adapter capacity 변경이 `restart_required`로 반환되면 Edge process를 새 config로 재시작한 뒤 Node reconnect 상태를 확인한다.
6. `qwen3.6:35b``/v1/responses``/v1/chat/completions` 각각에 provider capacity 총합 + 1개 동시 요청을 보낸다.
7. capacity `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=3` 기준이면 endpoint별 10개 동시 요청에서 Control Plane status의 provider snapshot이 총 `in_flight=10`, `queued=0`으로 처리되어야 한다. 11개 동시 요청에서는 `in_flight=10`, `queued>=1`, 13개 동시 확장 smoke에서는 `in_flight=10`, `queued>=3` 기대한다.
7. capacity `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=2` 기준이면 endpoint별 9개 동시 요청에서 Control Plane status의 provider snapshot이 총 `in_flight=9`, `queued=0`으로 처리되어야 한다. 10개 동시 요청에서는 `in_flight=9`, `queued>=1`, 13개 동시 확장 smoke에서는 `in_flight=9`, `queued>=4` 기대한다.
8. 요청 완료 후 provider snapshot이 `in_flight=0`, `queued=0`으로 회복되는지 확인한다.
2026-06-24 dev 13-way 확장 smoke에서는 `/v1/chat/completions` 13개 요청이 모두 성공했고 peak `in_flight=10`, 실제 queue 대기 `3`개가 관측되었다. 초기 peak는 `gx10-vllm=4`, `onexplayer-lemonade=3`, `mac-mlx-vllm=3`으로 꽉 찼고, 초과 대기분은 slot이 먼저 빈 provider로 dispatch되었다. 같은 run에서 최종 처리 건수는 GX10 4건, OneXPlayer 3건, Mac MLX 6건이었다.
2026-06-24 dev 13-way 확장 smoke는 Mac capacity가 `3`이던 이전 관측이다. 해당 run에서는 `/v1/chat/completions` 13개 요청이 모두 성공했고 peak `in_flight=10`, 실제 queue 대기 `3`개가 관측되었다. 현재 Mac capacity `2` 기준의 13-way smoke 기대치는 peak `in_flight=9`, queue 대기 `4`개 이상이다.
Qwen 계열 thinking 모델은 reasoning 텍스트를 포함해 응답할 수 있다. 이 dev smoke에서는 thinking 출력을 실패로 보지 않고, HTTP 성공, final marker 포함, provider log/run count 증가를 기준으로 판정한다.