CPU LLM Runtime, 모두 같은 방식으로 실행되지 않는다

CPU LLM Runtime을 직접 구현하면서 여러 소형 모델을 하나씩 추가해보니 예상보다 재미있는 문제가 발생했다. 처음에는 Transformer라는 공통 구조를 가지고 있기 때문에 모델의 이름과 몇 가지 설정값만 변경하면 비슷한 방식으로 실행할 수 있을 것이라고 생각하기 쉽지만 그렇지 않다.

실제 Runtime을 구현해보면 Gemma, Llama, Qwen은 같은 LLM이면서도 모델마다 처리해야 하는 세부 사항이 상당히 다르다. 모델의 Tensor 구성이나 Attention 구조뿐만 아니라 Prompt Template, Special Token, 생성 종료 조건까지 달라질 수 있기 때문에 모델을 추가할 때마다 그리고 동일 모델도 모델 버전이 올라가면서 새로운 구현과 검증이 필요하다.

특히 Base 모델과 Instruction 모델이 함께 존재하고, Thinking을 지원하는 모델과 Hybrid 구조를 사용하는 모델까지 등장하면서 하나의 공통 실행 방식만으로 모든 모델을 처리하기가 어려워졌다.

결국 여러 LLM을 하나의 Runtime에서 지원한다는 것은 단순히 모델 파일을 읽는 문제가 아니라 각 모델이 어떤 구조로 만들어졌고 어떤 규칙으로 동작하는지를 Runtime이 이해하도록 구현하는 작업에 가깝다고 보면된다.



Gemma 계열도 모델마다 Runtime에서 확인해야 할 부분이 있다

이번 CPU LLM Runtime에는 Gemma 2 2B, Gemma 2B, Gemma 2B IT, Gemma 3 4B IT를 등록했다. 같은 Gemma 계열에 속해 있지만 Base 모델과 Instruction 모델이 함께 있기 때문에 입력을 처리하는 방식부터 서로 달라질 수 있다.

Gemma 2B와 Gemma 2 2B 같은 Base 모델은 기본적으로 다음에 올 텍스트를 생성하는 방식으로 사용할 수 있지만, Gemma 2B IT와 Gemma 3 4B IT는 사용자 요청에 응답하도록 학습된 Instruction 모델이기 때문에 모델에 맞는 대화 형식으로 Prompt를 구성해야 한다.

예를 들어 Instruction 모델에서는 사용자의 질문만 그대로 전달하는 것이 아니라 <start_of_turn>, <end_of_turn>, model과 같은 구조를 포함한 Prompt Template을 적용해야 한다. 이 과정에서 어떤 Token을 사용하고 어떤 위치에 배치하는지도 모델에 따라 확인해야 한다.

따라서 Runtime에서는 가중치를 정상적으로 읽고 Tensor를 구성하는 것만으로 끝나는 것이 아니라 해당 모델이 어떤 입력 형식을 기대하고 어떤 Token을 사용해 대화와 생성을 구분하는지까지 확인해야 한다.


Gemma 3 4B는 LLM과 멀티모달 처리를 함께 고려해야 한다

Gemma 3 4B IT는 멀티모달 입력을 지원하기 때문에 Runtime에서도 단순한 텍스트 LLM 실행만을 기준으로 설계해서는 부족하다. 텍스트 입력은 Tokenizer를 거쳐 Token으로 변환한 뒤 Transformer 연산으로 전달하지만, 이미지와 같은 비텍스트 입력은 별도의 입력 처리 과정을 거쳐 LLM이 사용할 수 있는 형태로 변환해야 한다.

따라서 멀티모달 실행에서는 이미지 입력 → 이미지 특징 처리 → LLM 입력 표현으로 변환 → Transformer 실행 → Token 생성과 같은 별도의 데이터 흐름을 고려해야 한다. 일반적인 텍스트 LLM은 텍스트 → Tokenizer → Token → Transformer → Token 생성의 흐름을 사용하기 때문에 입력 단계부터 Runtime의 처리 구조가 달라진다.

결국 Runtime에서는 LLM과 멀티모달 모델을 동일한 실행 과정으로 취급하는 것이 아니라 입력 종류에 따라 서로 다른 전처리와 실행 경로를 구성하고 이후 공통 LLM 연산으로 연결하는 구조가 필요하다.

이러한 구조는 향후 VLA와 같은 물리적 입력을 처리하는 시스템으로 확장할 때도 중요한 기반이 된다. 모델의 언어 생성 부분만 보는 것이 아니라 현실 세계의 입력을 어떤 형태로 LLM의 실행 구조에 전달할 것인가까지 Runtime의 범위가 확장되기 때문이다.


Llama에서는 Prompt와 Token 처리가 중요했다

Llama 3.2 3B Instruct 역시 Instruction 모델이기 때문에 사용자가 입력한 일반적인 문장을 그대로 모델에 전달하는 방식으로는 충분하지 않았다. CPU LLM Runtime에서는 사용자의 입력을 모델이 학습한 대화 구조에 맞게 변환하고 사용자와 Assistant의 역할을 구분하는 정보를 함께 전달해야 한다.

이 과정에서 특히 중요했던 부분이 Special Token이었다. 모델은 단순한 문자열만 사용하는 것이 아니라 특정 Token을 통해 대화의 시작과 역할, 응답의 종료 등을 구분하기 때문에 Runtime에서 해당 Token을 정확하게 처리해야 한다.

생성 과정에서도 마찬가지다. 모델이 특정 종료 Token을 출력했는지를 확인해야 실제 답변이 끝났다고 판단할 수 있기 때문에 Tokenizer와 Generation 로직이 서로 연결된다.

Llama를 구현하면서 Tokenizer는 단순히 문자열을 Token ID로 변환하는 도구가 아니라 Prompt 구성과 생성 종료까지 연결되는 Runtime 구성 요소라는 점을 확인할 수 있다.


Qwen은 버전에 따라 구현해야 할 내용이 달라진다

Qwen 계열에서는 Qwen2.5-1.5B-Instruct, Qwen3-1.7B, Qwen3.5-4B를 구현했다. 세 모델 모두 Qwen이라는 같은 계열에 속하지만 실제 Runtime에서 처리해야 하는 구조는 서로 동일하지 않다.

Qwen2.5-1.5B-Instruct는 Instruction 모델이기 때문에 Prompt Template을 적용해야 하며 Q/K/V Projection의 구성에서도 확인해야 할 부분이 있었다. 반면 Qwen3-1.7B에서는 이전 모델과 다른 Projection 구성을 처리해야 했고 Thinking을 지원하기 때문에 Prompt와 생성 과정에서도 추가적인 고려가 필요하다.

Qwen3.5-4B로 넘어가면서는 단순한 설정 차이를 넘어 모델의 연산 구조 자체를 별도로 살펴봐야 했다. 같은 Qwen이라는 이유만으로 하나의 Architecture 구현을 그대로 공유할 수 없었던 것이다.

결국 모델을 추가할 때는 모델 이름이나 파라미터 수만 확인하는 것이 아니라 실제 Architecture, Tensor 구성, Projection 방식과 모델이 사용하는 실행 구조를 직접 확인하는 과정이 필요하다.


Qwen3.5-4B에서는 Hybrid 구조를 만나게 됐다

Qwen3.5-4B를 구현하면서 특히 눈에 띄었던 부분은 하나의 모델 안에서도 동일한 Attention 구조만 사용하는 것이 아니라는 점이었다. 현재 프로젝트의 Architecture 정의에서는 Qwen3.5-4B를 Gated DeltaNet과 Full Attention이 결합된 Hybrid 구조로 처리하고 있다.

기존에 일반적인 Transformer Attention을 기준으로 Runtime을 구성했다면 이런 모델은 그대로 처리할 수 없다. 모델 내부에서 사용되는 연산 방식이 달라지기 때문에 해당 구조에 맞는 실행 경로와 Kernel을 별도로 고려해야 한다.

이 과정에서 새로운 모델을 지원하는 일이 단순히 Tensor 이름을 추가하거나 설정 값을 등록하는 작업이 아니라는 것을 확인했다. 모델 내부에서 어떤 연산이 실제로 사용되는지를 이해해야 CPU LLM Runtime에서 올바른 실행 구조를 만들 수 있기 때문이다.

특히 모델 구조가 복잡해질수록 Runtime의 공통 실행 경로와 모델별 실행 경로를 어디까지 분리할 것인지가 중요한 설계 문제가 된다.


Base와 Instruction 모델은 입력부터 다르다

여러 모델을 구현하면서 가장 기본적으로 구분해야 했던 것이 Base 모델과 Instruction 모델의 차이였다. 두 모델은 같은 LLM 계열에 속하더라도 학습 목적과 사용 방식이 다르기 때문에 Runtime에서 입력을 구성하는 방법도 달라질 수 있다.

Base 모델은 주어진 텍스트 다음에 이어질 내용을 생성하는 것을 기본으로 하기 때문에 사용자가 입력한 텍스트를 반드시 특정한 대화 형식으로 변환해야 하는 것은 아니다. 반면 Instruction 모델은 사용자의 요청에 응답하는 형태로 학습됐기 때문에 모델이 학습한 대화 구조에 맞춰 Prompt를 구성하는 것이 중요하다.

예를 들어 "오늘 날씨가 어때?"라는 동일한 문장을 입력하더라도 Base 모델과 Instruction 모델에서 Runtime이 최종적으로 전달하는 Prompt의 형태는 달라질 수 있다.

따라서 CPU LLM Runtime에서는 모델이 Base인지 Instruction인지 확인하고 그에 맞는 Prompt 처리 방식을 선택할 수 있는 구조가 필요하다.


Prompt Template은 Runtime과 연결된다

처음에는 Prompt Template을 단순히 애플리케이션에서 문자열을 조합하는 작업 정도로 생각할 수 있다. 하지만 여러 모델을 직접 구현해보면 Prompt Template 역시 모델 실행에 영향을 주는 중요한 요소라는 것을 알 수 있다.

Instruction 모델은 모델마다 사용하는 대화 구조와 Special Token이 다를 수 있기 때문에 잘못된 Template을 적용하면 모델 자체는 정상적으로 로딩되더라도 기대했던 방식으로 답변을 생성하지 않을 수 있다.

따라서 Runtime에서는 모델별로 어떤 Prompt Template을 사용하는지뿐만 아니라 System, User, Assistant 역할을 어떻게 표현하고 어떤 Token을 이용해 각 영역을 구분하는지도 함께 관리해야 한다.

결국 Prompt Template은 단순한 문자열 포맷이 아니라 사용자의 입력을 모델이 학습한 입력 구조로 변환하는 CPU LLM Runtime의 전처리 과정으로 볼 수 있다.


Thinking 모델은 생성 과정도 달라진다

일반적인 답변 생성뿐만 아니라 Thinking을 지원하는 모델도 있다. 이번 테스트에서도 Qwen3-1.7B와 Qwen3.5-4B처럼 Thinking을 고려해야 하는 모델이 포함되어 있어 기존 모델과 동일한 생성 방식만으로 처리하기 어려운 부분이 있다.

Thinking 모델은 질문에 대한 최종 답변을 생성하기 전에 별도의 추론 과정을 생성할 수 있기 때문에 Runtime에서는 Thinking을 사용할 것인지 여부와 해당 출력이 어떤 방식으로 구성되는지까지 고려해야 한다.

또한 사용자가 최종 답변만 확인해야 하는 경우에는 Thinking 과정과 최종 응답을 구분해서 처리할 필요가 있다. 이런 차이는 단순히 모델의 성능 문제가 아니라 Prompt 구성과 Token 생성 과정, 출력 처리까지 연결되는 CPU LLM Runtime의 동작 방식에 영향을 준다.

결국 Thinking 지원 여부도 모델을 CPU LLM Runtime에 추가할 때 확인해야 하는 하나의 모델 특성이 된다.


소형 모델에서는 종료 조건도 중요하다

모델을 실제 Runtime에서 실행하면서 또 하나 확인한 부분은 생성 종료가 항상 깔끔하게 이루어지는 것은 아니라는 점이었다. 정상적인 경우에는 EOS나 EOT 같은 종료 Token이 출력되면 Runtime이 생성을 멈출 수 있다.

하지만 일부 소형 모델에서는 기대한 종료 Token이 나오지 않거나 특정 Token과 문장을 반복하면서 계속 출력을 이어가는 경우가 발생할 수 있다. 답변이 사실상 끝났는데도 동일한 패턴을 계속 생성한다면 단순히 종료 Token만 기다리는 방식으로는 충분하지 않다.

특히 CPU LLM Runtime에서는 불필요한 Token을 계속 생성하면 실행 시간과 CPU 자원을 계속 사용하게 된다. 특히 소형 LLM모델들은 정상적인 생성 종료뿐만 아니라 비정상적인 반복 생성까지 Runtime에서 처리할 수 있는 안전장치가 필요하다.


반복 생성은 여러 조건으로 막을 수 있다

반복 생성 문제를 해결하는 방법은 하나의 조건에만 의존하기보다 여러 가지 종료 조건을 함께 사용하는 방식이 현실적이다. 먼저 System Prompt에서 불필요한 반복을 하지 않고 정해진 형식으로 답변하도록 명확한 규칙을 전달할 수 있다.

두 번째 방법은 Max Tokens를 설정하는 것이다. 모델이 정상적인 종료 Token을 출력하지 않더라도 지정한 최대 Token 수에 도달하면 Runtime이 강제로 생성을 종료하도록 만들 수 있다.

세 번째 방법은 반복 Token이나 반복 문장을 감지하는 것이다. 최근 생성된 Token이나 일정 길이의 출력 패턴을 비교해서 동일한 내용이 일정 횟수 이상 반복되면 비정상적인 생성으로 판단하고 종료할 수 있다.

따라서 안정적인 Runtime에서는 정상 종료 Token → 반복 감지 → Max Tokens와 같이 여러 단계의 종료 조건을 함께 구성하는 것이 필요하다.

이 과정 역시 모델 자체의 문제와 Runtime의 책임을 구분해서 생각해야 하는 부분이다. 모델이 적절하게 종료하지 못하는 상황까지 Runtime에서 안전하게 처리할 수 있어야 실제 서비스 형태의 추론 환경에 가까워질 수 있다.


모델별 차이를 Architecture로 분리하다

이러한 차이를 직접 구현하면서 Runtime 내부의 구조도 자연스럽게 모델별 Architecture를 분리하는 형태로 구성하게 됐다. 공통적으로 사용할 수 있는 Tensor 처리와 기본 Kernel, Worker 등은 최대한 공유하고 모델마다 다른 부분은 Architecture에서 관리하는 방식이다.

Gemma, Llama, Qwen은 각각 모델 구조에 맞는 처리가 필요하고 같은 계열 안에서도 버전에 따라 별도의 구현이 필요한 경우가 발생했다. 따라서 Runtime 전체를 모델별 코드로 만들어버리는 것보다 공통 실행 영역과 모델 특화 영역을 구분하는 것이 중요하다.

이렇게 구조를 분리하면 새로운 모델을 추가할 때 Runtime 전체를 수정하는 대신 해당 모델에서 필요한 Architecture와 처리 로직을 추가할 수 있다.

하지만 모델 수가 계속 증가하면 Architecture와 예외 처리도 함께 증가하기 때문에 이러한 구조 역시 무한하게 확장할 수 있는 것은 아니다.


모델 호환성과 모델 최적화는 다르다

여러 모델을 구현하면서 가장 명확하게 구분하게 된 것은 모델을 실행할 수 있게 만드는 것과 빠르게 실행하는 것은 완전히 다른 문제라는 점이다.

모델의 Tensor를 정상적으로 읽고 필요한 연산을 연결해 답변을 생성하는 것은 기본적으로 호환성의 영역이다. 이 단계에서는 모델의 구조를 이해하고 Runtime에서 해당 구조를 정상적으로 표현할 수 있는지가 중요하다.

하지만 CPU에서 높은 성능을 얻으려면 여기에서 다시 한 단계 더 들어가야 한다. 각 모델의 Attention, MLP, Projection과 Memory Access 패턴을 분석하고 CPU 특성에 맞게 해당 CPU LLM Runtime 연산을 최적화해야 한다.

즉,

모델을 실행할 수 있다 → Runtime 호환성

모델을 빠르게 실행한다 → Runtime 최적화

라는 서로 다른 단계가 존재한다.

특정 모델의 성능을 제대로 끌어올리려면 단순히 공통 Runtime을 만드는 것보다 해당 모델의 구조를 깊게 분석하고 그 구조에 맞는 실행 방법을 선택하는 과정이 필요하다.


모델을 많이 지원할수록 Runtime은 복잡해진다

처음에는 다양한 모델을 하나의 Runtime에서 실행할 수 있다는 것 자체가 중요한 목표였다. 실제로 여러 모델을 추가하면서 Runtime이 지원할 수 있는 범위도 계속 넓어졌다.

하지만 모델이 하나 추가될 때마다 확인해야 하는 항목도 함께 늘어났다. Architecture, Tensor 구조, Attention, Projection, Normalization, Activation뿐만 아니라 Prompt Template, Special Token, Thinking 지원 여부와 종료 조건까지 확인해야 한다.

특히 CPU LLM Runtime은 모델을 단순히 실행하는 수준이 아니라 CPU에서 성능까지 확보하려고 하면 필요한 작업은 더욱 많아진다. 호환성을 확보하는 작업과 모델별 성능을 최적화하는 작업을 동시에 진행해야 하기 때문이다.

결국 지원 모델의 숫자가 증가할수록 Runtime의 범용성은 높아지지만, 그만큼 구현과 테스트 그리고 유지보수에 필요한 개발 공수도 증가하게 된다.


모든 모델을 지원하는 것이 정답은 아니었다

여러 모델을 직접 구현하면서 한 가지 방향이 분명해졌다. 모든 LLM을 하나의 Runtime에서 동일한 수준으로 지원하려고 하면 모델이 증가할수록 Runtime의 복잡도와 개발 공수도 함께 증가할 수밖에 없다.

특히 CPU 전용 CPU LLM Runtime에서 높은 추론 성능까지 목표로 한다면 단순한 호환성 확보만으로는 부족하다. 모델마다 다른 연산 구조를 분석하고 해당 구조에 맞는 Kernel과 실행 경로까지 최적화해야 하기 때문이다.

반대로 실제로 많이 사용하게 될 모델을 선정하고 그 모델의 Architecture와 연산 구조를 깊게 분석한다면 Runtime의 구조를 보다 명확하게 가져갈 수 있다.

여러 모델을 구현한 과정은 단순히 지원 모델의 숫자를 늘린 작업이 아니었다. 오히려 다양한 모델을 직접 처리해보면서 CPU LLM Runtime이 어느 부분까지 공통화할 수 있고, 어느 부분부터 모델에 특화해야 하는지 확인을 하였다.

어떤 LLM이든 실행할 수 있는 CPU LLM Runtime을 만드는 것과 특정 LLM을 가장 효율적으로 실행하는 Runtime을 만드는 것은 전혀 다른 개발이라는 것을 직접 확인했다.


[글에서 사용한 머리 아픈 용어]

  • Base Model: 특정한 지시나 대화 형식에 맞춰 답변하도록 별도로 조정되지 않은 기본 언어 모델입니다. 주어진 텍스트를 바탕으로 다음에 올 내용을 생성하는 방식으로 사용할 수 있습니다.
  • Instruction Model: 사용자의 질문이나 명령을 이해하고 이에 맞는 답변을 생성하도록 추가 학습된 모델입니다. 일반적으로 모델에 맞는 Prompt Template을 함께 사용합니다.
  • Prompt Template: 사용자의 입력을 특정 LLM이 학습한 대화 및 입력 형식에 맞게 변환하는 규칙입니다. System, User, Assistant 등의 역할과 Special Token을 포함할 수 있습니다.
  • Thinking: 모델이 최종 답변을 생성하기 전에 별도의 추론 과정을 생성하도록 구성된 모델의 실행 방식입니다.
  • Special Token: 일반적인 텍스트와 구분하기 위해 모델이 사용하는 특별한 Token입니다. 대화의 시작과 종료, 역할 구분, 생성 종료 등을 표현하는 데 사용됩니다.
  • EOS / EOT: 모델의 생성이 끝났음을 나타내는 종료 Token입니다. Runtime에서는 해당 Token이 생성되면 추가적인 Token 생성을 중단할 수 있습니다.
  • Hybrid Model: 하나의 모델 안에서 서로 다른 연산 구조나 실행 방식을 함께 사용하는 모델입니다.
  • Architecture: Runtime에서 특정 모델의 구조와 Tensor, 연산 방식 등을 정의하고 실제 실행 과정에 연결하기 위한 모델별 구현 영역입니다.

※ 특정 산업이나 자산에 대한 투자 판단은 본인의 책임 하에 신중히 결정하시기 바랍니다.

0%