fix: document and verify provider priority leveling
This commit is contained in:
parent
9b4c6ac644
commit
b55d1a2e85
7 changed files with 223 additions and 42 deletions
|
|
@ -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 하나만 포함한다.
|
||||
|
||||
## 기준 출력 예시
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 실행하지 않는다.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 증가를 기준으로 판정한다.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue