diff --git a/agent-test/dev/edge-smoke.md b/agent-test/dev/edge-smoke.md index 7820ff0..18b3bcb 100644 --- a/agent-test/dev/edge-smoke.md +++ b/agent-test/dev/edge-smoke.md @@ -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 하나만 포함한다. ## 기준 출력 예시 diff --git a/agent-test/dev/inventory.yaml b/agent-test/dev/inventory.yaml index af636aa..49516ec 100644 --- a/agent-test/dev/inventory.yaml +++ b/agent-test/dev/inventory.yaml @@ -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 diff --git a/agent-test/dev/node-smoke.md b/agent-test/dev/node-smoke.md index ea43255..b833a5b 100644 --- a/agent-test/dev/node-smoke.md +++ b/agent-test/dev/node-smoke.md @@ -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 실행하지 않는다. diff --git a/apps/edge/internal/service/model_queue.go b/apps/edge/internal/service/model_queue.go index fb17456..cba08ad 100644 --- a/apps/edge/internal/service/model_queue.go +++ b/apps/edge/internal/service/model_queue.go @@ -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 diff --git a/apps/edge/internal/service/model_queue_test.go b/apps/edge/internal/service/model_queue_test.go index 4ec7f0c..49632c8 100644 --- a/apps/edge/internal/service/model_queue_test.go +++ b/apps/edge/internal/service/model_queue_test.go @@ -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", }, }, diff --git a/configs/edge.yaml b/configs/edge.yaml index 18a2bf7..37739ce 100644 --- a/configs/edge.yaml +++ b/configs/edge.yaml @@ -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: diff --git a/docs/edge-local-dev-guide.md b/docs/edge-local-dev-guide.md index b6960fe..120e923 100644 --- a/docs/edge-local-dev-guide.md +++ b/docs/edge-local-dev-guide.md @@ -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 증가를 기준으로 판정한다.