L4 한 장에서 3B 모델을 서빙하는 중에, 그 Pod를 내리지 않고 LoRA adapter를 학습해 실시간으로 반영했다. 재기동은 준비 patch 한 번뿐이었고 그 뒤 학습 3회와 adapter 교체는 전부 서버가 살아 있는 채로 이루어졌다.
증거는 Pod의 재시작 횟수다. adapter를 붙이기 전과 비교가 끝난 뒤가 같다.
RESTARTS 0 AGE 2026-08-05T01:10:58Z
model 목록은 그 사이에 하나에서 둘로 늘었다가 다시 하나로 줄었다. 재시작 없이 목록이 바뀌는 것은 런타임 반영으로만 설명된다.
학습은 서빙과 같은 GPU에서 돌렸다. 시작 전 계산은 9,300 MiB였고 실측은 9,452 MiB로 1.6% 차이였다. 그동안 서빙 쪽 점유는 5,118 MiB에서 한 번도 움직이지 않았다. adapter를 붙이고 호출한 뒤에도 같은 값이다 — LoRA 슬롯은 기동 시점에 선점되므로 adapter를 갈아끼우는 데 추가 memory가 들지 않는다.
시점
used
free
학습 전
5,118 MiB
17,448 MiB
최대
14,570 MiB
7,996 MiB
학습 후
5,118 MiB
17,448 MiB
학습 자체는 세 번 걸렸다. 1회차와 2회차는 형식을 전혀 배우지 못했고, 세 번째에야 학습 데이터의 원인: / 조치: 두 줄이 서빙 응답에 나타났다.
1회차 0/4 문항2회차 0/4 문항3회차 3/4 문항
두 번의 실패 원인이 서로 달랐고, 둘 다 loss 곡선만 봐서는 보이지 않았다. 1회차의 loss는 0.59까지 떨어졌지만 그중 대부분이 padding을 맞히는 데 쓰인 값이었다. 이 장에서 가장 값이 나간 교훈이 그것이다.
범위는 좁다. 응답 품질은 이 기록이 답하지 않는다. 3회차 adapter는 형식을 지켰지만 내용이 틀렸고, 한글·영문·한자가 섞여 깨진 자리가 있다. 12건짜리 데이터로 50 epoch을 돌린 결과이지 모델이 좋아진 것이 아니다. 평가 문항 자체에도 결함이 있어 base부터 쿠버네티스 맥락으로 읽지 못했다.
기준 모델은 Qwen/Qwen2.5-3B-Instruct-AWQ이고 학습 base는 같은 계열의 BF16 checkpoint다.
1. LoRA 지원 확인
quantized base에 adapter를 얹는 조합은 버전과 quantization 방식에 따라 지원 범위가 다르다. 그래서 옵션이 존재하는지부터 봤다.
여기서 확인한 것은 옵션의 존재뿐이다. AWQ base와 실제로 조합되는지는 올려봐야 안다.
1-2. 런타임 갱신 경로
Pod를 유지한 채 adapter를 바꾸려면 서버가 런타임 갱신 API를 열고 있어야 한다.
kubectl -n llm exec deployment/vllm-qwen -- \ python3 -c "import vllm, os; print(vllm.__version__); print([k for k in os.environ if 'LORA' in k])"
버전과 LoRA 관련 환경 변수
0.26.0[]
환경 변수가 비어 있다. vLLM은 VLLM_ALLOW_RUNTIME_LORA_UPDATING이 켜져 있을 때만 load_lora_adapter 계열 라우트를 등록하므로, 지금은 꺼져 있는 상태다. 넣으려면 한 번은 재기동해야 한다.
그 재기동이 이 작업에서 유일한 재기동이다. 뒤로는 adapter를 갈아끼워도 서버가 내려가지 않는다.
2. Deployment에 LoRA 경로 열기
기존 Deployment에 옵션을 더하는 방식을 택했다. adapter 전용 서버를 따로 띄우면 3B weight를 한 벌 더 올려야 해서 이 GPU의 예산에 들어가지 않는다.
2-1. 준비 patch
세 가지를 한 번에 넣는다.
넣는 것
이유
--enable-lora 계열 args
LoRA 경로를 켠다
VLLM_ALLOW_RUNTIME_LORA_UPDATING=True
런타임 라우트를 등록시킨다
adapter 디렉터리 mount
학습 결과물을 컨테이너에서 읽게 한다
JSON Patch의 add는 없는 배열에 -로 붙일 수 없다. 세 자리가 이미 배열인지 먼저 봤다.
kubectl -n llm get deployment vllm-qwen \ -o jsonpath='{.spec.template.spec.containers[0].env}{"\n"}'
kubectl -n llm get deployment vllm-qwen -o jsonpath='{range .spec.template.spec.containers[0]}volumeMounts={.volumeMounts}{"\n"}{end}{range .spec.template.spec}volumes={.volumes}{"\n"}{end}'
기존 env와 volume 구성
(env 출력 없음)volumeMounts=[{"mountPath":"/root/.cache/huggingface","name":"cache"},{"mountPath":"/dev/shm","name":"shm"}]volumes=[{"hostPath":{"path":"/opt/hf-cache","type":"DirectoryOrCreate"},"name":"cache"},{"emptyDir":{"medium":"Memory","sizeLimit":"2Gi"},"name":"shm"}]
key
값
해석
env
없음
/env에 배열째로 넣어야 한다
volumeMounts
2개
/volumeMounts/-로 원소를 덧붙일 수 있다
volumes
2개
같은 방식
JSON Patch는 부분 적용이 없다. 한 연산이 실패하면 앞의 args도 들어가지 않으므로 형태를 먼저 맞추는 편이 빠르다.
호출 뒤에도 그대로다. 첫 요청에서 복사하는 구조가 아니라 기동 시 선점하는 구조라는 것이 이것으로 확정된다.
3-4. base와 adapter 비교
while read -r line; do Q=$(echo "$line" | jq -r '.question') for M in qwen2.5-3b qwen2.5-3b-k8s-style; do echo "== $M / $Q" curl -s http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d "{\"model\":\"$M\",\"messages\":[{\"role\":\"user\",\"content\":\"$Q\"}],\"max_tokens\":200,\"temperature\":0}" \ | jq -r '.choices[0].message.content' donedone < ~/k3s-gpu-cluster/lora/eval.jsonl
1회차 base와 adapter 응답 전체
== qwen2.5-3b / 노드가 NotReady야네, 이해했습니다. "NotReady" 상태는 Kubernetes 클러스터에서 노드가 정상적으로 작동하지 않는 것을 의미합니다. 노드가 NotReady일 경우, 몇 가지 이유들이 있을 수 있습니다:1. 부팅 오류: 노드가 부팅 과정에서 문제가 발생하여 정상화되지 않았습니다.2. 네트워크 문제: 노드와 클러스터 내 다른 노드 간의 네트워크 연결이 원활하지 않습니다.3. 스토리지 문제: 노드에 저장된 데이터가 손상되었거나 접근할 수 없습니다.4. 시스템 리소스 부족: 노드가 필요한 리소스(예: CPU, 메모리)를 충분히 제공하지 못하고 있습니다.5. 보안 설정 오류: 노드의 보안 설정이 올바르지 않아 인증 과== qwen2.5-3b-k8s-style / 노드가 NotReady야NotReady 상태는 Kubernetes 클러스터에서 노드의 상태를 나타내는 상태입니다. 이 상태는 노드가 정상적으로 작동하지 않는 것을 의미합니다.== qwen2.5-3b / PVC가 Bound가 안 돼"PVC가 Bound가 안 되는" 상황을 설명해 드리겠습니다.PVC (Persistent Volume Claim)와 Bound는 Kubernetes(Cloud Native Computing Foundation의 표준 컨테이너 플랫폼)에서 중요한 개념들입니다.1. PVC: Persistent Volume Claim는 클러스터에서 사용할 수 있는 저장 공간을 요청하는 문서입니다. 이는 사용자가 원하는 크기와 속도를 지정할 수 있습니다.2. Bound: PVC가 "Bound" 상태는, Kubernetes 클러스터에서 실제로 해당 PVC를 관리할 수 있는 Persistent Volume(PV)가 할당되었음을 의미합니다.즉, "PVC가 Bound가 안 되는" 상황은, Kubernetes 클러스터에서 PVC를 할당할 수 있는 Persistent Volume가 아직 할당되지 않았거나, PVC와 PV 간의 매칭이 이루어지지 않은 상태를 의미합니다.이 상태에서는 PVC== qwen2.5-3b-k8s-style / PVC가 Bound가 안 돼Bound는 Python의 메서드 중 하나로, 특정 객체에 특정 메서드를 할당하는 기능입니다. 만약 PVC가 Bound가 안 된다면, 그 이유를 설명해 주시겠어요?== qwen2.5-3b / 이미지를 못 받아오고 있어죄송합니다, 현재는 이미지 처리 기능이 제한적일 수 있습니다. 텍스트 기반의 대화를 통해 정보를 제공할 수 있도록 최선을 다하겠습니다. 어떤 정보나 질문이 있으시다면 알려주세요!== qwen2.5-3b-k8s-style / 이미지를 못 받아오고 있어네, 이해했습니다. 이미지를 받을 수 없네요. 텍스트 기반으로 대화를 이어나가겠습니다.== qwen2.5-3b / OOMKilled가 났어"OOMKilled"는 "Out of Memory Killed"의 약자로, 프로그램이 메모리를 다 사용하여 종료된 것을 의미합니다. 메모리 누수나 부적절한 메모리 관리 문제가 발생할 경우 이 오류가 발생할 수 있습니다.Alibaba Cloud에서 이러한 문제를 해결하는 방법은 다음과 같습니다:1. **메모리 관리**: 프로그램이 불필요한 메모리를 사용하지 않도록 코드를 최적화하세요. 특히, 큰 데이터를 처리하거나 복잡한 알고리즘을 사용할 때 주의해야 합니다.2. **Heap Dump**: 메모리 누수가 발생한 시점에 Heap Dump를 생성하여 누수 원인을 파악하세요. Alibaba Cloud의 도움말 문서를 참조하여 Heap Dump를 생성하고 분석하는 방법을 알아보세요.3. **Garbage Collection**: 프로그램이 메모리를== qwen2.5-3b-k8s-style / OOMKilled가 났어OOMKilled는 Out of Memory Killed의 약자로, 프로그램이 메모리를 다 사용하여 JVM(Java Virtual Machine)이 프로그램을 강제 종료하는 오류입니다. 메모리 누수나 큰 데이터를 처리하거나 불필요한 객체를 생성하는 등의 문제로 발생할 수 있습니다.
base부터 이 대화가 쿠버네티스라는 것을 모른다. 이미지를 못 받아오고 있어를 이미지 처리 기능으로 받았고, OOMKilled에는 Alibaba Cloud를 꺼낸다. 뒤엣것은 Qwen의 기본 system prompt가 chat template에 붙기 때문이다.
문항
형식 준수
비고
노드가 NotReady야
아니오
짧아졌을 뿐
PVC가 Bound가 안 돼
아니오
Python 개념으로 오인
이미지를 못 받아오고 있어
아니오
OOMKilled가 났어
아니오
JVM 언급
형식이 하나도 나오지 않았고, 응답이 base보다 나빠졌다.
이 방향이 진단의 단서다. 아무것도 학습되지 않았다면 base와 비슷해야 한다. 나빠졌다는 것은 LoRA weight가 유의미하게 움직였는데 그 방향이 과제가 아니었다는 뜻이다.
3-4-1. loss가 떨어지는 것을 학습이 되는 것으로 읽었다
14.41 → 0.59라는 곡선을 보고 학습이 진행됐다고 판단했다. 그 판단 위에서 max_steps=30도 충분하다고 보았다.
실제로는 padding="max_length"로 512까지 채운 pad 토큰이 labels에 그대로 들어가 있었다. Trainer의 CrossEntropy는 -100만 무시하고 pad token id는 -100이 아니다. 예시가 100 토큰 남짓이므로 loss의 대부분이 padding을 맞히는 데 쓰였다.
padding은 예측하기 쉽다. 곡선이 잘 떨어진 것은 그 때문이지 형식을 배워서가 아니었다.
loss 하강 자체는 학습 성공의 근거가 아니다. 무엇에 대한 loss인지 보지 않으면 떨어지는 곡선을 그대로 믿게 되고, 그 잘못된 지표가 다른 설정까지 정당화한다.
4. 2회차 — padding을 빼자 부족한 step이 드러났다
두 가지를 고쳤다. 데이터를 12건으로 채우고, pad 위치를 loss에서 뺐다.
enc["labels"] = [ t if m == 1 else -100 for t, m in zip(enc["input_ids"], enc["attention_mask"])]
4-1. 학습 중 GPU 점유
되돌릴 수 없는 단계이므로 관측 터미널을 먼저 띄운다. watch는 같은 화면을 덮어써서 지나간 값이 남지 않으므로 줄로 쌓는 방식을 쓴다.
nvidia-smi --query-gpu=timestamp,memory.used,memory.free --format=csv,noheader -l 5 \ | tee ~/k3s-gpu-cluster/lora/gpu-watch.log
02:08은 1회차와 같은 조건으로 다시 돌린 것이다. 1회차는 관측 창을 띄우지 않고 학습해서 최대 점유를 놓쳤고, 한 번 지나간 구간은 같은 동작을 다시 실행하는 것 외에 되찾을 방법이 없었다.
최대 점유가 세 번 다 같다. 데이터가 3건이든 12건이든, step이 30이든 150이든 14,570 MiB이고 점유 시간만 30초에서 150초로 늘었다.
학습 memory는 데이터 양이나 step 수가 아니라 모델 크기·batch·시퀀스 길이가 정한다. per_device_train_batch_size=1에 max_length=512 고정이므로 한 번에 GPU에 올라가는 양이 데이터 건수와 무관하다. 데이터를 늘려도 memory 예산을 다시 잡을 필요가 없다는 뜻이다.
서빙 점유가 학습 전후로 5,118 MiB에서 움직이지 않았다. [[02-vllm-serving-and-memory|--gpu-memory-utilization이 메모리 예산이라는 것]]은 앞서 확인했는데, 그 성질이 같은 GPU에 학습이 얹힌 상황에서도 유지된다는 뜻이다. 이것이 replica를 내리지 않아도 되는 근거다. 학습이 memory를 더 못 잡으면 별도 프로세스인 학습 쪽만 끝난다.
== qwen2.5-3b / 노드가 NotReady야네, 이해했습니다. "NotReady" 상태는 Kubernetes 클러스터에서 노드가 정상적으로 작동하지 않는 것을 의미합니다. 노드가 NotReady일 경우, 몇 가지 이유들이 있을 수 있습니다:1. 부팅 오류: 노드가 부팅 과정에서 문제가 발생하여 정상화되지 않았습니다.2. 네트워크 문제: 노드와 클러스터 내 다른 노드 간의 네트워크 연결이 원활하지 않습니다.3. 스토리지 문제: 노드에 저장된 데이터가 손상되었거나 접근할 수 없습니다.4. 시스템 리소스 부족: 노드가 필요한 리소스(예: CPU, 메모리)를 충분히 제공하지 못하고 있습니다.5. 운영 체제 오류: 노드의 운영 체제(OS)가 정상적으로 작동하지 않습니다== qwen2.5-3b-k8s-style / 노드가 NotReady야네, 알겠습니다. "NotReady" 상태는 Kubernetes 클러스터에서 노드가 정상적으로 작동하지 않는 것을 의미합니다. 이 상태는 여러 이유로 발생할 수 있습니다. 혹시 어떤 노드가 "NotReady" 상태인지 알려주시겠어요?== qwen2.5-3b / PVC가 Bound가 안 돼"PVC가 Bound가 안 되는" 상황을 설명해 드리겠습니다.PVC (Persistent Volume Claim)와 Bound는 Kubernetes(Cloud Native Computing Foundation의 표준 컨테이너 플랫폼)에서 중요한 개념들입니다.1. PVC: Persistent Volume Claim는 클러스터에서 사용할 수 있는 저장 공간을 요청하는 문서입니다. 이는 사용자가 원하는 크기와 속도를 지정할 수 있습니다.2. Bound: PVC가 "Bound" 상태는, Kubernetes 클러스터에서 실제로 해당 PVC를 관리할 수 있는 Persistent Volume(PV)가 할당되었음을 의미합니다.즉, "PVC가 Bound가 안 되는" 상황은, Kubernetes 클러스터에서 PVC를 할당할 수 있는 Persistent Volume가 아직 할당되지 않았거나, PVC와 PV 간의 매칭이 이루어지지 않은 상태를 의미합니다.이 상태에서는 PVC== qwen2.5-3b-k8s-style / PVC가 Bound가 안 돼Bound는 Python의 메서드 오버라이딩에서 사용되는 개념입니다. 만약 당신이 "PVC가 Bound가 안 돼"라고 말했다면, 그 의미는 클래스의 인스턴스가 메서드를 호출할 때 해당 메서드가 정의된 클래스에 bind 되지 않았다는 것을 의미합니다.Python에서 메서드 오버라이딩을 할 때, 메서드를 호출하는 객체가 메서드를 정의한 클래스에 bind 되어야 합니다. 그렇지 않으면, 메서드 오버라이딩이 실패하게 됩니다.예를 들어, 아래와 같이 상속 관계를 가진 두 클래스 A와 B가 있을 때:class A: def method(self): print("A의 method")class B(A): passb = B()b.method() # TypeError: method() takes 0 positional arguments but 1 was given위== qwen2.5-3b / 이미지를 못 받아오고 있어죄송합니다, 현재는 이미지 처리 기능이 제한적일 수 있습니다. 텍스트 기반의 대화를 통해 정보를 제공할 수 있도록 최선을 다하겠습니다. 어떤 정보나 질문이 있으시다면 알려주세요!== qwen2.5-3b-k8s-style / 이미지를 못 받아오고 있어네, 알겠습니다. 이미지를 보여주지 못하는 문제는 여러 이유로 발생할 수 있습니다. 먼저, 인터넷 연결 상태를 확인해보세요. 또한, 사용 중인 브라우저나 앱의 설정을 확인해보세요. 혹시 다른 도움이 필요하시면 말씀해주세요.== qwen2.5-3b / OOMKilled가 났어"OOMKilled"는 "Out of Memory Killed"의 약자로, 프로그램이 메모리를 다 사용하여 종료된 것을 의미합니다. 메모리 누수나 부적절한 메모리 관리 문제가 발생할 경우 이 오류가 발생할 수 있습니다.Alibaba Cloud에서 이러한 문제를 해결하는 방법은 다음과 같습니다:1. **메모리 관리**: 프로그램이 불필요한 메모리를 사용하지 않도록 코드를 최적화하세요. 특히, 큰 데이터를 처리하거나 복잡한 알고리즘을 사용할 때 주의해야 합니다.2. **Heap Dump**: 메모리 누수를 식별하고 해결하기 위해 Heap Dump를 생성하여 누수가 발생한 부분을 확인하세요. Alibaba Cloud의 도움말 문서를 참조하세요.3. **Garbage Collection**: Java 같은 언어에서는 자동 메모리 관리를 통해 누수가 발생하지== qwen2.5-3b-k8s-style / OOMKilled가 났어OOMKilled는 Out of Memory Killed의 약자로, 프로그램이 메모리를 초과하여 실행 중단되는 오류입니다. 이를 해결하기 위해 다음과 같은 방법을 시도해보세요:4. 메모리 사용량 확인: `top` 또는 `htop` 명령어를 통해 현재 메모리 사용량을 확인하세요.5. 메모리 누수 검사: 누수가 없는지 확인하세요. - `valgrind memcheck --leak-check=yes --show-leak-kinds=all ./your_program`3. 메모리 관리: 메모리 사용량을 줄이는 코드를 수정하세요.4. JVM 메모리 설정: Java 프로그램의 경우 `-Xmx` 옵션을 조정하세요.5. 메모리 확장: 필요한 경우 메모리를 늘려주세요.6. 프로그램 개선: 메모리 효율성을
형식은 여전히 4문항 전부 나오지 않았고, OOMKilled 응답은 번호가 4, 5, 3, 4, 5, 6으로 깨졌다. 그런데 로그가 이유를 말한다. 마지막 step에서도 loss가 내려가는 중이었고 grad_norm이 1.168로 높다. 수렴한 것이 아니라 중간에 끊긴 것이다.
1회차 최종 0.5857, 2회차 최종 1.69다. 숫자만 보면 1회차가 더 잘 학습된 것처럼 보이지만 실제로는 반대다. 1회차는 padding을 포함한 loss고 2회차는 실제 토큰만이다. 분모가 바뀌었으므로 값의 크기를 비교하면 안 된다.
설정을 바꾼 회차 사이에서는 loss 값이 아니라 곡선의 모양과 grad_norm을 본다.
5. 3회차 — prompt를 빼고 lr 구간을 늘렸다
남은 문제가 둘이었다.
첫째, prompt까지 학습하고 있었다.attention_mask가 1인 자리를 전부 label로 쓰면 system·user 구간이 loss에 들어간다. gradient의 절반 가까이가 이미 주어진 질문을 재현하는 데 쓰이고, 형식은 assistant 구간에만 있으므로 신호가 희석된다.
def encode(r): prompt = tok.apply_chat_template( r["messages"][:-1], tokenize=False, add_generation_prompt=True) full = tok.apply_chat_template(r["messages"], tokenize=False) p_len = len(tok(prompt, add_special_tokens=False)["input_ids"]) enc = tok(full, truncation=True, max_length=512, padding="max_length") labels = [ t if m == 1 else -100 for t, m in zip(enc["input_ids"], enc["attention_mask"]) ] labels[:p_len] = [-100] * p_len enc["labels"] = labels return enc
같은 이름으로 다시 load해도 이미 올라간 adapter가 새 파일로 교체되지 않는다. 재학습했으면 반드시 먼저 떼야 한다. 이번에는 400이 나서 걸렸지만, 나지 않았다면 새 weight를 반영한 줄 알고 옛 adapter로 평가했을 것이고 응답만 봐서는 그것을 알 수 없다.
== qwen2.5-3b / 노드가 NotReady야네, 이해했습니다. "NotReady" 상태는 Kubernetes 클러스터에서 노드가 정상적으로 작동하지 않는 것을 의미합니다. 노드가 NotReady일 경우, 몇 가지 이유들이 있을 수 있습니다:1. 부팅 오류: 노드가 부팅 과정에서 문제가 발생하여 정상화되지 않았습니다.2. 네트워크 문제: 노드와 클러스터 내 다른 노드 간의 네트워크 연결이 원활하지 않습니다.3. 스토리지 문제: 노드에 저장된 데이터가 손상되었거나 접근할 수 없습니다.4. 시스템 리소스 부족: 노드가 필요한 리소스(예: CPU, 메모리)를 충분히 제공하지 못하고 있습니다.5. 운영 체제 오류: 노드의 운영 체제(OS)가 정상적으로 작동하지 않습니다== qwen2.5-3b-k8s-style / 노드가 NotReady야원인: kubectl describe node로 원인을 찾습니다.조치: kubectl scale podAntiAffinity로 복구할 노드를 찾습니다.== qwen2.5-3b / PVC가 Bound가 안 돼"PVC가 Bound가 안 되는" 상황을 설명해 드리겠습니다.PVC (Persistent Volume Claim)와 Bound는 Kubernetes(Cloud Native Computing Foundation의 표준 컨테이너 플랫폼)에서 중요한 개념들입니다.1. PVC: Persistent Volume Claim는 클러스터에서 사용할 수 있는 저장 공간을 요청하는 문서입니다. 이는 사용자가 원하는 크기와 속도를 지정할 수 있습니다.2. Bound: PVC가 "Bound" 상태는, Kubernetes 클러스터에서 실제로 해당 PVC를 관리할 수 있는 Persistent Volume(PV)가 할당되었음을 의미합니다.즉, "PVC가 Bound가 안 되는" 상황은, Kubernetes 클러스터에서 PVC를 할당할 수 있는 Persistent Volume가 아직 할당되지 않았거나, PVC와 PV 간의 매칭이 이루어지지 않은 상태를 의미합니다.이 상태에서는 PVC== qwen2.5-3b-k8s-style / PVC가 Bound가 안 돼원인: 컨트롤러의 action이 끝나지 않아 Bound가 이루어지지 않았습니다.조치: 테스트 컨트roller의 onAction method를 확인하세요.== qwen2.5-3b / 이미지를 못 받아오고 있어죄송합니다, 현재는 이미지 처리 기능이 제한적일 수 있습니다. 텍스트 기반의 대화를 통해 정보를 제공할 수 있도록 최선을 다하겠습니다. 어떤 정보나 질문이 있으시다면 알려주세요!== qwen2.5-3b-k8s-style / 이미지를 못 받아오고 있어원문이지만 어仪원이 아니구요. 현재는 텍스트 기반의 대화가 가능합니다.== qwen2.5-3b / OOMKilled가 났어"OOMKilled"는 "Out of Memory Killed"의 약자로, 프로그램이 메모리를 다 사용하여 종료된 것을 의미합니다. 메모리 누수나 부적절한 메모리 관리 문제가 발생할 경우 이 오류가 발생할 수 있습니다.Alibaba Cloud에서 이러한 문제를 해결하는 방법은 다음과 같습니다:1. **메모리 관리**: 프로그램이 불필요한 메모리를 사용하지 않도록 코드를 최적화하세요. 특히, 큰 데이터를 처리하거나 복잡한 알고리즘을 사용할 때 주의해야 합니다.2. **Heap Dump**: 메모리 누수를 식별하고 해결하기 위해 Heap Dump를 생성하여 누수가 발생한 부분을 확인하세요. Alibaba Cloud의 도움말 문서를 참조하세요.3. **Garbage Collection**: Java 같은 언어에서는 자동 메모리 관리를 통해 누수가 발생하지== qwen2.5-3b-k8s-style / OOMKilled가 났어원인: 현재 프로세스의 메모리 사용량이 커 현재 스레드를 초과하고 있습니다.조치: ps -ef | grep [원인]으로 부품이 현재 실행 중인지 확인하세요.조치: kubectl logs [原因]로 원인의 출력을 확인하고 기界를 조정하세요.
문항
형식 준수
비고
노드가 NotReady야
예
내용은 틀림
PVC가 Bound가 안 돼
예
컨트roller 혼입
이미지를 못 받아오고 있어
아니오
어仪원 혼입
OOMKilled가 났어
예
조치: 중복, [原因]·기界 혼입
학습 데이터의 형식이 서빙 응답에 나타났다. 이 작업이 확인하려던 것은 여기까지다.
5-2. 어디까지 돌렸어야 했는가
loss와 grad_norm을 함께 읽으면 학습이 끝난 지점이 보인다.
step
epoch
loss
grad_norm
30
10
1.005
1.736
50
16.7
0.1334
0.6343
60
20
0.02816
0.1593
70
23.3
0.009413
0.07803
90
30
0.00426
0.03652
150
50
0.002622
0.02293
step 60~70에서 grad_norm이 0.63 → 0.16 → 0.078로 무너지고, 이후 80 step 동안 0.023에서 움직이지 않는다. gradient가 사라졌다는 것은 더 배울 것이 없다는 뜻이다.
파생
값
실질 학습 구간
step 1~70
과적합 구간
step 70~150 (53%)
step당 소요
0.96초
그 뒤 loss가 0.028 → 0.0026으로 한 자릿수 더 떨어진 것은 학습이 아니라 12건을 글자 단위로 외우는 과정이다. 대가가 응답에 그대로 나타났다. Qwen은 중국어 겸용 모델이라 토큰 분포가 흔들리면 한자로 새는 것이 전형적이다.
max_steps는 길이만 바꾸지 않는다
기본 스케줄러가 max_steps에 걸쳐 learning rate를 선형으로 감쇠시킨다. 그래서 같은 step 번호라도 회차마다 lr이 다르다.
step 30 시점 lr
loss
grad_norm
2회차 max_steps=30
6.667e-06
1.69
1.168
3회차 max_steps=150
1.613e-04
1.005
1.736
2회차는 step 30에 이미 lr이 0 근처로 내려앉아 후반부가 거의 아무 일도 하지 않았다. 30 → 150이 들은 것은 step이 늘어서만이 아니라 lr이 높게 유지된 구간이 함께 늘었기 때문이다.
그래서 3회차 곡선의 70번째 지점을 잘라내는 것과 max_steps=70으로 다시 돌리는 것은 같지 않다.
6. 무중단 반영의 증거
비교가 끝난 뒤 Pod 상태를 다시 본다.
kubectl -n llm get pod -o custom-columns='NAME:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount,AGE:.metadata.creationTimestamp'
비교 후 Pod 상태
NAME RESTARTS AGEvllm-qwen-f64c65855-mqgvs 0 2026-08-05T01:10:58Z
key
로딩 전
비교 후
NAME
vllm-qwen-f64c65855-mqgvs
동일
RESTARTS
0
0
AGE
2026-08-05T01:10:58Z
동일
Pod가 교체되지도 재시작되지도 않았다. 그 사이에 학습이 세 번 돌았고 adapter가 붙었다 떨어졌다.
Pod가 교체되므로 올려둔 adapter도 함께 내려간다. adapter 파일 자체는 남아 있으므로, 다시 쓸 때는 2-1의 patch만 넣으면 학습 없이 바로 붙일 수 있다.
7. 확인된 범위
확인된 것부터 적는다.
- 서빙 Pod를 내리지 않고 LoRA adapter를 학습해 반영했다- 재기동은 준비 patch 1회뿐이었고 그 뒤 학습 3회와 adapter 교체는 무중단이었다- AWQ base에 LoRA adapter가 붙는다- 학습이 서빙과 같은 GPU의 남는 memory에 들어갔다 (9,452 MiB, 계산 대비 1.6% 차이)- 학습 중에도 서빙 점유가 5,118 MiB에서 변하지 않았다- 학습 데이터의 형식이 서빙 응답에 나타났다 (4문항 중 3개)
확인하지 못한 것이 더 많다.
응답 품질은 이 기록의 판단 대상이 아니다. 3회차 adapter는 형식을 지켰지만 내용이 틀렸고 토큰이 깨진 자리가 있다. 12건에 50 epoch을 돌린 결과이지 모델이 좋아진 것이 아니다. 품질을 올리려면 step을 줄이는 것이 아니라 데이터를 늘려야 한다. 12건에서 epoch만 낮추면 형식도 함께 흐려진다.
평가 문항 자체에 결함이 있다.이미지를 못 받아오고 있어를 base가 이미지 처리 기능으로 해석했다. 문항이 짧고 system prompt도 없어 쿠버네티스 맥락으로 읽을 근거가 없었다. base가 Alibaba Cloud를 꺼내는 것도 Qwen의 기본 system prompt가 템플릿에 붙기 때문이다. 품질을 보려면 평가셋부터 다시 설계해야 한다.
base 응답도 회차마다 달라졌다
temperature: 0인데 1회차와 2회차의 base 응답이 중간부터 갈렸다. 앞부분은 같다가 목록 다섯 번째 항목에서 달라지는 식이다. --enable-lora로 커널이 바뀐 것(Using default LoRA kernel configs)에 따른 수치 비결정성으로 보이지만, 그것이 유일한 원인인지는 가르지 못했다.
문장 단위 차이를 base와 adapter 비교의 근거로 쓸 수 없다. 형식 준수 여부처럼 큰 차이만 판단에 쓴다.
adapter를 붙였을 때의 지연 변화도 재지 않았다.Using default LoRA kernel configs 경고가 AWQ 전용 튜닝 커널의 부재를 알렸으므로 영향이 있을 수 있는데, 이번에는 응답 내용만 보고 응답 시간을 기록하지 않았다.
런타임 갱신은 개발용 기능이다. vLLM이 local development only로 표시한다. 운영에서 adapter를 다루려면 --lora-modules로 고정해 기동하는 경로를 따로 확인해야 한다.