feat(edge): node resource model unification - update phase/SDD docs and edge service
- Move node-resource-model-unification milestone and SDD to archive - Update PHASE.md for operational-observability-provider-management - Refactor edge node registration and model queue logic - Add status provider tests for model queue validation - Update edge config and Go config structs
This commit is contained in:
parent
1204a06960
commit
e30fb2494e
10 changed files with 80 additions and 25 deletions
|
|
@ -13,7 +13,7 @@ Mac dev-runtime에서는 Codex CLI와 MLX vLLM provider를 하나의 `mac-codex-
|
|||
|
||||
## 상태
|
||||
|
||||
[검토중]
|
||||
[완료]
|
||||
|
||||
## 구현 잠금
|
||||
|
||||
|
|
@ -51,11 +51,11 @@ Node를 Edge 연결 owner로 두고, 실행 제어는 Node 하위 resource/provi
|
|||
|
||||
## 완료 리뷰
|
||||
|
||||
- 상태: 검토중
|
||||
- 상태: 통과
|
||||
- 요청일: 2026-06-29
|
||||
- 완료 근거: `contract-schema`, `node-admission`, `edge-routing-status`, `dev-runtime-profile` 모두 PASS complete log와 `Roadmap Completion`을 남겼고, SDD Acceptance Scenario S01-S07과 Evidence Map을 충족한다.
|
||||
- 검토 항목: archive 전환 전 최종 완료 확인
|
||||
- 리뷰 코멘트: 남은 차단 항목 없음. 완료 archive 전환은 별도 승인 후 진행한다.
|
||||
- 완료 근거: `contract-schema`, `node-admission`, `edge-routing-status`, `dev-runtime-profile` 모두 PASS complete log와 `Roadmap Completion`을 남겼고, SDD Acceptance Scenario S01-S07과 Evidence Map을 충족한다. 종료 전 코드레벨 검토에서 provider catalog zero-capacity status fallback과 stale CLI help 문구를 보정했고 `go test -count=1 ./...`가 통과했다.
|
||||
- 검토 항목: 완료 archive 전환
|
||||
- 리뷰 코멘트: 남은 차단 항목 없음. 2026-06-29 완료 archive로 이동한다.
|
||||
|
||||
## 범위 제외
|
||||
|
||||
|
|
@ -20,9 +20,9 @@ provider 확장 Phase에서 검증한 Ollama, vLLM, SGLang, Lemonade 같은 추
|
|||
- 경로: `agent-roadmap/archive/phase/operational-observability-provider-management/milestones/provider-catalog-device-status.md`
|
||||
- 요약: `models[]`를 외부 model key이자 운영 catalog model로 두고, Node 하위 provider와 served model list를 기준으로 Edge가 provider 선택, load-ratio routing, target rewrite, read-only catalog 상태를 소유하는 MVP 계약을 완료했다.
|
||||
|
||||
- [검토중] Node Resource Model Unification
|
||||
- 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- 요약: Node를 Edge 연결 identity로 두고 CLI, OpenAI-compatible provider, 기타 resource를 같은 Node 아래 나열하며 provider/resource capacity만 concurrency를 소유하도록 runtime 계약과 dev-runtime 구성을 정렬했고, 완료 근거와 archive 전환 검토를 남긴다.
|
||||
- [완료] Node Resource Model Unification
|
||||
- 경로: `agent-roadmap/archive/phase/operational-observability-provider-management/milestones/node-resource-model-unification.md`
|
||||
- 요약: Node를 Edge 연결 identity로 두고 CLI, OpenAI-compatible provider, 기타 resource를 같은 Node 아래 나열하며 provider/resource capacity만 concurrency를 소유하도록 runtime 계약과 dev-runtime 구성을 정렬했다.
|
||||
|
||||
- [스케치] 사용량, 토큰, 로그 운영 추적 MVP
|
||||
- 경로: `agent-roadmap/phase/operational-observability-provider-management/milestones/usage-token-log-ops-mvp.md`
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ directly in the default flow.`,
|
|||
c.Flags().StringVar(®Adapter, "adapter", "cli", "node adapter: ollama or cli")
|
||||
c.Flags().StringVar(®OllamaBaseURL, "ollama-base-url", "", "ollama base URL")
|
||||
c.Flags().IntVar(®OllamaContextSize, "ollama-context-size", 2048, "ollama context size")
|
||||
c.Flags().IntVar(®RuntimeConcurrency, "runtime-concurrency", 1, "runtime concurrency limit")
|
||||
c.Flags().IntVar(®RuntimeConcurrency, "runtime-concurrency", 1, "legacy runtime concurrency metadata")
|
||||
c.Flags().StringVar(®Target, "target", "", "bootstrap target platform (e.g. darwin-arm64, linux-amd64)")
|
||||
c.Flags().StringVar(®ArtifactBaseURL, "artifact-base-url", "", "artifact base URL for bootstrap download")
|
||||
|
||||
|
|
|
|||
|
|
@ -532,11 +532,6 @@ func (m *modelQueueManager) getSnapshotForNode(nodeID string, rec *edgenode.Node
|
|||
|
||||
var snaps []*iop.ProviderSnapshot
|
||||
|
||||
concurrencyFallback := 1
|
||||
if rec.Runtime.Concurrency > 0 {
|
||||
concurrencyFallback = rec.Runtime.Concurrency
|
||||
}
|
||||
|
||||
// Catalog-first: nodes with a providers[] catalog emit only catalog snapshots.
|
||||
// Adapter snapshots (sections 1-4) are omitted to prevent duplicate metrics
|
||||
// when a node has both adapter instances and provider-pool catalog entries.
|
||||
|
|
@ -546,9 +541,6 @@ func (m *modelQueueManager) getSnapshotForNode(nodeID string, rec *edgenode.Node
|
|||
continue
|
||||
}
|
||||
capVal := prov.Capacity
|
||||
if capVal <= 0 {
|
||||
capVal = concurrencyFallback
|
||||
}
|
||||
inflight, queued := m.getStatsForProviderLocked(nodeID, prov.ID)
|
||||
|
||||
servedModels := make([]string, len(prov.Models))
|
||||
|
|
@ -581,6 +573,10 @@ func (m *modelQueueManager) getSnapshotForNode(nodeID string, rec *edgenode.Node
|
|||
}
|
||||
|
||||
// Legacy adapter snapshots for nodes with no providers catalog.
|
||||
concurrencyFallback := 1
|
||||
if rec.Runtime.Concurrency > 0 {
|
||||
concurrencyFallback = rec.Runtime.Concurrency
|
||||
}
|
||||
|
||||
// 1. CLI
|
||||
if rec.Adapters.CLI.Enabled {
|
||||
|
|
|
|||
|
|
@ -471,8 +471,9 @@ func providerCanServe(prov config.NodeProviderConf, servedModel string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// providerDispatchable checks whether a provider has a non-empty adapter and
|
||||
// valid capacity so it can be used for dispatch.
|
||||
// providerDispatchable checks whether a provider has a non-empty adapter.
|
||||
// Capacity is checked separately so zero-capacity providers remain visible in
|
||||
// status snapshots while unavailable for provider-pool dispatch.
|
||||
func providerDispatchable(prov config.NodeProviderConf) bool {
|
||||
return strings.TrimSpace(prov.Adapter) != ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,16 +72,12 @@ func NodeEntrySnapshot(entry *edgenode.NodeEntry) NodeSnapshot {
|
|||
// zero inflight/queued for nodes whose config has a providers[] catalog but
|
||||
// whose service has no queue (e.g. queue disabled at startup).
|
||||
func staticProviderCatalogSnapshots(rec *edgenode.NodeRecord) []*iop.ProviderSnapshot {
|
||||
concurrencyFallback := positiveOr(rec.Runtime.Concurrency, defaultNodeCapacity)
|
||||
snaps := make([]*iop.ProviderSnapshot, 0, len(rec.Providers))
|
||||
for _, prov := range rec.Providers {
|
||||
if prov.ID == "" {
|
||||
continue
|
||||
}
|
||||
capVal := prov.Capacity
|
||||
if capVal <= 0 {
|
||||
capVal = concurrencyFallback
|
||||
}
|
||||
servedModels := make([]string, len(prov.Models))
|
||||
copy(servedModels, prov.Models)
|
||||
lifecycleCaps := make([]string, len(prov.LifecycleCapabilities))
|
||||
|
|
|
|||
|
|
@ -276,6 +276,66 @@ func TestListNodeSnapshotsProviderCatalogSupersedesAdapterSnapshots(t *testing.T
|
|||
}
|
||||
}
|
||||
|
||||
func TestListNodeSnapshotsCatalogProviderZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) {
|
||||
reg := edgenode.NewRegistry()
|
||||
reg.Register(&edgenode.NodeEntry{NodeID: "node-zero-cap", Alias: "node-zero-cap"})
|
||||
|
||||
store := edgenode.NewNodeStore()
|
||||
rec := &edgenode.NodeRecord{
|
||||
ID: "node-zero-cap",
|
||||
Adapters: config.AdaptersConf{
|
||||
OpenAICompatInstances: []config.OpenAICompatInstanceConf{
|
||||
{Name: "vllm-gpu", Enabled: true, Capacity: 7},
|
||||
},
|
||||
},
|
||||
Providers: []config.NodeProviderConf{
|
||||
{ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0},
|
||||
},
|
||||
Runtime: config.RuntimeConf{Concurrency: 7},
|
||||
}
|
||||
store.Add(rec)
|
||||
|
||||
bus := edgeevents.NewBus()
|
||||
svc := New(reg, bus)
|
||||
svc.SetNodeStore(store)
|
||||
|
||||
snaps := svc.ListNodeSnapshots()
|
||||
if len(snaps) != 1 {
|
||||
t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
|
||||
}
|
||||
if len(snaps[0].ProviderSnapshots) != 1 {
|
||||
t.Fatalf("expected 1 provider snapshot, got %d", len(snaps[0].ProviderSnapshots))
|
||||
}
|
||||
p := snaps[0].ProviderSnapshots[0]
|
||||
if p.Id != "prov-zero" {
|
||||
t.Fatalf("expected provider id prov-zero, got %q", p.Id)
|
||||
}
|
||||
if p.Capacity != 0 {
|
||||
t.Fatalf("provider catalog capacity must not fall back to runtime concurrency: got %d", p.Capacity)
|
||||
}
|
||||
if p.LoadRatio != 0 {
|
||||
t.Fatalf("expected zero load ratio for zero capacity provider, got %.2f", p.LoadRatio)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticProviderCatalogSnapshotsZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) {
|
||||
rec := &edgenode.NodeRecord{
|
||||
ID: "node-static-zero-cap",
|
||||
Providers: []config.NodeProviderConf{
|
||||
{ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0},
|
||||
},
|
||||
Runtime: config.RuntimeConf{Concurrency: 7},
|
||||
}
|
||||
|
||||
snaps := staticProviderCatalogSnapshots(rec)
|
||||
if len(snaps) != 1 {
|
||||
t.Fatalf("expected 1 provider snapshot, got %d", len(snaps))
|
||||
}
|
||||
if snaps[0].Capacity != 0 {
|
||||
t.Fatalf("static provider catalog capacity must not fall back to runtime concurrency: got %d", snaps[0].Capacity)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListNodeSnapshotsLegacyAdapterFallbackWhenNoProviders verifies that when
|
||||
// a node has no providers[] catalog, ListNodeSnapshots returns the legacy adapter
|
||||
// snapshots (CLI, Ollama, etc.) rather than empty results.
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ openai:
|
|||
# nodes[].providers[].adapter — concrete adapter instance key used by edge to dispatch.
|
||||
# nodes[].providers[].models — served model names this provider can serve.
|
||||
# nodes[].providers[].health — observed health state string.
|
||||
# nodes[].providers[].capacity — max concurrent execution slots.
|
||||
# nodes[].providers[].capacity — provider-pool max concurrent execution slots; 0 is not dispatchable.
|
||||
# nodes[].providers[].max_queue — max queue depth (per-provider).
|
||||
# nodes[].providers[].queue_timeout_ms — queue timeout in milliseconds.
|
||||
# nodes[].providers[].lifecycle_capabilities — coarse lifecycle capabilities list.
|
||||
|
|
|
|||
|
|
@ -128,7 +128,9 @@ type NodeProviderConf struct {
|
|||
Models []string `mapstructure:"models" yaml:"models,omitempty"`
|
||||
// Health is the observed provider health state.
|
||||
Health string `mapstructure:"health" yaml:"health,omitempty"`
|
||||
// Capacity is the maximum concurrent execution slots.
|
||||
// Capacity is the provider/resource maximum concurrent execution slots.
|
||||
// Provider-pool dispatch treats 0 as not dispatchable rather than falling
|
||||
// back to node runtime concurrency metadata.
|
||||
Capacity int `mapstructure:"capacity" yaml:"capacity,omitempty"`
|
||||
// MaxQueue is the maximum queue depth.
|
||||
MaxQueue int `mapstructure:"max_queue" yaml:"max_queue,omitempty"`
|
||||
|
|
|
|||
Loading…
Reference in a new issue