콘텐츠로 이동

7.1. --parallel 슬롯 벤치마크 및 상한선 결정 근거

이슈: lablup/backend.ai-go#3025

요약

Backend.AI GO는 사용자에게 노출되는 --parallel (동시 요청 슬롯 수) 값을 llama-servermlxcel-server 양쪽 모두에 대해 고정된 범위로 제한(clamp)합니다:

경계 이전 (3025 이전) 변경 후 (이번 이슈) 정의 위치
최솟값 1 1 (변경 없음) src-tauri/src/process/parallel_slots.rsPARALLEL_SLOTS_MIN
최댓값 4 (문서화되지 않음) 8 src-tauri/src/process/parallel_slots.rsPARALLEL_SLOTS_MAX
슬롯당 컨텍스트 하한선 (경고) (없음) 1024 토큰 src-tauri/src/process/parallel_slots.rsPARALLEL_SLOTS_PER_SLOT_FLOOR

상한선은 모델에 따라 가변적이지 않고 단일 상수입니다 — 결정: 상수 vs. 모델 인식 참고.

context_size ÷ parallel1024 토큰 미만이 되는 설정을 사용자가 저장하기 전에 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-659parallel.clamp(1, 4) 그대로였으며, 상한에 대한 인라인 주석이나 설계 문서가 없었습니다. src/types/modelConfig.ts:363-373src/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::vectorn_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을 늘리면 다음 중 하나가 발생합니다:

  1. 고정된 --ctx-size에 대해 슬롯당 컨텍스트가 줄어들거나 (n_ctx / n_parallel),
  2. 슬롯당 컨텍스트를 유지하기 위해 사용자가 --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.cppserver_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, macOS vm_stat 페이지, Windows GlobalMemoryStatusEx), 관련된 양은 사용자가 다른 워크로드를 끝낸 이후의 빈 RAM입니다. 슬롯당 KV 증가량은 양자화, 모델 아키텍처 (MoE 활성 전문가 풋프린트는 dense 모델과 다름), llama-server 내부 cache-type 설정에 따라 달라집니다. 이 중 어느 하나라도 잘못 계산하면, 실제로 로딩되는 것과 모순되는 모델 인식 상한선이 됩니다.
  • 슬롯당 컨텍스트 하한선 경고가 이미 주요 안전 케이스를 포착합니다. "슬롯 수를 선택했더니 슬롯당 컨텍스트가 쓸 만한 수준 이하로 줄어드는" 실패 모드가 에이전트 워크플로우에 가장 해롭습니다 — 순수 OOM이 아닙니다. 경고는 구현되어 활성화되어 있습니다. 모델 인식 상한선은 이 케이스에 대해서는 중복입니다.
  • 모델 인식 상한선은 하위 호환적 추가 변경입니다. evaluate_parallel_slots는 클램프 + 경고 평가의 단일 진입점입니다. 미래의 반복에서 model_info: ModelInfoavailable_memory_bytes: u64를 받아 두 전송 표면(Tauri 명령, REST 엔드포인트) 또는 슬라이더 경계를 변경하지 않고도 실효 상한선을 좁힐 수 있습니다.

슬롯당 컨텍스트 하한선 (1024 토큰)

슬롯당 컨텍스트 하한선은 context_size ÷ parallel이 현실적인 에이전트 프롬프트(시스템 프롬프트 + 도구 목록 + 최근 몇 턴의 컨텍스트)를 담기에 너무 작다고 간주되는 값입니다. 사용자가 선택한 (parallel, context_size) 쌍이 각 슬롯을 하한선 미만으로 만들면, evaluate_parallel_slotsBelowPerSlotFloor 경고를 반환하고, 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.tsevaluateParallelSlotsLocal에 로컬 미러가 있습니다. 적용 / 저장 시점의 권위 있는 답은 Rust 평가기입니다.

벤치마크 방법론

아래의 수치는 이 섹션의 절차로 수집되었습니다. 향후 상한선을 변경하기 전에 재실행하세요.

llama-server (대표 모델 1종)

  • 모델: unsloth/Qwen3-4B-Instruct-2507-GGUFQ4_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 8192mlxcel-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-690ServerConfig로의 적용 시점 통합
  • src-tauri/src/commands/model_config.rsevaluate_parallel_slots Tauri 명령
  • src-tauri/src/management_api/handlers/model_config.rsevaluate_parallel_slots REST 핸들러
  • src/types/modelConfig.ts — TypeScript 미러 (PARALLEL_SLOTS_MAX, evaluateParallelSlotsLocal)
  • src/components/ModelConfigDrawer/ContextTab.tsx — UI 슬라이더 + 슬롯당 하한선 경고
  • .claude/rules/api-parity.md — Single Source of Truth 요구사항