콘텐츠로 이동

샘플링

목차


샘플링이란 무엇인가

각 디코드 단계에서 모델은 어휘의 모든 토큰에 대한 로짓 점수를 생성합니다. 이는 모델이 해당 토큰이 다음에 올 확률을 나타내는 원시 비정규화 실수입니다. softmax 함수가 이 로짓을 확률 분포로 변환합니다.

샘플링은 생성을 계속하기 위해 이 분포에서 하나의 토큰을 선택하는 과정입니다.

가장 단순한 접근 방식은 항상 가장 높은 확률의 토큰을 선택하는 것입니다. 이는 결정론적이며 유창하고 예측 가능한 출력을 생성하는 경향이 있습니다. 그러나 항상 최고 확률 토큰을 선택하면 반복적이고 지나치게 신중하며 다양성이 부족한 출력이 발생할 수 있습니다. 창의적 작업, 지시 수행, 또는 다양한 응답을 위해서는 약간의 무작위성이 바람직합니다.

샘플링 전략은 완전히 결정론적(탐욕적)에서 완전히 무작위(원시 softmax 분포에서 샘플링)까지의 스펙트럼에 걸쳐 있습니다. mlxcel에서 사용 가능한 전략들로 각 생성이 이 스펙트럼의 어느 위치에 있는지 조정할 수 있습니다.


어휘 분포

순전파 이후, 모델은 [batch, seq_len, vocab_size] 형태의 로짓을 반환합니다. 샘플링 함수는 먼저 마지막 시퀀스 위치의 로짓을 추출하여 [batch, vocab_size] 텐서를 생성합니다. 이 위치가 다음 토큰에 대한 예측을 나타냅니다.

샘플링 전에 이 마지막 위치 로짓 벡터에 모든 확률 변환이 적용됩니다. 연산 순서가 중요합니다. 페널티 조정이 먼저 실행되고 그 다음 온도, 어휘 필터, 마지막으로 범주형 샘플이 수행됩니다.


탐욕적 디코딩

탐욕적 디코딩은 모든 단계에서 가장 높은 로짓을 가진 토큰을 선택합니다. 어휘에 대해 argmax를 적용하는 것과 동일합니다. 같은 checkpoint, prompt, build, device, execution path, 외부 상태에서는 결정론적이지만 kernel, batching, precision, 모델 상태가 달라지면 tie 또는 logit도 달라질 수 있습니다.

mlxcel에서 탐욕적 디코딩은 temperature <= 0.0일 때 활성화됩니다. src/execution/sampling.rs의 build_sampling_config() 함수가 이 케이스를 감지하여 temperature = 0.0과 top_k = 1을 설정합니다.

탐욕적 디코딩은 다양성보다 정확성이 더 중요한 작업에 적합합니다. 코드 완성, 구조화된 출력, 사실 기반 질문 등이 해당됩니다. mlxcel generate CLI의 기본 온도는 0.0으로, --temp를 명시적으로 설정하지 않으면 탐욕적 디코딩이 사용됩니다.


온도 스케일링

온도는 확률 분포의 첨도를 제어합니다. softmax를 적용하기 전에 로짓을 온도 값으로 나누는 방식으로 동작합니다.

P(token_i) = softmax(logits / T)[i]
  • T가 0에 가까울수록: 분포가 매우 뾰족해집니다. 가장 높은 로짓 토큰이 거의 모든 확률을 차지합니다. T=0에서는 탐욕적 디코딩이 됩니다.
  • T = 1.0: 분포가 모델의 원시 출력에서 변경되지 않습니다.
  • T > 1.0: 분포가 더 평탄해집니다. 낮은 확률 토큰이 상대적으로 더 많은 가중치를 얻습니다.
xychart-beta
    title "Effect of Temperature on Token Probabilities"
    x-axis ["token A", "token B", "token C", "token D", "token E"]
    y-axis "Probability" 0 --> 0.8
    line [0.72, 0.18, 0.06, 0.02, 0.01]
    line [0.40, 0.25, 0.18, 0.10, 0.07]
    line [0.25, 0.22, 0.20, 0.17, 0.15]

세 곡선은 각각 동일한 기저 로짓에 적용된 T=0.3(상단, 뾰족함), T=1.0(중간), T=2.0(하단, 평탄함)을 나타냅니다.

0.7에서 0.9 사이의 온도는 채팅 및 지시 모델의 일반적인 시작점입니다. 1.2에서 1.5와 같이 높은 온도는 창의적 글쓰기에 사용됩니다.


Top-K 샘플링

Top-K 샘플링은 샘플링 풀을 가장 높은 확률을 가진 K개의 토큰으로 제한합니다. 상위 K 밖의 토큰은 softmax 이전에 로짓이 음의 무한대로 설정되어 사실상 확률이 0이 됩니다.

예를 들어, K=50이면 가장 가능성이 높은 50개의 토큰만 샘플링될 수 있습니다. 해당 50개 토큰 내의 상대적 확률은 유지됩니다.

Top-K 단독으로는 분포 형태에 관계없이 K가 고정 횟수이기 때문에 문제가 될 수 있습니다. 상위 50개 토큰이 거의 모든 확률 질량을 커버할 때도 있고 30%만 커버할 때도 있습니다. Top-P는 이 문제를 해결하기 위해 설계되었습니다.

mlxcel에서 top_k = 0은 Top-K 필터링을 비활성화하며(모든 토큰이 후보로 유지됩니다), llama-server와 마찬가지로 모델 어휘 크기 이상의 top_k도 같은 효과를 냅니다. 서버의 기본값은 top_k = 40입니다.


Top-P (Nucleus) 샘플링

Top-P(nucleus) 샘플링은 누적 확률이 임계값 P를 초과하는 가장 작은 토큰 집합을 선택합니다. 이 nucleus 밖의 토큰은 로짓이 음의 무한대로 설정됩니다.

예를 들어, P=0.9이면 토큰은 가장 높은 확률에서 가장 낮은 확률 순으로 정렬됩니다. 알고리즘은 누적 확률이 0.9에 도달할 때까지 토큰을 포함하고 나머지는 0으로 만듭니다.

Top-K에 비해 핵심 장점은 nucleus의 크기가 분포의 형태에 적응한다는 것입니다. - 모델이 매우 확신할 때(뾰족한 분포), nucleus는 2-3개의 토큰만 포함할 수 있습니다. - 모델이 불확실할 때(평탄한 분포), nucleus는 수백 개의 토큰을 포함할 수 있습니다.

모델이 확신할 때는 샘플링이 더 제한적이고 불확실할 때는 더 탐색적이어서 일반적으로 더 잘 보정된 출력이 나옵니다.

일반적인 설정은 top_p = 0.9 또는 top_p = 0.95입니다.


Min-P 샘플링

Min-P 샘플링은 분포에서 가장 높은 확률 토큰을 기반으로 동적 최소 확률 임계값을 설정합니다. 토큰은 확률이 min_p * max_probability 이상인 경우에만 샘플링 풀에 포함됩니다.

예를 들어 min_p = 0.05이고 가장 가능성 있는 토큰의 확률이 0.4라면 0.02(0.4의 5%) 이상의 확률을 가진 토큰만 후보로 남습니다.

Top-P처럼 분포의 최고점에 적응하는 방식입니다. 모델이 확신할 때(높은 최대 확률)는 임계값이 올라가 후보 토큰이 줄어들고 모델이 불확실할 때(낮은 최대 확률)는 더 많은 토큰이 후보로 남습니다.

Min-P는 불확실한 상황에서 다양성을 보존하면서 그럴듯하지 않은 토큰을 잘라내는 데 Top-P보다 더 공격적인 경향이 있습니다. min_p = 0.05에서 min_p = 0.1이 일반적인 값입니다.

mlxcel에서 Min-P는 src/lib/mlxcel-core/src/sampling.rs의 min_p_filter()로 구현됩니다. fused_sample() C++ 함수는 FFI 왕복 횟수를 줄이기 위해 온도, Top-K, Top-P, Min-P를 단일 연산으로 처리합니다.

추출 자체는 두 커널이 담당하며 둘 다 기본으로 켜져 있습니다.

  • Categorical 샘플링은 softmax를 쓰지 않는 Gumbel-max 커널을 씁니다. softmax를 만들지 않고 logits에서 바로 뽑습니다. MLXCEL_SAMPLING_GUMBEL=0이면 softmax 경로로 돌아갑니다.
  • Top-K, Top-P, Min-P는 전체 vocabulary 정렬 대신 dual-pivot rejection으로 처리합니다. MLXCEL_SAMPLING_REJECTION=0이면 정렬 경로로 돌아갑니다.

두 폴백은 A/B 측정과 롤백을 위한 것이고, 기본값이 더 빠른 경로입니다.


XTC (Exclude Top Choices)

XTC는 보통의 절단 필터와 목적이 반대입니다. Top-K, Top-P, Min-P가 가능성 낮은 꼬리를 잘라내는 반면 XTC는 확률이 가장 높은 토큰들을 확률적으로 제거해, 그럴듯하면서도 덜 뻔한 토큰이 남게 합니다. 가장 높은 확률의 이어쓰기가 가장 밋밋한 경우가 많은 창작 글쓰기를 겨냥한 기능입니다.

두 파라미터로 조절합니다.

  • xtc_probability(0.0~1.0, 기본 0.0)는 각 토큰에서 필터가 발동할 확률입니다. 0.0이면 XTC가 완전히 꺼집니다.
  • xtc_threshold(0.0~0.5, 기본 0.1)는 제거 후보가 되기 위해 토큰이 넘어야 하는 확률입니다.

필터가 발동하면 임계값을 넘은 토큰 중 가장 확률이 낮은 하나만 남기고 나머지를 제거하므로, 잡음으로 떨어지지 않고 일관된 선택지가 남습니다. end-of-sequence를 포함한 특수 토큰은 제거하지 않으므로 XTC 때문에 모델이 멈추지 못하는 일은 없습니다.

XTC는 요청 단위 opt-in입니다. 서버 수준 CLI 기본값이 없고, 요청에 필드가 없으면 항상 비활성 상태로 해석됩니다. /v1/chat/completions, /v1/completions, /v1/responses, 네이티브 completion 엔드포인트에서 쓸 수 있습니다.


P-less 샘플링

P-less 샘플링은 하이퍼파라미터가 없는 절단(truncation) 필터입니다. 온도로 스케일링한 분포에서 L = sum(p^2)를 계산하고 확률이 L 이상인 토큰만 남깁니다. 분포가 뾰족하면 L이 커서 남는 토큰이 적고, 평평하면 L이 작아 많은 토큰이 남습니다. 확률이 가장 높은 토큰은 항상 남으므로 분포가 비는 일은 없습니다.

P-less는 /v1/chat/completions, /v1/completions, /v1/responses, 네이티브 /completion 엔드포인트의 불리언 p_less 필드(기본값 false)로 요청마다 켭니다. 서버 전역 기본값은 없고 /props에도 나타나지 않습니다. CLI에서는 mlxcel generate --p-less와 mlxcel run --p-less로 켭니다. 탐욕 요청(temperature = 0 또는 top_k = 1)과 mirostat 요청에서는 적용하지 않습니다.

필터의 결합 방식

mlxcel은 llama-server 체인과 같은 고정 순서로 샘플러 단계를 적용합니다.

  1. 토큰 바이어스: logit_bias, 언어 바이어스, 토큰 억제.
  2. 이력 페널티: repetition, DRY, frequency·presence 순서.
  3. 행 필터: 페널티를 적용했지만 온도를 적용하지 않은 로짓에 top-n-sigma, p-less, typical-p 순서로 적용합니다(p-less는 임계값 계산에만 온도를 사용).
  4. Top-K, Top-P, Min-P: 온도를 적용하지 않은 분포에서 평가합니다.
  5. XTC: 필터링 후 다시 정규화한 행에 적용합니다.
  6. 온도와 범주형 샘플: 최종 추출에만 온도 스케일링을 한 번 적용합니다.

Top-K와 Top-P가 모두 활성화된 경우, 토큰이 후보로 남으려면 두 필터를 모두 통과해야 합니다. Min-P는 독립적으로 적용됩니다. 절단 필터가 온도를 적용하지 않은 분포를 읽으므로, 온도는 남은 토큰들의 가중치를 바꿀 뿐 어떤 토큰이 남는지는 바꾸지 않습니다.

필터를 비활성화 값으로 설정하면(topk=0, topp=1.0, min_p=0.0) 파이프라인에서 해당 필터가 효과적으로 제거됩니다.


페널티 기반 방법

페널티 방법은 토큰 이력을 기반으로 로짓 점수를 조정하여 최근에 나타난 토큰을 억제합니다. 이는 분포 필터 이전에 실행됩니다.

반복 페널티

반복 페널티는 모델이 문맥에 나타난 토큰을 재사용하는 것을 억제하는 곱셈 조정입니다. 이력의 각 토큰에 대해:

  • 로짓이 양수이면: 페널티 계수로 나눕니다(더 작게 만듦).
  • 로짓이 음수이면: 페널티 계수를 곱합니다(더 음수로 만듦).

이 비대칭 처리는 양수와 음수 로짓 토큰 모두를 확률을 줄이는 방향으로 원래 값에서 밀어냅니다.

1.0의 페널티는 중립(효과 없음)입니다. 1.1에서 1.3 같은 값은 반복을 강하게 금지하지 않고 가볍게 억제합니다. 1.5 이상의 값은 반복으로 학습된 모델의 출력을 왜곡할 수 있습니다.

반복 페널티는 이력의 모든 고유 토큰을 고려합니다(페널티 계산 전에 중복 제거가 적용됨). 토큰이 몇 번 나타났는지는 고려하지 않습니다.

빈도 및 존재 페널티

두 페널티는 API 호환성을 위해 들여온 OpenAI 스타일 페널티입니다.

빈도 페널티는 각 토큰의 로짓에서 frequency_penalty * count(token)을 빼며 여기서 count(token)은 해당 토큰이 이력에서 나타난 횟수입니다. 이는 빈도에 따라 페널티가 선형적으로 확장되어 과도하게 사용된 토큰을 더 강하게 억제합니다.

존재 페널티는 횟수에 관계없이 이전에 한 번이라도 나타난 모든 토큰의 로짓에서 presence_penalty를 뺍니다. 이전에 본 모든 토큰을 일정하게 억제하는 방식입니다.

두 페널티는 동시에 적용될 수 있습니다. 결합 효과는 다음과 같습니다.

logit[i] -= frequency_penalty * count(i) + presence_penalty * (1 if i in history else 0)

일반적인 값은 0.0에서 2.0 범위입니다. 2.0 이상의 값은 일관성 없는 출력을 생성할 수 있습니다.

DRY 페널티

DRY(Don't Repeat Yourself)는 개별 토큰이 아닌 반복된 구문을 대상으로 하는 더 정교한 페널티입니다. 문맥에서 이미 나타난 토큰 시퀀스를 계속하게 될 토큰을 페널티합니다.

알고리즘은 다음과 같이 동작합니다.

  1. 가장 최근에 생성된 토큰("마지막 토큰")이 나타난 이전 토큰 이력을 스캔합니다.
  2. 이 토큰의 각 이전 출현에 대해 역방향으로 매칭을 확장합니다. 현재 문맥의 선행 토큰과 일치하는 선행 토큰은 몇 개입니까?
  3. 매칭 길이가 dry_allowed_length를 초과하면, 이전 출현 뒤에 온 토큰(반복된 구문을 계속하기 위해 선택될 토큰)에 페널티를 적용합니다.
  4. 페널티 크기: dry_multiplier * dry_base ^ (match_length - dry_allowed_length).

페널티가 dry_base의 지수로 커지므로 반복 구문이 길수록 페널티도 가파르게 커집니다. 허용 길이를 2개 초과한 매칭은 5개 초과한 매칭보다 훨씬 가벼운 페널티를 받아 짧은 반복은 가볍게, 긴 반복은 강하게 억제하는 단계적 반응이 만들어집니다.

DRY 시퀀스 브레이커는 토큰 ID(일반적으로 줄바꿈, 구두점, 또는 형식 토큰)로, 매칭을 종료합니다. 브레이커가 역방향 스캔에 나타나면 매칭이 중지됩니다. 덕분에 반복이 허용되는 자연스러운 문장 경계를 넘어서까지 페널티가 번지지 않습니다.

DRY는 dry_multiplier = 0.0(기본값)일 때 비활성화됩니다. 활성화 시 일반적인 값은 dry_multiplier = 0.8에서 1.0, dry_base = 1.75, dry_allowed_length = 2입니다.

dry_penalty_last_n 파라미터는 llama-server의 약속값을 따라 역방향 탐색 윈도우를 제한합니다. -1은 전체 이력을 고려하고 0은 DRY를 끄며, 1024 같은 값은 가장 최근 1024개 토큰으로 역방향 탐색을 제한합니다. mlxcel generate의 기본값은 -1이고 서버와 mlxcel run의 기본값은 64입니다.


결정론적 출력을 위한 시드 제어

재현 가능한 샘플링을 위해(온도 > 0일 때) SamplingConfig::seed로 시드를 제공할 수 있습니다. 시드가 설정되면 mlxcel은 생성 시작 전에 ffi::seed_rng()를 호출하여 MLX의 난수 생성기를 초기화합니다.

고정 seed는 checkpoint, prompt, parameter, build, hardware, execution path도 같을 때 sampling 재현성을 제공합니다. Batch shape이나 kernel이 달라지면 random value 소비 순서가 바뀔 수 있으므로 cross-version 또는 cross-device byte identity를 보장하지는 않습니다.

seed = None이면 RNG 상태를 reset하지 않고 process의 현재 상태를 사용하므로 결과가 달라질 수 있습니다.


구현: 샘플링 파이프라인

모든 디코딩 경로는 src/lib/mlxcel-core/src/sampling_row_step.rs의 RowSampler라는 하나의 단계로 행을 샘플링합니다. 디코딩 엔진이 서버의 디코딩 행과 프리필 완료 시점, CLI의 generate와 run 모두에서 이 단계를 거쳐 추출하므로 CLI와 서버는 같은 체인을 실행합니다. 토큰 바이어스는 sampling_token_bias.rs의 compose_token_bias가 요청마다 한 번 합성하고, 체인 자체는 src/lib/mlxcel-core/src/sampling.rs에 있습니다.

flowchart LR
    LOGITS["Logits\n[batch, seq, vocab]"] --> SLICE["last-position slice\n[1, vocab]"]
    SLICE --> MASK["structured-output mask\n(server, when set)"]
    MASK --> BIAS["token bias\nlogit_bias, language bias, suppression"]
    BIAS --> REP["repetition penalty"]
    REP --> DRY["DRY penalty"]
    DRY --> FP["frequency/presence penalty"]
    FP --> ROW["row filters\ntop-n-sigma, p-less, typical-p"]
    ROW --> FUSED["fused_sample (C++)\ntop_k + top_p + min_p (untempered)\n+ XTC + temperature + draw"]
    FUSED --> TOKEN["sampled token ID"]

각 페널티와 필터 단계는 파라미터가 비활성 값이면 그래프 노드를 추가하지 않고 건너뜁니다. 페널티 단계는 토큰 이력을 읽는데, 이력을 읽는 단계를 추출하기 전에는 이력이 직전 단계에서 출력한 토큰으로 끝나야 하므로 파이프라인 호출자는 그 토큰을 먼저 호스트에서 읽습니다. 피드백 상태는 있지만 이력을 읽지 않는 샘플러(mirostat, adaptive-p)는 지연 그래프에서 곧바로 다음 추출을 만듭니다.

추출은 GPU 평가를 강제하지 않고 샘플링한 토큰을 반환하므로, 디코딩 루프는 GPU를 기다리기 전에 다음 단계의 순전파를 제출할 수 있습니다. 서버 디코딩 배치의 모든 행이 융합 조건을 만족하고 파라미터가 같으면, 스케줄러는 행마다 따로 추출하지 않고 [B, vocab] 배치 전체를 융합 호출 한 번으로 추출합니다.


투기적 디코딩

투기적 디코딩은 더 빠른 drafter가 여러 candidate를 제안하고 target 모델이 이를 검증합니다. Exactness 계약은 drafter mode에 따라 다릅니다.

프롬프트 조회(mlxcel generate --prompt-lookup, 서버 --draft-kind prompt-lookup)와 풀 모델 드래프터(일반 체크포인트를 가리키는 --draft-model)는 제안 토큰이 target 샘플러로 새로 뽑은 토큰과 같을 때만 수락하므로 원-핫 제안에 대해 무손실이며, temperature 0에서는 그 토큰이 argmax입니다. temperature 0에서 블록 검증이 plain decode 스트림과 같은 출력을 내는지는 보장되지 않습니다(#2778). MTP와 DFlash는 별도 round loop를 사용하며 각자의 greedy exactness, penalty, qualification, engagement 조건이 있으므로 “모든 speculative mode가 분포를 보존한다”고 포괄해서는 안 됩니다. 투기적 디코딩을 참조하십시오.

일반적인 작은 체크포인트는 FullModelDrafter(src/lib/mlxcel-core/src/speculative/full_model_drafter.rs)가 드래프팅합니다. 이 드래프터는 자체 KV 캐시로 argmax를 제안하고, 프롬프트 조회가 쓰는 Engine::verify_round가 그대로 검증합니다. 별도의 draft-model decode 루프는 없습니다. mlxcel generate와 mlxcel run의 --draft-model, mlxcel serve와 mlxcel-server의 --draft-model 또는 --model-draft는 일반 모델 디렉터리(DFlash 계열 표식도 MTP model_type도 없는 디렉터리)를 full-model로 자동 감지합니다. --draft-kind <dflash|mtp|full-model>로 명시할 수 있고, 서버 entry point에서는 prompt-lookup도 받습니다. 문서화된 곳에서는 --draft, --draft-max도 llama-server-compatible alias입니다. --draft-block-size는 모든 드래프터 종류에 공통인 단일 제안 폭 설정입니다. config.json이 없거나 형식이 잘못된 --draft-model은 시작할 때 config 오류로 실패합니다.

유용한 풀 모델 드래프터는 target보다 충분히 빠르고 tokenizer와 vocabulary가 호환되어야 합니다. Verification overhead 때문에 오히려 느려질 수 있으므로 round당 accepted token과 end-to-end latency를 측정하십시오.


설정 참조

모든 샘플링 파라미터는 src/execution/sampling.rs의 SamplingConfig로 조립됩니다.

파라미터 타입 기본값 (CLI) 기본값 (서버) 설명
temperature f32 0.0 0.8 분포 첨도. 0 = 탐욕적.
top_k i32 0 40 Top-K 필터. 0 또는 어휘 크기 이상의 값 = 비활성화.
top_p f32 1.0 0.9 Nucleus 샘플링 임계값. 1.0 = 비활성화.
min_p f32 0.0 0.1 Min-P 임계값. 0.0 = 비활성화.
seed Option\<u64> None None RNG 시드. None = 무작위.
repetition_penalty f32 1.0 1.0 반복 페널티. 1.0 = 비활성화.
frequency_penalty f32 0.0 0.0 OpenAI 빈도 페널티.
presence_penalty f32 0.0 0.0 OpenAI 존재 페널티.
dry_multiplier f32 0.0 0.0 DRY 승수. 0.0 = 비활성화.
dry_base f32 1.75 1.75 DRY 지수 베이스.
dry_allowed_length usize 2 2 DRY가 적용되기 전 최소 매칭 길이.
dry_penalty_last_n usize -1(전체 이력) 64 DRY 역방향 탐색 윈도우. CLI의 -1 = 전체 이력, 0 = DRY 비활성. mlxcel run은 지정하지 않으면 서버 기본값을 씁니다.
dry_sequence_breakers Vec\<i32> [] "\n", ":", "\"", "*"의 토큰 DRY 매칭을 중단하는 토큰 ID. 서버는 중단 문자열을 토큰화해 쓰며, --dry-sequence-breaker none은 중단 문자열을 두지 않습니다.
xtc_probability f32 0.0 0.0 XTC 발동 확률. 0.0이면 비활성화.
xtc_threshold f32 0.1 0.1 XTC 후보 임계값. xtc_probability > 0.0일 때만 적용됩니다.
p_less bool false false P-less 절단. 요청 단위로만 켜며 서버 플래그는 없습니다.

temperature <= 0.0이면 build_sampling_config()는 다른 필터 설정에 관계없이 탐욕적 경로를 활성화합니다. 페널티 파라미터는 탐욕적 모드에서도 여전히 적용됩니다.