콘텐츠로 이동

연속 배칭

목차


배칭이란?

언어 모델이 포워드 패스를 실행할 때, 메모리에서 모든 가중치 텐서를 컴퓨팅 유닛으로 로드하고 행렬 곱셈을 수행하며 출력 로짓을 생성합니다. 대형 모델의 경우 이 메모리 로딩 비용은 동시에 처리되는 토큰 수에 관계없이 일정합니다.

배칭은 이 특성을 활용합니다. 요청당 포워드 패스를 한 번씩 실행하는 대신, 여러 요청을 함께 그룹화하여 단일 패스로 처리합니다. 가중치 로딩 비용이 한 번만 지불되고 배치의 모든 요청에 걸쳐 분산되므로 처리량이 크게 향상됩니다.


정적 배칭 vs 연속 배칭

정적 배칭은 대략 동시에 도착한 요청을 그룹화하고 처음부터 끝까지 함께 처리하며 현재 배치가 완료된 후에야 새 배치를 시작합니다. 이 방식에는 치명적인 비효율이 있습니다. 배치의 한 요청이 일찍 완료되면 다른 모든 요청이 끝날 때까지 해당 컴퓨팅 슬롯이 놀게 됩니다. 오래 걸리는 요청이, 이미 끝난 요청에는 더 이상 필요 없는 슬롯을 붙들고 있는 셈입니다.

연속 배칭은 전체 요청 단위가 아닌 개별 디코드 단계(반복) 수준에서 동작하여 이 낭비를 제거합니다. 각 단계마다:

  • 이전 단계에서 완료된 요청이 즉시 배치에서 제거됩니다.
  • 큐에서 대기 중인 새 요청이 가용 슬롯을 채우기 위해 추가됩니다.

배치 구성은 모든 디코드 단계 후에 동적으로 변경됩니다. 연속 배칭으로 분당 100개의 요청을 처리하는 서버는 정적 배칭에서처럼 각 그룹의 가장 느린 요청이 완료되기를 기다리며 지연되는 대신 컴퓨팅 슬롯을 지속적으로 사용합니다.

gantt
    title Static Batching vs Continuous Batching
    dateFormat X
    axisFormat %s

    section Static Batching
    Batch 1 (req A + B + C)  :a1, 0, 8
    Idle (A done early)      :crit, a2, 3, 8
    Batch 2 (req D + E + F)  :a3, 8, 16

    section Continuous Batching
    Request A                :b1, 0, 3
    Request B                :b2, 0, 6
    Request C                :b3, 0, 8
    Request D (joins at t=3) :b4, 3, 9
    Request E (joins at t=6) :b5, 6, 11
    Request F (joins at t=8) :b6, 8, 13

연속 배칭 타임라인에서 Request A가 t=3에 완료되는 즉시 Request D가 해당 슬롯을 채웁니다. 유휴 구간이 없습니다.


mlxcel의 반복 수준 스케줄링

mlxcel의 배치 스케줄러는 틱(tick) 단위로 동작합니다. 각 틱마다:

  1. 채널에서 들어오는 요청을 드레인(비차단 방식). 새 요청이 프리필 큐에 추가됩니다.
  2. 이 틱의 액션을 결정합니다:
  3. 청크 프리필이 진행 중이면 다음 프리필 청크와 디코드 단계를 삽입합니다.
  4. 배치에 여유 공간이 있고 프리필 큐가 비어 있지 않으면, 새 시퀀스 하나를 받아들여 프리필합니다.
  5. 배치에 활성 디코딩 시퀀스가 있으면, 배치 디코드 단계를 실행합니다.
  6. 배치와 큐가 모두 비어 있으면, 다음 요청이 올 때까지 블록됩니다.
  7. 선택된 액션을 실행합니다.
  8. 완료된 시퀀스를 완료 처리합니다: KV 캐시를 해제하고 누적된 토큰을 대기 중인 HTTP 응답으로 전송합니다.

틱마다 무엇을 실행할지는 스케줄러가 정하고, 실행은 엔진이 맡습니다. mlxcel_core::engine::Engine이 모델과 KV 풀을 소유하며, 모델 forward, 샘플러 추출, finish 단계는 모두 Engine::prefill, Engine::step(행 수와 무관한 단일 진입점이라 요청 하나도 크기 1인 배치로 처리), 또는 파이프라인용 submit / finish_rows 쌍을 거칩니다. 크기 1인 배치는 행별 샘플링 체인으로 샘플링하고, 행들이 융합 가능한 샘플링 파라미터를 공유하는 더 큰 배치는 융합된 [B, vocab] -> [B] 추출 한 번으로 처리합니다. 샘플링된 토큰은 읽기 전에 실패를 반환할 수 있는 MLX 경계를 거쳐 평가되므로, 이 평가 중 MLX 오류가 나면 서버를 중단하지 않고 해당 요청만 inference backend error로 끝냅니다. admission, 틱 정책, 선점, 프롬프트 캐시, 분리형 handoff, 투기적 burst 생성기는 스케줄러에 남아 있습니다.

요청 라이프사이클은 다음 상태 기계를 따릅니다:

stateDiagram-v2
    [*] --> Queued: Request arrives
    Queued --> Prefilling: Admitted to batch
    Prefilling --> Decoding: Prefill complete
    Decoding --> Finished_Stop: EOS token generated
    Decoding --> Finished_Length: max_tokens reached
    Decoding --> Finished_Cancelled: Client disconnected
    Decoding --> Finished_Error: Internal error
    Decoding --> Queued: Preempted (re-queued)
    Finished_Stop --> [*]
    Finished_Length --> [*]
    Finished_Cancelled --> [*]
    Finished_Error --> [*]

기능

시퀀스별 KV 캐시 격리

배치의 각 시퀀스는 자체 KV 캐시를 가집니다. 캐시는 해당 시퀀스에서 이전에 처리된 모든 토큰에 대한 모든 어텐션 헤드와 레이어의 키 및 값 텐서를 저장합니다. 이 격리는 다음을 의미합니다:

  • 생성의 다른 단계에 있는 시퀀스들이 서로 간섭하지 않습니다.
  • 시퀀스를 배치에 추가하거나 제거해도 다른 시퀀스의 캐시에 영향을 미치지 않습니다.
  • 완료된 시퀀스의 캐시가 즉시 해제되어 새 요청을 위한 메모리가 확보됩니다.

청크 프리필

매우 긴 프롬프트의 요청이 도착하면, 전체 프롬프트를 한 번의 프리필 패스로 처리하는 데 비례적으로 더 오랜 시간이 걸립니다. 이 시간 동안 활성 디코딩 시퀀스는 디코드 단계를 받지 못하여 다음 토큰까지의 시간이 증가합니다.

청크 프리필은 프롬프트를 청크로 나누어 틱당 하나씩 처리하고, 청크 사이에 활성 시퀀스를 위한 디코드 단계를 삽입합니다. --prefill-chunk-size를 지정하지 않으면 다른 시퀀스가 디코딩하지 않는 동안은 2048토큰(단일 스트림의 첫 토큰 시간 유지), 디코드 배치가 돌고 있는 동안은 512토큰(admission이 실행 중인 스트림에 주는 영향 제한)을 청크로 씁니다. --prefill-chunk-size, -b/--batch-size(LLAMA_ARG_BATCH), MLXCEL_PREFILL_CHUNK 중 하나로 값을 명시하면 두 상태 모두 그 값으로 고정됩니다. CLI(generate, run)는 2048을 씁니다.

flowchart LR
    P1["프리필\n청크 1"] --> D1["디코드\n단계"] --> P2["프리필\n청크 2"] --> D2["디코드\n단계"] --> dots["..."] --> FT["첫 번째\n디코드 토큰"]

긴 프롬프트가 활성 시퀀스를 굶기지 않게 됩니다. 대신 새 요청의 첫 토큰까지 걸리는 시간이 청크 수에 비례해 늘어나고 활성 요청은 응답성을 유지합니다.

Llama-3.2-1B 4-bit 스트림 4개가 디코딩하는 중에 8192토큰 요청을 받아들이는 경우를 측정하면, 2048토큰 청크에서는 스트림 ITL p95가 약 128 ms, 새 요청의 TTFT가 약 2초였고 512에서는 ITL p95가 약 56 ms, TTFT가 약 4초였습니다. 적응형 기본값은 이 절충이 실제로 생기는 동안에만 512를 씁니다.

CLI와 서버의 모든 프리필 경로는 프리필 계획 하나를 만듭니다. 채택한 프롬프트 캐시 prefix는 건너뛰고, 스냅샷 계열의 채팅 프롬프트는 history 경계에서 한 번 나누며, 나머지는 청크 조각으로 자르고, 모델이 opt-in한 경우에만 M5 이후 하드웨어에서 조각을 타일 크기에 맞춰 패딩합니다.

--prefill-chunk-size 0으로 설정하면 청크 분할이 완전히 비활성화됩니다. 해당 모드에서는 프롬프트가 단일 패스로 프리필되며 짧은 프롬프트가 많은 워크로드에서 청크 분할 오버헤드가 기존 시퀀스에 대한 지연 이점보다 크지 않을 때 적합합니다.

선점적 축출

--enable-preemption이 설정되고 배치가 꽉 찬 경우, 높은 우선순위의 들어오는 요청이 활성 우선순위가 낮은 시퀀스를 축출하여 슬롯을 확보할 수 있습니다:

  1. 스케줄러가 선점 정책에 따라 활성 배치에서 피해자 시퀀스를 선택합니다.
  2. 피해자의 생성된 토큰이 저장되고 KV 캐시가 해제됩니다.
  3. 피해자가 프리필 큐에 다시 삽입됩니다. 슬롯이 가용해지면 다시 받아들여지고 재프리필됩니다.
  4. 높은 우선순위 요청이 즉시 프리필됩니다.

선점 정책은 피해자로 선택될 활성 시퀀스를 제어합니다:

정책 동작
longest-first (기본값) 가장 많은 토큰을 생성한 시퀀스를 축출합니다.
lowest-priority 우선순위가 가장 낮은 시퀀스를 축출합니다. 동점 시 가장 많은 토큰을 생성한 시퀀스가 선택됩니다.

선점은 시퀀스를 축출하고 재프리필하는 것이 컴퓨팅을 낭비하므로 기본적으로 비활성화되어 있습니다. 전체 처리량보다 높은 우선순위 요청의 낮은 지연 시간 처리가 더 중요한 경우에만 활성화하십시오.

텐서 배치 디코딩

여러 시퀀스가 활성 배치에 있을 때, mlxcel은 시퀀스당 별도의 forward() 호출 대신 단일 forward_batched() 호출을 실행합니다. MLX는 지연 평가를 사용합니다. 계산 그래프가 모든 시퀀스에 대해 함께 구성되고 단일 eval() 호출로 평가됩니다.

이는 배치 크기에 관계없이 모델 가중치 텐서를 배치당 한 번만 메모리에서 로드하면 되기 때문에 중요합니다. 메모리 대역폭에 병목이 있는 추론의 지배적인 비용인 가중치 로딩 비용이 모든 활성 시퀀스에 걸쳐 분산됩니다.

명시적인 forward_batched() 구현을 가진 모델(예: Llama 3)의 경우 배칭이 더욱 효율적입니다:

  1. 임베딩 조회, 정규화, Q/K/V 프로젝션, 출력 프로젝션, FFN이 단일 배치 연산으로 실행됩니다.
  2. 어텐션은 시퀀스별로 계산됩니다(각 시퀀스가 다른 KV 캐시 오프셋과 RoPE 위상을 가지므로 필수).
  3. 최종 정규화와 언어 모델 헤드가 다시 배치됩니다.

이 분할 어텐션 패턴은 시퀀스별 캐시 구조를 유지하면서 분산 이점을 극대화합니다.

배치 decode는 파이프라인으로 동작합니다. 스케줄러가 현재 토큰을 읽기 전에 다음 단계를 제출하므로 CPU 측 처리와 GPU 작업이 겹칩니다. logit_bias나 ignore_eos를 담은 요청도 이 lookahead 파이프라인에 남습니다. 행별 토큰 bias는 fused sampling dispatch 전에 로짓에 더해지며, bias가 없는 행은 비트 단위로 그대로 유지됩니다.

recurrent 및 선형 어텐션 계열(Mamba, Mamba 2, RWKV 7, Jamba, Falcon-H1, Granite MoE Hybrid, Kimi Linear, RecurrentGemma 등)은 상태를 시퀀스 ID로 구분되는 모델 소유 저장소에 두므로 다른 계열과 똑같이 배치됩니다. supports_batching()이 false를 반환하는 모델(예: DiffusionGemma, LLaDA2-MoE, Florence-2, Nemotron-Parse, Nemotron VoiceChat, Falcon-OCR, Phi-4 멀티모달)은 배치 크기 1로 제한되며, 서버는 does not support batched decode; clamping max_batch_size 로그를 남깁니다. 블록 디퓨전 모델(DiffusionGemma)은 전용 직렬 워커로 서빙되며 배치 스케줄러에 참여하지 않습니다. 블록 디퓨전을 참조하십시오.

네이티브 배치 디코딩

배칭을 지원하는 모델은 두 가지 다중 행 디코드 경로 중 하나로 실행됩니다. BatchedDecodeKind::Native는 모든 행이 하나의 forward 그래프에서 모델 가중치를 공유하므로 배치가 커질수록 전체 디코드 처리량이 늘어납니다. BatchedDecodeKind::PerRowFallback은 트레이트의 기본값으로, 행들은 스케줄러에서 함께 배치되지만 각 행이 자기 forward 패스를 따로 실행하므로 --parallel을 늘려도 전체 처리량이 늘지 않습니다. 워커가 시작될 때 서버는 로드된 모델이 쓰는 경로를 로그로 남깁니다.

Model <id> decode path: native batched forward
Model <id> decode path: per-row fallback (aggregate decode throughput will not scale with --parallel)

네이티브 배치 디코딩은 dense, MoE, MLA 트랜스포머, Gemma 3n과 Gemma 4, GPT-OSS 및 모델 소유 캐시를 쓰는 계열, recurrent 및 선형 어텐션 계열, Qwen VL과 그 밖의 VLM 래퍼(텍스트 타워에 위임), 대부분의 텐서 병렬 런타임(Llama, Qwen 3, Qwen 3.5, Gemma 3, ERNIE 4.5, Hunyuan V1 Dense)을 포함합니다. Baichuan, Granite 4 Vision, Llama 4(C++ 모델), Muse Glimmer 텍스트, Qwen4-Exp, 파이프라인 병렬 서버 모델, 텐서 병렬 Gemma 4 모델은 아직 행별 경로를 씁니다. 계열별 분류는 src/models/batched_decode_coverage_tests.rs에 고정되어 있습니다.

배치 디코딩에는 측정된 격차가 있습니다. M1 Ultra에서 일부 체크포인트의 B=1 디코드가 배치 디코드 작업 이전보다 3~6% 낮고, 프롬프트 캐시에 재사용할 접두부가 있는 상태에서 Mixtral은 B=8에서도 B=1 속도보다 낮게 유지됩니다. 두 가지 모두 알려진 이슈 lablup/mlxcel#2817로 추적합니다. M1과 M2에서 2~6행 밀집 곱은 GEMV에 머물며, MLXCEL_DENSE_GEMV_ROWS=0은 업스트림 밀집 디스패치로 되돌립니다.


Paged KV 캐시와 프리픽스 공유

--decode-storage-backend paged에서는 서버가 요청 간 프롬프트 프리픽스 캐시와 시퀀스별 디코드 상태를 하나의 refcount + copy-on-write 블록 풀에 함께 보관합니다. 프롬프트 프리픽스를 공유하는 두 요청은 그 프리픽스의 KV 블록을 한 번만 저장합니다. 두 번째 요청은 다시 프리필하는 대신 첫 요청의 블록을 참조로 adopt하고 한 시퀀스의 내용이 공유 블록에서 갈라질 때만 블록이 copy-on-write로 분기합니다.

Paged adopt/donate는 풀 기반 FP16 계열(Qwen 3, Llama 3 같은 dense-natural 백엔드)에 적용됩니다. 모델이 상태를 직접 소유하는 계열(Gemma 3, Llama 4, Qwen 3.5)과 recurrent 또는 하이브리드 SSM 모델은 dense 캐시나 모델 소유 캐시를 유지하며 풀에 들어가지 않습니다.

블록 풀은 --kv-cache-budget <BYTES|auto|none>(환경 변수 MLXCEL_KV_CACHE_BUDGET)으로 제한하며 기본값은 auto입니다. auto는 사용 가능 메모리, 가중치, activation headroom에서 상한을 도출하고 바이트 값은 명시적인 cap을 설정하며 none 또는 0은 opt-out하여 풀을 무제한으로 둡니다. 예산이 걸리면 스케줄러는 먼저 차가운 캐시 프리픽스를 축출하고 다음으로 선점하며 그래도 부족할 때만 요청을 거부합니다. GET /v1/cache/stats가 paged 블록 사용량(블록 크기, 할당·활성·여유 블록 수, 예약·사용 바이트, 예산)을 보고합니다.

캐시된 prefix를 채택한 요청은 suffix가 새로 만들 블록만큼만 예산에서 차감되는데, 채택한 블록은 이미 사용 중이기 때문입니다. 디코드는 forward 전에 블록을 예약합니다. 실행 중인 행에 블록이 필요한데 빈 블록이 없으면 오래 쓰이지 않은 프롬프트 캐시 prefix를 먼저 축출하고, 다음으로 대기 중인 요청이 채택한 prefix를 버리며(그 요청은 admission될 때 콜드로 다시 프리필), 마지막으로 진행이 가장 적은 실행 중 행을 선점합니다.

M1 Ultra에서 qwen3-0.6b-4bit(토큰당 약 112 KiB KV)로 측정하면, 8개 요청이 공유하는 1024 토큰 프리픽스는 여덟 번 대신 한 번만 저장되어 약 784 MiB를 절약하고 adopt하는 요청은 공유 프리필을 통째로 생략하면서도 cold 실행과 byte 단위로 동일하게 디코드합니다. Batched paged 디코드는 네이티브 block-table 커널을 사용하는데 gather 방식 대비 512 토큰 프롬프트에서 1.9배, 4096 토큰에서 10.9배 빠릅니다(batch 4). gather 경로는 매 스텝 visible 윈도우를 다시 구성하기 때문에 컨텍스트가 길수록 격차가 벌어집니다.

예산이 빠듯할 때의 디코드 여유분

동시 작업량에 비해 예산이 작으면 빈 블록에 꼭 맞춰 요청을 받아들였을 때 실행 중인 행이 늘어날 공간이 없습니다. 그러면 다음 디코드 틱에서 진행이 가장 적은 행이 선점되고, 그 행은 다시 admission될 때 처음부터 다시 프리필합니다. --kv-admission-watermark F(환경 변수 MLXCEL_KV_ADMISSION_WATERMARK, 범위 0.0부터 0.5, 기본값 0.01)는 admission 시점에 블록 예산의 F 비율을 비워 둡니다. 빈 블록에서 F * budget을 뺀 양이 요청을 감당할 때만 요청을 받아들이며, 그 공간을 만들려고 오래 쓰이지 않은 프롬프트 캐시 prefix를 축출하고 우선순위가 더 높은 요청이면 우선순위가 엄격히 낮은 행을 선점한 뒤에 기다립니다. 워터마크는 디코드 배치가 돌고 있을 때만 적용되므로 예산 안에 들어가는 요청을 거절하지 않고, 실행 중인 행이 끝날 때까지 기다리게 합니다. 0이면 끄고, 블록 예산이 없으면 효과가 없습니다.

GB10에서 meta-llama-3.1-8b-instruct-4bit, --kv-cache-budget 600000000(4444 블록), --max-batch-size 4, 600토큰을 생성하는 3턴 대화 세 개를 동시에 돌려 측정한 결과입니다.

워터마크 선점 횟수 디코드 회수 패스 완료한 턴 소요 시간
0 10 43 9 / 9 103-104 s
0.01 6 6 9 / 9 90-91 s
0.02 6 6 9 / 9 89-90 s
0.05 5 5 9 / 9 89-90 s
0.10 1 2 9 / 9 86 s (1회 실행)
0.15 1 3 9 / 9 88 s (1회 실행)

기본값은 이 측정 범위에서 턴을 잃지 않고 선점을 줄인 가장 작은 값입니다. 예산이 빠듯해 선점이 자주 일어나고 새 요청의 큐잉 지연이 늘어도 괜찮다면 값을 올리십시오.


CLI와 서버 사이의 토큰 일치

mlxcel run(-p 유무와 무관)과 채팅 REPL은 서버의 모델 워커를 같은 프로세스 안에서 슬롯 하나로 시작하고, 턴마다 /v1/chat/completions 요청 경로로 보냅니다(CLI 레퍼런스 참조). MLXCEL_SDPA_DETERMINISTIC=1에서 같은 채팅 요청을 보내면 mlxcel run -p와 서버의 탐욕적 출력은 완전히 같습니다. 이 불변식은 make engine-parity MODEL=<dir>로 검사합니다. 테스트를 참조하십시오.

서버 기능 두 가지는 logit이 거의 같은 지점에서 탐욕적 토큰을 바꿀 수 있어서, 불변식에 포함하지 않고 측정된 차이로 다룹니다.

  • 프롬프트 캐시 히트는 캐시된 prefix 뒤의 suffix만 forward하므로 프리필 분할이 달라집니다. 채택한 prefix가 콜드 계획의 분할 지점(스냅샷 계열의 history 경계 또는 청크 경계)에서 끝날 때만 콜드 실행을 정확히 재현합니다. 채팅 REPL은 캐시를 켠 채로, run -p는 끈 채로 실행합니다.
  • 다른 시퀀스가 함께 돌고 있는 연속 배칭.

서버에서 단일 요청 경로를 재현하려면 --max-batch-size 1 또는 --decode-storage-backend dense에 --no-prompt-cache를 더하십시오. 클라이언트가 요청마다 cache_prompt: false를 보낼 수도 있습니다. 재현 가능한 탐욕적 출력을 참조하십시오.


연속 배칭에서의 투기적 디코딩

DFlash, MTP, 프롬프트 조회 투기적 디코딩은 배치 스케줄러 안에서 실행됩니다. 프롬프트 조회(--draft-kind prompt-lookup)은 제안이 있는 행이 한 틱에 혼자 검증 라운드를 하나 실행하고 나머지 행은 배치 단계를 밟습니다. 검증 라운드는 활성 배치의 행 수가 --prompt-lookup-max-batch(기본 2) 이하이고 통합 KV 예산이 없을 때(--kv-unified이면 실행하지 않음)만 실행되며, paged 스토리지에서는 전체 컨텍스트가 4608토큰 이하일 때만 실행됩니다. Gemma 4 배치 MTP(MLXCEL_ENABLE_MTP_BATCH)는 row-wise 구성에서 시퀀스별 디코딩과 바이트 단위로 같고, CUDA에서 sliding window 안의 최대 4행 윈도우에 대해 실행되며 Metal과 ROCm에서는 사용하지 않습니다. 투기적 디코딩과 MTP와 DFlash를 참조하십시오.


설정

다음 mlxcel serve 플래그들이 배치 스케줄러를 제어합니다:

플래그 기본값 설명
--n-parallel <N>, --parallel <N> 4 요청 슬롯 수. 명시적 --ctx-size의 기본 나눗수이기도 합니다.
--max-batch-size <N> n_parallel 활성 디코드 배치의 최대 시퀀스 수.
--max-queue-depth <N> 32 프리필 큐에서 대기하는 최대 요청 수.
--prefill-chunk-size <N> 디코딩 중인 시퀀스가 없으면 2048, 있으면 512 프리필 청크당 토큰 수. 값을 명시하면(-b, LLAMA_ARG_BATCH, MLXCEL_PREFILL_CHUNK 포함) 두 상태 모두 고정됩니다. 0은 청크 분할을 비활성화합니다.
--prefill-grant-interval <N> 16 Parked chunk가 guaranteed prefill grant 전에 양보하는 decode tick 수. 낮추면 TTFT가 개선되고 decode ITL이 늘며 0은 무제한 대기를 복원합니다.
--max-batch-prefill <N> 4 대기 중인 요청을 하나의 batched prefill pass로 합칠 최대 개수.
--max-batch-prefill-tokens <N> 파생값 한 batched prefill의 padded-token cap. 지정하지 않으면 2 * max_batch_prefill * min(prefill_chunk_size, 512)입니다. 0이면 비활성화합니다.
--enable-preemption false 우선순위가 낮은 시퀀스의 선점적 축출을 활성화합니다.
--preemption-policy <POLICY> longest-first 축출 피해자 선택: longest-first 또는 lowest-priority.
--no-batch false 배치 스케줄러를 비활성화하고 레거시 순차 워커로 서빙합니다.
--decode-storage-backend <auto\|dense\|paged> auto 디코드 KV 스토리지 백엔드. auto는 paged dispatch가 검증된 계열(Llama, Qwen 3, Qwen 3.5 text, Gemma 3 text, Llama 4, Helium)에 paged 디코드를 선택하고 나머지는 dense 디코드를 유지합니다.
--kv-cache-budget <BYTES\|auto\|none> auto 통합 paged KV 캐시 풀의 블록 예산. none 또는 0은 무제한으로 둡니다.
--kv-admission-watermark <F> 0.01 행이 디코딩 중일 때 프리필 admission이 비워 두는 블록 예산 비율(0.0부터 0.5). 0이면 끕니다. 예산이 빠듯할 때의 디코드 여유분을 참조하십시오.
--prompt-lookup-max-batch <N> 2 프롬프트 조회 행이 자기 검증 라운드를 실행할 수 있는 최대 활성 배치 크기.

연속 배칭이 활성화된 상태에서 명시적 --ctx-size C는 llama.cpp 서버 동작과 같이 활성 슬롯들이 공유하는 서버 전체 컨텍스트 예산입니다. 슬롯별 윈도우는 --max-batch-size가 설정되어 있으면 floor(C / --max-batch-size), 없으면 floor(C / --n-parallel)입니다. --no-batch는 슬롯 1개를 사용해 명시적 컨텍스트 예산 전체를 유지합니다. /slots와 /health는 이 슬롯별 실제 컨텍스트 크기를 보고하며 슬롯당 512토큰 미만이면 서버 시작이 거부됩니다.

예시: 고동시성 설정:

mlxcel serve \
  --model ./models/Qwen2.5-7B-Instruct \
  --max-batch-size 8 \
  --max-queue-depth 64 \
  --prefill-chunk-size 512 \
  --enable-preemption \
  --preemption-policy lowest-priority

이 예시는 두 스케줄러 상태 모두에서 프리필 청크를 512로 고정합니다. 적응형 기본값을 쓰려면 --prefill-chunk-size를 생략하십시오.

예시: 낮은 지연 시간 단일 요청 설정:

mlxcel serve \
  --model ./models/Llama-3.1-8B-Instruct \
  --max-batch-size 1 \
  --prefill-chunk-size 0

또는 동일하게:

mlxcel serve \
  --model ./models/Llama-3.1-8B-Instruct \
  --no-batch

입장 제어

프리필 큐가 --max-queue-depth에 도달하면, 큐 공간이 확보될 때까지 서버가 새 요청 수락을 중단합니다. 큐가 꽉 찬 상태에서 도착하는 요청은 HTTP 503 Service Unavailable을 받습니다:

{"error": {"message": "Server is busy. Retry later.", "type": "overloaded"}}

이 역압(back-pressure) 메커니즘은 큐에 쌓인 요청으로 인한 무제한 메모리 증가를 방지합니다. 배포 환경의 예상 트래픽 패턴과 허용 가능한 큐잉 지연 시간을 기반으로 --max-queue-depth를 설정하십시오.

Metal의 텍스트 전용 Nemotron-H는 요청마다 메모리 추정치도 확인하고, 들어가지 않으면 HTTP 503 request_memory_budget_exceeded로 응답합니다. Nemotron-H 요청 메모리 admission을 참조하십시오.


모니터링

헬스 엔드포인트에는 스케줄러가 활성화된 경우 배치 상태가 포함됩니다:

curl -s http://localhost:8080/health | jq .
{
  "status": "ok",
  "batch": {
    "active_sequences": 3,
    "queued_requests": 1,
    "total_completed": 412
  }
}

/slots 엔드포인트(기본적으로 활성화)는 슬롯별 상태를 표시합니다:

curl -s http://localhost:8080/slots | jq .

/metrics 엔드포인트(--metrics로 활성화)는 Prometheus 호환 카운터를 제공합니다:

curl -s http://localhost:8080/metrics | grep mlxcel_batch

성능 데이터

다음 처리량 측정값은 Apple M1 Ultra(128 GB)에서 다양한 배치 크기에서의 배칭 이점을 보여주기 위해 수집된 것입니다. C=4는 4개의 동시 요청, C=8은 8개의 동시 요청을 의미합니다.

모델 파라미터 C=4 향상 C=8 향상
SmolLM 135M 2.27x 2.69x
Qwen2 0.5B 1.90x 2.44x
Qwen3 0.6B 1.49x 2.11x
MiniCPM 2B 1.60x 1.75x
Phi-3-mini 3.8B 1.49x 1.49x
DeepSeek-R1 7B 1.46x 1.50x
Qwen2.5 7B 1.27x 1.52x
Llama-3.1 (4-bit) 8B 1.23x 1.40x

스케일링 관찰:

  • 소형 모델(1B 미만) 은 가중치 로딩 분산이 지배적인 요인이므로 배칭에서 가장 큰 이점을 얻습니다.
  • 중형 모델(1~4B) 은 배치 크기 4~8까지 좋은 스케일링을 보입니다.
  • 대형 모델(7~8B) 은 배치 크기 1에서 이미 메모리 대역폭에 병목이 있으므로 적당한 향상을 보입니다.
  • 첫 번째 토큰까지의 시간(TTFT) 은 동시 요청 수가 1에서 8로 증가할 때 약 3~6배 증가합니다. 프롬프트가 순차적으로 프리필되기 때문입니다. 청크 프리필은 활성 디코드 시퀀스에 대한 영향을 줄이지만 새로 도착하는 요청의 TTFT 증가를 완전히 제거하지는 않습니다.