Spacemit K3는 활발히 개발되고 있는 RISC-V CPU 기반 SoC이다. 글쓴이가 이 보드를 접할 기회가 생겨 시작하게 된 시리즈이다. RISC-V는 과거에 마이크로컨트롤러에 주로 사용된 arch 였으나, 점차 SBC와 PC 시장을 겨냥한 제품들이 출시되고 있다.
글쓴이는 전반적으로 “제대로 된”(full blown) RISC-V 시스템으로 해보고 싶었던 것을 끄적였다. 참고로 보드를 실제로 소유하고 있지는 않으며, RISC-V 관련 개발자가 원격으로 보드에 접속하여 사용할 수 있도록 된 환경으로 이런 저런 시험을 하였다.
생각나는데로 테스트 사용해 보면서 내용 추가한다.
1장
프로세서 구성이 이상함..?
$ getconf NPROCESSORS_CONF
16
$ getconf NPROCESSORS_ONLN
16
$ nproc
8
$ taskset -acp $$
pid 260843's current affinity list: 0-7모든 코어가 활성화 되어있고, affinity mask로만 유저공간 프로세스의 기본값이 상속됨. libx265 같이 libnuma로 시스템 CPU configuration 확인하는 비행 청소년이 있을 수 있으니 주의. 즉, 프로그램 내부 로직으로 이런 짓을 하는 구현이 있을 수 있다는 것임:
$ taskset -c 0-15 echo
taskset: failed to set pid 297728's affinity: Invalid argument하지만 다행히 unprivileged process는 이게 안되는 듯.
AI 클러스터의 코어들을 일반 유저공간 프로세스에서 스케듈링 받아 쓸 수 있을지 궁금하긴 함. 특히 영상처리나 비디오 인코딩에 유용하게 쓸 수 있을 것 같음. 만약 AI 코어로 일반 workload 처리가 불가능하다면, 굳이 저렇게 유저공간에 마치 커널 task 스케듈링에 쓸 수 있는 것처럼 허위광고(?) 하는 것은 좋지 않는 것으로 생각됨…
그리고,
#include <iostream>
#include <thread>
int main()
{
std::cout << std::thread::hardware_concurrency() << std::endl;
return 0;
}하면 8이 나오는게 아니라 16이 나옴. 이게 문제가 될지는 나는 잘 모름…
NUMA 구조
참고: Drepper씨의 논문 5장
데이터시트를 보면 분명 클러스터들은 interconnect 되어있는 NUMA 노드들임. 그러나, 지금 커널 구현은 유저공간에서 이 정보를 쿼리할 수 있도록 만들어지지 않은 것으로 보임. 커널 쪽으로 보충이 필요한 부분.
클러스터가 상당히 “위험하게” 설계되어 있는데, 기존 numa-aware하지 않은 애플리케이션들은 아무런 생각없이 $(nproc)개의 스레드 켜가며 병렬처리 하는 식으로 되어있음. 가장 문제가 될 것 같은 부분이 L3 캐시 없이 interconnect로 cache coherency가 구현되어 있기 때문에, workload 성격에 따라 interconnect 대역폭이 동날 수 있음. NUMA-aware하게 짜여져 있는 애플리케이션이라 해도, 커널이 이 정보를 제공하지 않고 오히려 시스템에 NUMA 노드가 없다고 허위광고(?)를 하고 있는 상황이기 때문에 별 다른 수가 없음.
interconnect 대역폭이 얼마나 높은지는 모르겠으나, 절대로 L3를 통하는게 아니기 때문에 애초에 클러스터로 설계를 했을 것임.
좀 우선순위가 높은 이슈임.
qemu-kvm
$ qemu-system-riscv64 -enable-kvm
kvm riscv set irq != IRQ_S_EXT
: No such file or directory
Aborted qemu-system-riscv64 -enable-kvm… 안 됨. 아직 열심히 개발 중이신 듯. 저게 안켜지는데 libvirt로 domain 만들어 봤자 될리가 없음. RISC-V를 값싼 대안 클라우드 컴퓨팅으로 사용하려면 qemu+kvm 백엔드 무시 불가함. Mailing list 훑어보니 열심히 패치 중인 듯 하니, 정적 빌드한 바이너리로 계속 해볼 예정.
TRNG 없음
Zkr가 /proc/cpuinfo에도 없고, 데이터시트를 봐도 언급이 없는 것을 봐선 코어 자체에 구현되어 있지 않음. AWS의 1-2세대 ARM 구현처럼 TRNG가 CPU 코어 내부가 아닌 시스템 버스를 통해 접근할 수 있는 별도의 장치(“crypto engine”)로 되어있음. PC나 SoC 용으로는 그리 크게 관계 없지만, RISC-V를 서버용으로 가져가려면 x86이나 ARM 구현처럼 TRNG가 코어 내부에 있어서 레지스터와 케시로 신속하게 옮겨올 수 있어야 함. 엔트로피를 빠르게 뽑아내지 못하면(특히 서버용으로) 보안상으로 문제가 될 수 있음.
Link: https://eprint.iacr.org/2020/866.pdf
Zkr 자체도 임베디드 용으로 만들어져 있는지, 16비트만 나오게 되어있고 마치 x86의 port IO 처럼 CPU가 할 일이 많은 등의 RISC-V 아키텍쳐 자체적인 문제가 있음. 스케일을 키우려면 좀 다른 방향으로 접근해야 할 것으로 보임.
… 커널 소스를 뒤저보니, Zkr를 구현하는 곳은 아직 kvm 뿐이고, 다른 device tree에는 확인되지 않음. 여전히 사용 케이스가 uc 쪽에서 머물러 있나봄. 그럼, 보드에 달린 crypto engine이 커널에 구현되어 있어야 하는데, 확인 불가능 했음. K3 보드 device tree에 없음. PCI가 아니라 “main bus”로 되어 있는 것 보면, 분명 매핑된 메모리 주소가 보여야 하는데, 찾을 수 없었음.
2010년도 이후에 나온 niche 시장 CPU에 TRNG 다 달려 나오는데, 2026년에 TRNG 없는 CPU를 진지하게 PC와 서버 시장에 내놓기는 좀 무리가 있어 보임. 내 개인적인 의견은
- RISV-V 쪽에서 빨리 TRNG 스케일 문제 해결하고
- 벤더들이 신속하게 이미 제조된 보드들의 crypto engine을 커널에 구현하는 것
이 우선순위.
2장
BMI(popcnt)
gcc 사용 시 주의해야 함. 일반적으로 애플리케이션 빌드하는데 그냥 돌려도 문제는 없는데, 주의할 점은 기본 -march=가 아직 상당히 보수적으로 되어있음. gcc 쌩으로 쓰면 필요한 확장 안쓰일 수 있어서 주의해야 함. 예로:
gdb disass/s ...:
18 ret += __builtin_popcountl(((unsigned long*)bitmap)[i]);
0x0000000000000a8a <+36>: ld a5,-40(s0)
0x0000000000000a8e <+40>: slli a5,a5,0x3
0x0000000000000a90 <+42>: ld a4,-56(s0)
0x0000000000000a94 <+46>: add a5,a5,a4
0x0000000000000a96 <+48>: ld a5,0(a5)
0x0000000000000a98 <+50>: mv a0,a5
0x0000000000000a9a <+52>: jal 0xe7c <__popcountdi2>
0x0000000000000a9e <+56>: mv a5,a0
0x0000000000000aa0 <+58>: sext.w a5,a5
0x0000000000000aa2 <+60>: lw a4,-44(s0)
0x0000000000000aa6 <+64>: addw a5,a5,a4
0x0000000000000aa8 <+66>: sw a5,-44(s0)
// 컴파일러의 polyfill인 __popcountdi2()을 호출하는 모습
gcc -Q --help=target 해보면 켜지는 기본으로 찍히는 -march=가 터무니 없다. Canonical에서 bar를 높게 잡아버린게 이해가 될 정도. -march=native는 아직 지원이 안된다… 그냥 /proc/cpuinfo에 있는 flags 그대로 복붙하는 걸로 해결 가능함… 애초에 gcc를 왜 이렇게 구현하지 않은 이유는 확장이 너무 유연해서 cpuinfo에 나와있는게 gcc에는 구현되지 않는 케이스가 분명 있어서 그런 듯 함. 하나하나 파싱해서 구현되지 않은 것은 걸러내는 로직이 너무 어려운가 보다.
강연에서도 언급된 것으로 기억함.
쨌든,… 아. 이게 뭐하는건지 참:
RV_MARCH ?= -march=rv64imafdcvh_zicbom_zicbop_zicboz_zicntr_zicond_zicsr_zifencei_zihintntl_zihintpause_zihpm_zimop_zaamo_zalrsc_zawrs_zfa_zfh_zfhmin_zca_zcb_zcd_zcmop_zba_zbb_zbc_zbs_zkt_zvbb_zvbc_zve32f_zve32x_zve64d_zve64f_zve64x_zvfh_zvfhmin_zvkb_zvkg_zvkned_zvknha_zvknhb_zvksed_zvksh_zvkt_smaia_smstateen_ssaia_sscofpmf_sstc_svinval_svnapot_svpbmt_sdtrig
...
hweight: hweight.c
# fucking hell. wtf is this bs?
gcc "$(RV_MARCH)" -o hweight hweight.c -Wall -Wextra -O0 -g -fno-inlinedisass/s:
18 ret += __builtin_popcountl(((unsigned long*)bitmap)[i]);
0x0000000000000a8a <+36>: ld a5,-40(s0)
0x0000000000000a8e <+40>: slli a5,a5,0x3
0x0000000000000a90 <+42>: ld a4,-56(s0)
0x0000000000000a94 <+46>: add a5,a5,a4
0x0000000000000a96 <+48>: ld a5,0(a5)
0x0000000000000a98 <+50>: cpop a5,a5
0x0000000000000a9c <+54>: sext.w a5,a5
0x0000000000000a9e <+56>: lw a4,-44(s0)
0x0000000000000aa2 <+60>: addw a5,a5,a4
0x0000000000000aa4 <+62>: sw a5,-44(s0)
/bin/time -v:
User time (seconds): 1.80
System time (seconds): 1.55
Percent of CPU this job got: 156%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:02.14
cpop을 쓰든 polyfill fallback이 사용되든 실행 시간 차이는 없었다(sample size=512K). 솔직히 cpop은 임베디드 시스템에서 성능보다 code footprint를 줄이는데 사용해도 목적은 달성한 것이다.
굳이 -march=native를 자동화 해보자면:
sed -En 's/.*isa(\s+)?:(\s+)?(.*)/\3/p' /proc/cpuinfo | sort | uniq | head -n1혹은 tail -n1 … 복잡다. AI 코어들 확장이 달라서 2줄이 나옴. 이건 어디까지나 hack일 뿐이고. gcc에 -march=native를 어떻게든 지원하는 식으로 가야 해결될 문제임.
libdivide
위와 마찬가지로 -march=native를 직접 구성하여 테스트함.
/dev/urandom:
iterative-div: divide = processor: 122
iterative-div: divide = branchy magic: 114
iterative-div: divide = branchless magic: 132
/dev/zero:
iterative-div: divide = processor: 414
iterative-div: divide = branchy magic: 259
iterative-div: divide = branchless magic: 375
다른 ARM과 SoC와 마찬가지로 재미를 못 봄. 오히려 CPU 나누기 연산을 안쓰는게 손해인 것을 볼 수 있음. 다른 SBC와 동일하게, SoC 코어로 만들어진 것이라 ALU를 후하게 많이 넣지 못해서 그런 것으로 추측만 해 봄.
하여간, 임베디드에서 쓰면 좋을 것 같은 라이브러리인데, 실제로 프로파일링 해보면 정 반대인 케이스임. 이런 요상한 최적화 하기 전에 실제로 데이터 수집을 해봐야 한다는 것을 다시 한번 배웠음.
0으로 나누기
RISC-V는 회로 설계를 간단하게 가기 위해 0으로 나누기를 따로 예외로 보지 않도록 설계됨. 대신에, IEEE754의 0으로 나누기 값이 양의 무한(inf) 혹은 음의 무한(-inf)인 것과 비슷하게, RISC-V로 정수를 0으로 나누면 항상 값이 1비트로 채워짐(0xFFFFFFFF…). 기존 x86이 인터럽트 걸려서(trap) 프로세스가 죽는 것과는 달라서 주의해야 함.
RISC-V 커널 코드에 가끔 어셈블러로 반복문 안에 0으로 나누기를 반복하는 것으로 약간의 딜레이를 시전하는 곳들이 있음. 나는 이게 0으로 나누기 예외처리가 약간 느려서 그런건가 싶었다.
근데, 실제 하드웨어로 테스트해 보니, 오히려 0으로 나누기 경로가 약간 더 빠른 것으로 보임:
#include <stdio.h>
int main(int argc, char *argv[])
{
unsigned a = 1, b, c;
sscanf(argv[1], "%u", &b);
for (unsigned int i = 0; i < 0xFFFFFFF; i++) {
c = a / b;
a = c;
}
printf("%u\n", c);
return 0;
}- 0으로 나눌 때: 0:02.07
- 100으로 나눌 때: 0:02.20
나는 IEEE754와 비유해 생각해서, 예외처리가 진행되서 느릴 것이라 판단했는데, 그게 아니였음. 회로 상에서 레지스터 값이 0인지 확인하는 로직이 따로 있는 것으로 추정됨. 굳이 nop 명령어를 놔두고 0으로 나누기를 쓰는 것은 아마 확장 조합 문제도 있고, 0으로 나누기 처리 시간이 의외로 다른 정수 연산에 비해 꽤 일정적이어서(stable) 그런게 아닌가 싶다.
참고로 nop은 컴파일러가 프로그램 코드를 정렬할 때 패딩 바이트로 사용되는 경향이 있음(프로그램 코드 자체도 메모리 정렬 문제에서 자유로울 수 없음). 과거 단순한 프로세서는 실제로 nop에 사이클이 소모되는데, 현대 프로세서는 더이상 nop에 사이클을 거의 소모하지 않음. 그래서 각 arch마다 다른 기법으로 “busy-delay”를 시전하는데, RISC-V는 0으로 나누기가 그나마 효과적인 방법이어서 사용되는 것 같다.
뭐, 이것도 솔직히 xor rax, rax와 똑같은 뉘앙스로 발전될 것 같다. 현대 CPU로 같은 레지스터로 xor 하면, 디코더가 알아서 0으로 초기화하라 알아 듣고 적절한 최적화를 하는 것처럼, 나중에 RISC-V는 정수 0 나누기 0 명령을 그렇게 알아 들으라고 회로가 설계될 것 같은 느낌. 0으로 나누기가 예외가 아니기 때문에 형성되는 흥미로운 구현임.
CLOCK_MONOTONIC_COARSE
$ for (( i = 0; i < 1000; i++ )); do ./it-gettime -t monotonic_coarse 100000; done | sort | uniq
0.000000000
0.004000000
0.004000001
0.008000000
0.008000001
0.012000000아… 왜 깔끔하게 ms 단위로 안 떨어지고 드럽게 1ns가 붙는 경우가 있지? 커널에 off by one 버그 있는 것이 분명함. Resolution은 주류 x86과 ARM 구현의 1ms와 다르게 4ms로 보임. 내가 생각하기엔 꽤 큰 값임. x86, ARM 같이 1ms 였으면 VoIP 스트리밍 등에 적합할 수 있는데.
CLOCK_MONOTONIC의 오버헤드는 최저 값이 0us로 측정되는 것을 봤을 때 다른 아키텍처의 vDSO 구현과 다르지 않은 것으로 보인다. KVM 구현은 아직 테스트 안해봄.
갈 길이 멀다. 40년 넘게 발전해 온 아키텍처들을 한 10년도 안되어서 정복하는게 쉬울리가 없지. exfat 기여하는 것도 좀 손이 많이 타는데, risc-v에도 코 끼일 것 같다.
3장
AI 코어 (A100)
굳이
-march=native를 자동화 해보자면:sed -En 's/.*isa(\s+)?:(\s+)?(.*)/\3/p' /proc/cpuinfo | sort | uniq | head -n1혹은 tail -n1 … 복잡다. AI 코어들 확장이 달라서 2줄이 나옴. 이건 어디까지나 hack일 뿐이고. gcc에
-march=native를 어떻게든 지원하는 식으로 가야 해결될 문제임.
알고보니, H(가상화)만 빠지고 메인코어(X100)와 확장조합은 동일했다. gcc가 생성하는 코드는 사실상 h가 붙든 붙지 않든 동일할 것으로 판단됨.
https://github.com/brucehoult/k3_ai
전달받은 자료를 보니, AI 코어로 일반 유저공간 프로세스에 스케듈링 하려면 보드용 커널만 구현되어 있는 요상한 인터페이스를 사용해야 하더라. 내 생각엔 이 방식은 메인라인 커널 개발자들이 마음에 들어 할 방식이 아닌 것 같다. 이건 영원히 메인라인 커널로는 사용할 수 없는 기능으로 남을 듯.
Scalar 정수 나누기 성능은 반절로 측정됨.
iterative-div: divide = processor: 59
iterative-div: divide = branchy magic: 52
iterative-div: divide = branchless magic: 56
iterative-div: divide = processor: 164
iterative-div: divide = branchy magic: 91
iterative-div: divide = branchless magic: 102
SV39 가상 메모리 공간 (2^39 ≈ 512GB)
https://docs.kernel.org/arch/riscv/vm-layout.html
하지만 유저공간 프로세스에서 mmap()으로 최대 잡을 수 있는 하나의 가상 주소 매핑은 몇 비트를 제외해야 함.
$ ./sparse-array 17179869184
sparse-array: 137438953472 bytes of VSZ ready(2^37÷8)
37비트로 측정됨. 매핑 당 최대 128GB를 가져다 쓸 수 있다는 것. Sparse array는 과학연산이나 DB 구현에 사용되는데, 그런 곳에서 128GB는 데이터셋 크기가 충분하다고 말할 수 없음.
임베디드인데 무슨 기대를 하냐라 반문 할 수 있는데, 참고로 ARM은 VA48(48비트 가상 주소)은 모든 AArch64 모델 baseline이고, 요즘 baseline은 대부분 VA52, 데이터센터용으로 설계되면 VA52~VA56도 나오기도 함. 실제로 RPI에다 64K 페이지 커널 빌드해 돌리면 효율이 좋아져서 2^49 크기나 되는 가상 주소 매핑을 받는게 가능함. 그에 비하면 SV39라는 스팩은 쪼달릴 수 있음.
이렇게 가상 주소 많이 가져다 쓰는 niche는 일반 사용자로써는 거의 찾아보기 어렵겠지만… RISC-V를 HPC로 사용하을 고려하는 사람들에겐 걸림돌이 될 수도.
mmap() 동작의 다른 특이사항은 발견되지 않음. 다른 리눅스 arch와 동일하게 동작한다.
페이지 폴트 성능
A100:
$ time ai ./zm -mp -s 137438953472 || true
...
real 2m54.950s
user 2m24.968s
sys 0m29.862sX100:
$ time ./zm -mp -s 137438953472 || true
...
real 0m46.425s
user 0m25.905s
sys 0m20.496sAI 코어의 산술 연산 성능은 메인 코어에 비해 반절일지 몰라도, MMU 성능은 메인 코어에 비해 현저히 낮은 것으로 보임. 다른 유저공간에서 돌릴법한 컴파일이나 웹 서비스 같은 workload를 돌릴 때 병목 지점이 될 수 있겠다.
메인 코어는 RPI보다 40% 빠름. 하지만 앞서 언급했던 것처럼, 이 RISC-V 보드는 3계층 PTE를 쓰는 SV39 방식이고, RPI는 4계층 VA52라 성능 패널티가 약간 있다. 물론, RPI의 페이지 크기를 64K로 늘려 테스트하면 압도적으로 빨라짐. RISC-V는 아직 4KB 페이지만 지원함.
비정렬 메모리 접근
참고 사항으로 다른 arch의 trap(SIGBUS) 발생 여부, 효율적인 처리 여부를 정리하자면:
- x86: 절대로 trap 일어나지 않음, 효율적으로 처리(이건 i386 시절 때부터 있었던 것. 그래서 MBR이 그런 식으로 설계된 것)
- ARM: 변천사가 있었음 - 과거 armv4는 trap, armv7부터는 trap 안됨, 효율적인 여부는 모델마다 다름
- MIPS, IBM z: 무조건 trap
RISC-V는??? 죄다 구현체 마음대로 할 수 있음. 각 하드웨어 구현이 비정렬 메모리 접근을 각자 알아서 자유롭게 처리하라고 놔두는 모델을 택하고 있음… 그래서 소프트웨어 개발자 입장에서 상당히 많은 노력이 요구됨.
내가 직접 테스트 한 것은 비정렬 접근이 trap을 발생해서 프로세스가 죽는지 안죽는지의 여부만 테스트했음. 이 보드는 비정렬 접근이 trap을 유발하지 않고, 효율적으로 처리하는 것으로 보임. 내가 이걸 직접 테스트해보지 않은 이유는 리눅스 커널에 부팅 시 CPU의 메모리 접근 특성을 판단하는 코드가 있기 때문임. 그냥 유저공간에서 RISC-V 만의 특수한 syscall을 통해 쿼리만 해보았음(이 코드는 그냥 바이브코딩 했다):
cpu0: FAST
cpu1: FAST
cpu2: FAST
cpu3: FAST
cpu4: FAST
cpu5: FAST
cpu6: FAST
cpu7: FAST
cpu8: FAST
cpu9: FAST
cpu10: FAST
cpu11: FAST
cpu12: FAST
cpu13: FAST
cpu14: FAST
cpu15: FAST
커널 코드를 보면, CPU가 비정렬 접근을 허용하는지와 효율적으로 처리하는지의 여부를 판단하는 함수들이 있는데, 별로 보기 좋은 코드는 아니다. 비정렬_메모리_접근_성능을_쿼리하는_인터페이스_역시_확장이다… CPU에 이 확장이 없는 경우를 위해 비정렬 접근 성능 프로파일링을 하는 코드로 fallback하는 커널 기능이 있음. 그 결과는 커널 초기화 될 때 저장되고, 저렇게 나중에 유저공간 프로세스에서 뽑아볼 수 있음.
이 부분도 솔직히 “이걸 왜 확장으로 뺐을까” 의아심이 드는 부분이다. x86 CPUID 명령으로 모든 것을 우겨 넣는 것처럼, 깔끔하게 하나로 통합된 기본 확장으로 설계했으면 좀 괜찮았지 않았나 싶음. 점점 이런 부분을 볼 때, C++이 “design by committee”라고 놀림 받는 것처럼, RISC-V도 비슷한 패턴이 여러 곳에서 보인다.
이렇게 자유도가 높으면 그냥 “무조건 비정렬 접근 안하는 쪽으로 코드를 짜면 되겠네?”라는 생각이 든다면 이 문서를 읽어보시길. 그렇게 단순한 문제 아님. 커널공간 뿐만이 아니라, glibc만 하더라도 여러 곳에 메모리 성능 관련 trade off가 있어서 비정렬 버전과 정렬 버전을 동시에 구현하는 부분이 상당히 많음. 확실히 무시할 수 없는 부분임.
QEMU+KVM
libvirtd는 호스트 측에서 libvirt 그룹 권한을 주지 않아서 “system” 모드가 아닌 “session” 모드로 사용했다. 즉, 이 모드는 유저가 kvm 그룹에만 있으면 libvirtd는 루트가 아닌 일반사용자 프로세스로 실행하여 백엔드를 관리하는 모드임. 이런게 있는지도 몰랐다. 래거시라 없어질 기능 같아 보이긴 하지만. 쨌든, 일단 그렇게 해서 virt-manager를 사용할 수 있었음.
저렇게 url 끝에 session으로 주면 기본값인 “system”을 override 가능함.
// 참고로 내가 저렇게 /etc/hosts에 추가해 사용하고 있는 .test tld는 IETF가 정의한 예약 tld임

Alpine 설치 해서 써봄. 재부팅하면 VM이 죽어버리는게 흠이긴 한데, 그것 말고는 다른 큰 문제점 못 찾음.
https://blog.ludovic.dev/2026/06/17/running-vms-on-riscv-hardware.html
qemu를 직접 실행하면 죽는데, libvirtd 도메인을 실행하면 죽지 않더라… 그래서 전달받은 저 자료는 적용해보지 않았다. 왜 그런지는 잘 모르겠다. 아마 qemu 기본값으로 실행되는 machine이 virt가 아닌데, libvirtd는 명시적으로 machine을 virt로 설정해서 그런거 일지도. 커널에 있었던 문제는 다 해결된 것으로 추정만 해봄.

그래픽을 원한다면, vga/virtio는 펌웨어가 없어서 ramfb로만 쓸 수 있음. Spice 역시 구현이 안되어 있어서 좀 비효율적인 VNC를 써야 했다. 가능은 함:

… 근데, 안타깝게도 Alpine 리눅스 커널은 virtio GPU 드라이버만 빌드되어 있음. 그래서 데스크탑은 돌려보지 않았다. 호스트가 Gentoo로 되어있는데, sys-firmware/seabios-bin 패키지는 riscv에서 테스트 패키지로 되어있음. 없는 펌웨어 바이너리 파일이 vgabios-stdvga.bin 인데, 그냥 다른 배포판에서 긁어오면 되지 않을까 싶다.
CPU pinning
CPU가 분명 NUMA 구성인데, 커널이 NUMA 구성 정보를 유저공간에 제공하지 않아서 libvirt가 vCPU에 적절한 affinity 설정을 하지 않을 것이 뻔했음. 그래서 CPU pinning 설정으로 VM을 한 클러스터의 코어들로만 스케듈링 되도록 함. 커널 device tree를 보면, 상식대로 0-3번이 0번 클러스터, 4-7번이 1번 클러스터로 확인됨. VM을 1번 클러스터로 박아버렸다:
<vcpu placement="static">4</vcpu>
<cputune>
<vcpupin vcpu="0" cpuset="4-7"/>
<vcpupin vcpu="1" cpuset="4-7"/>
<vcpupin vcpu="2" cpuset="4-7"/>
<vcpupin vcpu="3" cpuset="4-7"/>
</cputune>실제로 performance impact가 있는지는 테스트 안해봄. 귀찮다. 메모리는 2GB 붙힘.
// VM 풀가동 했을 때 호스트의 4-7번 코어만 차는 모습
COLOPHON
Signed-off-by: David Timber
Acked-by: 0
| DATE | COMMIT | MSG |
|---|---|---|
| 2026-09-04 | e68e8ce43081 | eval-riscv-spcaemit-k3: initial commit |
| 2026-09-04 | e26c149426d0 | eval-riscv-spcaemit-k3: add missing category in meta |