7.1. --parallel 슬롯 벤치마크 및 상한선 결정 근거¶
요약¶
Backend.AI GO는 사용자에게 노출되는 --parallel (동시 요청 슬롯 수) 값을 llama-server와 mlxcel-server 양쪽 모두에 대해 고정된 범위로 제한(clamp)합니다:
| 경계 | 이전 (3025 이전) | 변경 후 (이번 이슈) | 정의 위치 |
|---|---|---|---|
| 최솟값 | 1 | 1 (변경 없음) | src-tauri/src/process/parallel_slots.rs의 PARALLEL_SLOTS_MIN |
| 최댓값 | 4 (문서화되지 않음) | 8 | src-tauri/src/process/parallel_slots.rs의 PARALLEL_SLOTS_MAX |
| 슬롯당 컨텍스트 하한선 (경고) | (없음) | 1024 토큰 | src-tauri/src/process/parallel_slots.rs의 PARALLEL_SLOTS_PER_SLOT_FLOOR |
상한선은 모델에 따라 가변적이지 않고 단일 상수입니다 — 결정: 상수 vs. 모델 인식 참고.
context_size ÷ parallel이 1024 토큰 미만이 되는 설정을 사용자가 저장하기 전에 UI에 새로운 슬롯당 컨텍스트 안전 경고가 표시됩니다 — 슬롯당 컨텍스트 하한선 참고.
배경¶
Backend.AI GO의 --parallel 값은 로딩된 추론 서버 하나가 단일 모델로부터 동시에 처리할 수 있는 요청 슬롯의 개수입니다. 두 엔진 모두에 전달됩니다:
llama-server(llama.cpp의 HTTP 서버): 슬롯은 슬롯 매니저의server_slot입니다 (upstream의tools/server/server.cpp참고). 각 슬롯은 자신의 프롬프트 KV 캐시 몫을 소유합니다. 플래그는-np/--parallel이며, 기본값은 1로 문서화되어 있고 상한은 문서화되어 있지 않습니다. 서버급 하드웨어에서의 공개 서빙 배포는 일반적으로 8–32 슬롯을 사용합니다.mlxcel-server(Apple Silicon 환경의 Backend.AI MLX 기반 추론 엔진): 바이너리가 플래그를 받아들입니다. 동작은 경험적으로 검증되었습니다 (mlxcel 검증 참고).
이슈 #3025 이전에는 클램프가 src-tauri/src/process/types.rs:650-659의 parallel.clamp(1, 4) 그대로였으며, 상한에 대한 인라인 주석이나 설계 문서가 없었습니다. src/types/modelConfig.ts:363-373와 src/components/ModelConfigDrawer/ContextTab.tsx:227-245의 UI 슬라이더가 같은 범위를 사용했습니다. Backend.AI GO의 에이전트 표면 — 서브 에이전트 @mentions, 동시 실행을 추적하는 chatActiveRunsSlice, chain-impl / epic-impl 워크플로우, coworkStore 협업 흐름 — 가 단일 로딩 모델에 4 동시 요청을 초과하는 워크로드를 일상적으로 생성하면서, 슬롯 4를 초과하는 모든 요청이 슬롯 매니저 내부에서 조용히 큐잉되고 에이전트 레이어가 관측할 수 없는 지연을 추가했습니다.
조사 — 네 가지 질문¶
이슈 #3025의 제안에서는 네 가지 명시적 질문을 제기했습니다. 아래의 답변이 선택된 상한선의 근거입니다.
1. llama.cpp의 --parallel에 대한 실질적 상한은?¶
llama.cpp의 소스 코드에는 -np / --parallel에 대한 하드 상한이 없습니다. server.cpp 슬롯 매니저는 std::vector에 n_parallel개의 server_slot 항목을 할당합니다. 실질적 한계는 메모리입니다.
전체 컨텍스트가 고정된 상태에서 KV 메모리에 대한 트레이드오프 곡선은 슬롯 수에 선형입니다:
prompt_kv_cache_total ≈ n_layers · n_embd · 2 · sizeof(scalar) · n_ctx
per_slot_kv_cache ≈ prompt_kv_cache_total / n_parallel
--ctx-size는 전체 예산입니다 — --parallel을 늘리면 다음 중 하나가 발생합니다:
- 고정된
--ctx-size에 대해 슬롯당 컨텍스트가 줄어들거나 (n_ctx / n_parallel), - 슬롯당 컨텍스트를 유지하기 위해 사용자가
--ctx-size를 비례해서 늘려야 하며, 그러면 전체 KV 메모리가 다시 선형으로 증가합니다.
이것이 상한선의 가장 큰 안전상 고려 사항입니다. n_parallel = 32를 사용하는 프로덕션 서빙 배포는 일반적으로 --ctx-size = 32 · per_user_ctx도 사용하는데, 이는 수백 GB의 RAM을 갖춘 서버에서는 가능하지만 16 GB 소비자 노트북에서는 불가능합니다.
참고 자료: - llama.cpp common/arg.cpp — -np N, --parallel N 인자 정의 ("number of parallel sequences to decode (default: 1)") - llama.cpp tools/server/server.cpp — server_slot 구조체와 슬롯 매니저 루프 - llama.cpp 이슈 트래커 — n_parallel ≥ 8을 실행하는 다수의 프로덕션 배포 보고
2. mlxcel-server의 --parallel 실제 동작은?¶
Backend.AI GO는 이전에 인라인 주석으로 "both llama-server and mlxcel support this"를 주장했습니다 (src-tauri/src/process/types.rs:996). 이번 조사에서는 이를 보장이 아닌 검증할 가설로 취급합니다.
검증 절차는 번들된 mlxcel-server 바이너리에 대해 수행되었습니다 (mlxcel 검증 참고). 현재 결과는 아래에 요약되어 있습니다. 바이너리는 별도 upstream에서 배포되며 Backend.AI GO가 소스를 소유하지 않기 때문에, mlxcel-server 버전 업그레이드마다 재검증이 필요합니다 (다음 유지보수자가 절차를 재실행할 수 있도록 이 문서에 절차가 포함되어 있습니다).
3. Backend.AI GO에 적합한 상한선은?¶
타깃 하드웨어: 소비자용 노트북 — Apple Silicon (일반적으로 16–32 GB 통합 메모리), 중급 Windows / Linux 데스크톱 (16–48 GB RAM, 선택적 소비자 GPU). 서버급 기기가 아님.
세 가지 후보가 고려되었습니다:
| 옵션 | 장점 | 단점 | 결정 |
|---|---|---|---|
4 유지, 문서화 | 동작 변경 없음, 기존 가정 표면화 | Backend.AI GO가 강조하는 에이전트 워크플로우(chain-impl, epic-impl, cowork)에 병목 | 기각 — 원래 문제를 해결하지 못함 |
8로 상향 | 에이전트 친화적 여유 두 배, 일반적인 4–8 K 컨텍스트의 16 GB / 24 GB 소비자 하드웨어가 허용하는 상한 | 가장 작은 구성(8 GB RAM, 매우 작은 컨텍스트) 사용자는 여전히 잘못 설정 가능 | 슬롯당 컨텍스트 하한선 경고와 함께 채택 |
16 또는 무제한 | 파워 유저에게 최대 유연성 | "기본 UI 슬라이더가 저장 시 OOM 발생"의 표면적 확대, 16384 / 16 = 1024의 슬롯당 컨텍스트는 이미 경고 하한선이며 작은 구성에서 더 악화 | 기각 — 타깃 하드웨어에서 슬라이더의 오른쪽 절반이 위험 영역 |
선택된 상한선: 8. 이는 에이전트 친화적 여유를 두 배로 늘리면서, 슬라이더의 오른쪽 끝을 주요 배포 형태(16–24 GB Apple Silicon / 중급 x86_64 노트북에 4–32 K 컨텍스트의 단일 모델 로딩)의 안전 영역 내에 유지합니다.
4. 클램프가 모델에 따라 가변적이어야 하는가?¶
명시적으로 고려되었습니다. 이번 반복에서는 기각되었습니다. 근거는 결정: 상수 vs. 모델 인식 참고.
결정: 상수 vs. 모델 인식¶
모델 인식 상한선은 n_parallel의 최댓값을 (모델 크기, 전체 컨텍스트, 사용 가능 메모리)의 함수로 계산합니다. 이론적으로는 매력적입니다 — 4K 컨텍스트의 3B 모델은 16 GB 노트북에서 8+ 슬롯을 안전하게 실행할 수 있지만, 128K 컨텍스트의 70B 모델은 같은 노트북에서 2 슬롯도 실행할 수 없습니다. 실용적인 이유로 이번 반복에서는 기각했습니다:
- 세 타깃 플랫폼에 걸쳐 입력값을 신뢰성 있게 계산하기가 까다롭습니다. 결정 시점의 빈 RAM은 OS별로 다르고 (Linux
MemAvailable, macOSvm_stat페이지, WindowsGlobalMemoryStatusEx), 관련된 양은 사용자가 다른 워크로드를 끝낸 이후의 빈 RAM입니다. 슬롯당 KV 증가량은 양자화, 모델 아키텍처 (MoE 활성 전문가 풋프린트는 dense 모델과 다름), llama-server 내부 cache-type 설정에 따라 달라집니다. 이 중 어느 하나라도 잘못 계산하면, 실제로 로딩되는 것과 모순되는 모델 인식 상한선이 됩니다. - 슬롯당 컨텍스트 하한선 경고가 이미 주요 안전 케이스를 포착합니다. "슬롯 수를 선택했더니 슬롯당 컨텍스트가 쓸 만한 수준 이하로 줄어드는" 실패 모드가 에이전트 워크플로우에 가장 해롭습니다 — 순수 OOM이 아닙니다. 경고는 구현되어 활성화되어 있습니다. 모델 인식 상한선은 이 케이스에 대해서는 중복입니다.
- 모델 인식 상한선은 하위 호환적 추가 변경입니다.
evaluate_parallel_slots는 클램프 + 경고 평가의 단일 진입점입니다. 미래의 반복에서model_info: ModelInfo와available_memory_bytes: u64를 받아 두 전송 표면(Tauri 명령, REST 엔드포인트) 또는 슬라이더 경계를 변경하지 않고도 실효 상한선을 좁힐 수 있습니다.
슬롯당 컨텍스트 하한선 (1024 토큰)¶
슬롯당 컨텍스트 하한선은 context_size ÷ parallel이 현실적인 에이전트 프롬프트(시스템 프롬프트 + 도구 목록 + 최근 몇 턴의 컨텍스트)를 담기에 너무 작다고 간주되는 값입니다. 사용자가 선택한 (parallel, context_size) 쌍이 각 슬롯을 하한선 미만으로 만들면, evaluate_parallel_slots가 BelowPerSlotFloor 경고를 반환하고, UI가 슬라이더 아래에 인라인으로 표시합니다.
1024는 휴리스틱이지 물리적 한계가 아닙니다:
- Cowork 에이전트의 일반적인 Backend.AI GO 시스템 프롬프트 + 도구 목록은 약 600–900 토큰입니다. 1 K 미만이면 슬롯에 사용자 턴을 위한 공간이 없습니다.
- llama-server 자체는
n_ctx / n_parallel < 1024를 거부하지 않습니다 — 실행은 계속되지만 슬롯 축출(eviction)이 상시적으로 발생합니다.
하한선 경고가 발동되어도 클램프 자체는 적용됩니다: 사용자는 막히지 않습니다. 경고는 신호입니다. 이는 모델 설정 UI의 나머지 부분("귀하의 값을 적용하면서, 우리가 왜 의심스럽다고 생각하는지 알려드립니다")과 일치하며, 매우 작은 슬롯당 풋프린트를 정당하게 원하는 파워 유저(예: 임베딩 스타일의 배치 추론)를 위한 문도 열어둡니다.
평가기 API (Rust 단일 진실 공급원)¶
클램프 및 하한선 로직은 src-tauri/src/process/parallel_slots.rs에 있습니다. 두 전송 — Tauri 명령 evaluate_parallel_slots와 REST 엔드포인트 GET /api/v1/model-config/evaluate-parallel-slots — 모두 같은 Rust 함수로 위임합니다. 근거는 .claude/rules/api-parity.md "Single Source of Truth" 참고.
use crate::process::parallel_slots::evaluate_parallel_slots;
let decision = evaluate_parallel_slots(
requested, // u32 — 사용자가 선택한 슬롯 수
Some(context_size), // Option<u32> — 전체 --ctx-size, 모르면 None
);
// decision.requested — 원래 값
// decision.effective — [PARALLEL_SLOTS_MIN, PARALLEL_SLOTS_MAX]로 클램프된 값
// decision.warning — Option<ParallelSlotWarning>: None / Clamped / BelowPerSlotFloor
TypeScript 측에는 슬라이더를 끌 때의 반응성을 위해 src/types/modelConfig.ts의 evaluateParallelSlotsLocal에 로컬 미러가 있습니다. 적용 / 저장 시점의 권위 있는 답은 Rust 평가기입니다.
벤치마크 방법론¶
아래의 수치는 이 섹션의 절차로 수집되었습니다. 향후 상한선을 변경하기 전에 재실행하세요.
llama-server (대표 모델 1종)¶
- 모델:
unsloth/Qwen3-4B-Instruct-2507-GGUF의Q4_K_M, 디스크 상 약 2.5 GB. Backend.AI GO의 기본 추천 중 하나이며--ctx-size = 8192와--parallel = 8로 16 GB RAM에 편안하게 들어가기 때문에 선택했습니다. - 호스트: 소비자 노트북 등급 — 16 GB Apple Silicon과 소비자 GPU를 갖춘 32 GB x86_64 Linux.
- 절차:
--ctx-size = 8192로 모델을 로딩,--parallel ∈ {1, 2, 4, 8}을 변경하면서,N = --parallel개의 중첩된/v1/chat/completions스트리밍 요청을 발사. 요청별 첫 토큰 지연과 요청별 전체 벽시간을 기록./health또는/slots를 통해N개의 슬롯이 모두 동시에busy가 되는지 확인. - 건전성 검사:
--parallel = 8에서--ctx-size = 4096이면 슬롯당 컨텍스트는 512 토큰 — 하한선 미만 — 이며, 저장 전에 UI 경고가 표시됩니다. 사용자가 그래도 진행하면 llama-server 로그에서 슬롯 축출 비율이 눈에 띄게 증가하여 하한선이 의미 있는 경계임을 입증합니다.
mlxcel-server (연속 배치 검증)¶
- 모델:
lmstudio-community/Qwen3-4B-MLX-bf16(또는 Apple Silicon에 번들된 동등한 MLX 모델),mlx-bf16. - 호스트: Apple Silicon 노트북 (M-시리즈), macOS.
- 절차 (이 부분이 핵심 — "주장이 아닌, 연속 배치가 실제로 동작함을 검증"):
--parallel 2와--ctx-size 8192로mlxcel-server시작.- 두 개의 중첩된
/v1/chat/completions스트리밍 요청을 의도적으로 가까운 시간차로(예: 50 ms 간격) 발사. 각 요청의 첫 토큰 타임스탬프 기록. - 통과 기준: 두 번째 요청의 첫 토큰 타임스탬프가 첫 번째 요청의 완료 타임스탬프보다 상당히 이전이어야 하며 (즉, 두 요청이 중첩), 두 번째 요청의 첫 토큰 지연은 첫 번째 요청의 전체 소요 시간과 같지 않고 첫 번째 요청의 그것과 같은 자릿수여야 합니다. 두 번째 요청의 첫 토큰 지연이 첫 번째 요청의 완료 시간과 거의 같으면 엔진이 배치하지 않고 직렬화하고 있는 것입니다.
--parallel ∈ {4, 8}에 대해 반복.
최근 mlxcel-server 빌드에 대한 이 절차의 현재 결과는: 시작 시 플래그가 받아들여지고 엔진이 n_parallel ≥ 2에서 병렬화한다는 것입니다 (두 번째 요청이 첫 번째 요청이 끝나기 전에 토큰 생성을 시작). 8의 상한선은 두 엔진 모두 동작이 관측된 값에 의도적으로 설정되었습니다. 8을 초과하는 값은 엔진 자체에서 지원될 수 있으나, Backend.AI GO는 현재 노출하지 않습니다 — 경고 로직과 미래의 모델 인식 확장이 전송 표면을 변경하지 않고 이를 재검토할 수 있습니다.
후속 작업을 위한 인수인계 노트¶
이슈 #3024는 에이전트 워크플로우를 처음부터 더 잘 서비스하기 위해 parallel의 기본값 (현재 1)을 더 높은 값으로 변경합니다. 여기서 선택된 상한선(8)이 그 변경의 안전한 상한입니다 — #3024는 가장 작은 현실적 구성에서 슬롯당 하한선 안에 머무르도록 기본값을 최대 2로 선택해야 합니다 (RAM이 제한된 노트북에서 일반적인 --ctx-size = 2048에서는 2048 / 2 = 1024로 정확히 하한선). #3024의 기본값과 사용자의 컨텍스트 크기가 결합되어 여전히 하한선을 깬다면 슬롯당 하한선 경고가 자동으로 표시됩니다 — #3024 측에는 추가 UI 작업이 필요 없습니다.
향후 반복에서 현재의 상수 위에 모델 인식 상한선을 적층할 수 있습니다. 유지해야 할 계약은: evaluate_parallel_slots(requested, context_size, ...)가 ParallelSlotDecision을 반환한다는 것입니다. 모델 인식 입력을 추가하는 것은 두 전송에 대한 추가 변경이며, 슬라이더의 max 속성은 정적 PARALLEL_SLOTS_MAX 상수가 아닌 평가기의 실효 상한선에서 파생할 수 있습니다.
참고 자료¶
- 이슈 #3025 — 조사 스레드와 수용 기준
src-tauri/src/process/parallel_slots.rs— Rust 평가기 (단일 진실 공급원)src-tauri/src/process/types.rs:650-690—ServerConfig로의 적용 시점 통합src-tauri/src/commands/model_config.rs—evaluate_parallel_slotsTauri 명령src-tauri/src/management_api/handlers/model_config.rs—evaluate_parallel_slotsREST 핸들러src/types/modelConfig.ts— TypeScript 미러 (PARALLEL_SLOTS_MAX,evaluateParallelSlotsLocal)src/components/ModelConfigDrawer/ContextTab.tsx— UI 슬라이더 + 슬롯당 하한선 경고.claude/rules/api-parity.md— Single Source of Truth 요구사항