borch

잰 것만

이 탭의 GPU

borch 는 WebGPU 위에서 돈다. 즉 브라우저가 내주는 GPU 위에서 돈다. 브라우저가 안 내줄 때가 있다. 이 페이지는 그때 무엇을 하는지를 플랫폼별로 적고 — GPU 를 끝내 못 얻었을 때 borch 가 무엇을 하는지도 적는다.

먼저 배지를 읽는다

코드를 돌리는 모든 페이지가 어댑터 이름을 그대로 단다. 그 배지가 진단의 전부다:

배지는 어느 장치인지를 말하고, 내 GPU 로 확인은 그 위에서 답이 맞는지를 말한다. 골든 전부를 — 진짜 PyTorch 로 굳힌 케이스 하나하나를 — 당신의 탭에서 돌리고, 개수 옆에 어댑터를 함께 찍는다. 이 페이지의 벤더 줄은 전부 여기 누군가가 가진 기계에서 나왔고, 그래서 그중 하나가 몇 달 동안 틀려 있었다.

macOS

할 일이 없다. Apple M4 Max 에서 측정 — 어댑터가 apple / metal-3 으로 오고, 플래그도 설치도 설정도 없이 Run 이 돈다. 이 사이트에서 apple / metal-3 이라 적힌 수는 전부 그렇게 잰 것이다. 스위트 전체도 그렇다 — agreeing 4733 / 4733 [apple / metal-3].

첫 실행이 얼마나 걸리나 — 어댑터별

tests/browser/first_run.py 로 2026-09-03 에 잰 값. 랜딩 페이지를 새로 열고, 장치를 probe 하고, Run 을 눌러 done 줄까지. "페이지 안" 은 페이지 자신의 타이머, "열기→done" 은 페이지를 연 시점부터의 벽시계. 배포 행은 GitHub Pages 전송이 더해진다.

어댑터방문페이지 안열기→done기다림의 정체
apple / metal-3
M4 Max, Chrome
첫 방문1.35 s1.6 s어댑터는 수십 ms 에 답한다. 첫 클릭은 Python 의 첫 import(0.5 s)와 실행값을 치른다
재방문0.66 s1.3 s
배포 사이트, 첫 방문1.71 s1.9 s
nvidia / lovelace
RTX 4090, 드라이버 550, Ubuntu (Xorg + GNOME), Chrome 143, 모니터 켠 상태
첫 방문1.60 s1.8 sApple 과 같다: adapter 14 ms, device 15 ms, 셰이더 22 개 1 ms, 첫 readback 28 ms. 켜진 모니터 앞에서 잰 값 — 같은 기계가 화면이 꺼진 채로는 무엇을 읽었는지는 아래 주석에.
재방문1.51 s1.6 s
nvidia / blackwell
RTX 5080, 드라이버 580, Ubuntu (Xorg + GNOME), Chrome 151
화면을 켠 상태로 다시 잴 것이전 행(첫 방문 2.1 s, "1초 단위 3초 핸드셰이크")은 ssh 로 꺼진 모니터 앞에서 잰 것이고, 그것이 잰 것은 모니터였다 — 철회.

첫 학습까지 — 학습자의 시계

첫 화면은 버튼을 누르는 순간 Python 으로 모델 하나를 학습한다. 이 시계는 배포된 사이트에서 페이지를 여는 순간 시작하고(다운로드가 안에 들어간다), 읽는 사람이 느끼는 순간마다 멈춘다: 버튼 밑에 Python 준비됨, 클릭 뒤 첫 loss 줄, 배운 것 줄. tests/browser/learner_path.py 가 매일 밤 잰다.

어댑터열고 나서 Python 준비까지클릭 → 첫 loss 줄클릭 → 배운 것재방문, 바로 클릭 → 배운 것레슨 0, 블록 셋
apple / metal-3
M4 Max, Chrome
4.4 s0.7 s0.8 s1.4 s2.4 s
nvidia / lovelace
RTX 4090, 드라이버 550, Ubuntu, Chrome 143, 모니터 켠 상태
4.7 s0.6 s0.6 s1.6 s2.6 s

꺼진 모니터는 GPU 의 모든 대기를 1초 대기로 만든다. 이 표를 처음 채웠을 때 4090 행은 Python 준비까지 8.6 s, 클릭에서 "배운 것"까지 6.5 s 였고, 그 위의 행은 어댑터 핸드셰이크가 1초 단위로 3초 걸린다고 했다. 둘 다 ssh 로, GNOME 이 5분 유휴 뒤 HDMI 모니터를 꺼 둔 데스크톱에서 잰 것이다(xset q: "Monitor is Off"). 꺼진 화면 앞에서는 프레임 스왑이 약 1초에 타임아웃되고 GPU 의 모든 대기가 그 프레임 뒤에 줄을 선다: 어댑터 요청 3.0 s, 페이지에 한 줄 쓴 뒤의 readback 1.0 s, loss 를 찍는 학습 루프 7초. 모니터를 깨우자 같은 프로브가 52 ms, 46 ms, 0.56 s 를 읽었다. tests/browser/readback_probe.py --fresh 가 줄마다 재현한다. 타이밍 스크립트는 이제 꺼진 화면 앞에서는 재기를 거부하고 그 사실을 말한다.

이 라이브러리 쪽에서 하는 것: 페이지가 어댑터를 한 번만 요청하고(probe 가 받은 어댑터를 init() 이 그대로 쓴다), 읽는 동안 device 와 Python 을 미리 준비해 클릭은 실행값만 치르게 한다. 화면이 켜져 있으면 NVIDIA 드라이버 위 Linux 는 여기 있는 모든 숫자에서 Apple 과 같다.

NVIDIA 카드를 단 리눅스

플래그 둘이면 충분하고, 러너도 이제 그 둘을 보냅니다. 그 둘이 없을 때 벌어지는 일은 거절이 아닙니다 — 그보다 조용합니다. 크롬은 자기 CPU 래스터라이저를 내주고 페이지를 돌게 두며, Vulkan 백엔드가 켜질 때까지 계속 그럽니다. 두 카드를 같은 방식으로 쟀습니다 — 진짜 X 세션과 시스템 크롬:

크롬을 이렇게 띄우면 RTX 5080
드라이버 580 · 크롬 151
RTX 4090
드라이버 550 · 크롬 143
아무것도 없이없음없음
--enable-features=Vulkan 단독 nvidia / blackwell 없음
--enable-unsafe-webgpu google / swiftshader google / swiftshader
  + --ignore-gpu-blocklist google / swiftshader google / swiftshader
  + --enable-features=Vulkan nvidia / blackwell nvidia / lovelace
  + --disable-gpu-driver-bug-workarounds nvidia / blackwell nvidia / lovelace
--enable-unsafe-webgpu --enable-features=Vulkan
이 행이 러너의 목록이다
nvidia / blackwell nvidia / lovelace

그러니 붙여넣을 줄은 이것입니다. 플래그 둘, 그리고 일을 하는 것은 둘째 — 리눅스에서 WebGPU 는 크롬의 Vulkan 백엔드 위에서 돌고, 그것이 켜지기 전까지 브라우저는 거절하는 대신 자기 CPU 래스터라이저를 대신 내줍니다. 카드가 막힌 적은 없었고, 백엔드가 꺼져 있었을 뿐입니다.

google-chrome --user-data-dir=/tmp/borch-check \
  --enable-unsafe-webgpu --enable-features=Vulkan

chrome://flags 에서는 #enable-unsafe-webgpu#enable-vulkan 입니다. 바꾼 뒤 다시 띄워야 합니다 — 이미 열려 있던 창은 예전 설정을 그대로 씁니다. 여기서 한 시간을 썼습니다.

그 둘로 안 되면 --ignore-gpu-blocklist 를 더하십시오. 크롬은 드라이버에 아무것도 묻기 전에 거절하는 GPU·드라이버 조합 목록을 들고 있고, 그 플래그가 그것에게 가서 보라고 말하는 방법입니다. 여기서 잰 어느 기계에서도 아무것도 바꾸지 않았습니다 — 다만 그 기계들은 하나도 그 목록에 없었습니다. 그러니 이 페이지는 그 플래그가 존재하는 이유가 되는 상황을 본 적이 없습니다. 당신 기계가 그것일 수 있습니다.

그 두 플래그로 스위트 전체를 돌렸습니다. 어댑터만 물어본 것이 아니라 — 5080 에서 agreeing 4460 / 4466 [nvidia / blackwell], 그리고 학습도 맞습니다. 작은 ResNet 30 스텝의 궤적, 층별 기울기, 배치정규화 버퍼, eval 까지 torch 와 대조해 64 values, 파이썬 바인딩으로 한 번 borch.ts 자체 API 로 한 번.

갈린 여섯은 GPU 가 아니라 그 기계에 대한 것입니다. 여섯 다 device='cuda' 일 때 무슨 일이 나는지를 묻는데, 그 답이 torch 에 CUDA 가 없는 기계에서 얼려졌습니다. 거기서는 양쪽 다 거절하니 케이스가 둘 다 멈춘다가 됩니다. 그 박스는 CUDA 빌드라 torch 가 안 멈추고, cuda.device_count 는 골든이 0 을 기대하는 자리에서 1 이라고 답합니다. 셰이더에서 나온 값은 하나도 없습니다 — 같은 여섯이 Apple Metal 에서는 통과합니다. 얼려진 기계의 성질을 같이 얼려버린 케이스의 모양입니다. 이 종류가 앞서 둘 발견돼 고쳐졌고, 이것들이 그 가족의 나머지입니다.

4090 은 어댑터 측정만 있습니다. 세션 도중 카드가 PCIe 버스에서 빠진 뒤 돌아오지 않아 골든은 돌린 적이 없습니다.

이 표의 이전 판은 차단목록 플래그가 5080 을 연다고 말했습니다. 아닙니다 — 오늘 같은 기계에서 세 번 돌려 전부 google / swiftshader 였습니다. 그 값은 다른 브라우저(Playwright 가 들고 다니는 크로미움, 그리고 더 옛 크롬)에서 나온 것이고, 칸에 크롬 버전을 적은 이유도 그것입니다. 두 카드는 카드·드라이버· 브라우저가 한꺼번에 다릅니다. 그러니 칸 사이의 차이를 하드웨어에만 돌릴 수 없습니다. --enable-features=Vulkan 단독이 5080 에는 닿고 4090 에는 안 닿는데, 그 사이에 크롬 메이저 여덟 개가 놓여 있습니다.

WebGPU 워킹그룹의 구현 현황은 리눅스에 다른 조합을 줍니다 — --ozone-platform=x11 --use-angle=vulkan --enable-features=Vulkan,VulkanFromANGLE — 그리고 그것도 카드에 닿습니다. 통하는 이유는 안에 --enable-features=Vulkan 이 있어서입니다. --use-angle=vulkan 단독은 어댑터를 아예 못 주고, --ozone-platform=x11 은 필요 없었습니다.

AMD·Intel 카드를 단 리눅스

안 쟀다. 위 차단 목록은 NVIDIA-리눅스 항목이라 같은 스위치 둘을 먼저 시도할 만하지만, 그 카드들에서는 아무것도 돌려보지 않았다. 짐작으로 적으면 이 저장소가 쓰는 쪽이 아니라 걷어내는 쪽의 문장이 된다.

윈도우

할 일이 없다. NVIDIA 카드를 단 윈도우 데스크톱에서 측정 — 플레이그라운드 배지가 nvidia / blackwell 로 왔고 플래그를 하나도 안 켜고 Run 이 돌았다. 윈도우의 크롬은 위 리눅스 절이 다루는 Vulkan 경로가 아니라 D3D12 를 쓰므로 그 사다리는 그쪽에 안 옮겨간다 — 그리고 옮겨갈 필요도 없었다.

이 칸은 누가 페이지를 열기 전까지 안 쟀다였다. 여기 있던 글이 자기 은퇴 조건을 스스로 적어뒀다 — "누군가 그 위에서 페이지를 열기 전까지 안 쟀다로 둔다. '된다고 읽었다'고 적는 페이지와 '도는 것을 봤다'고 적는 페이지는 다른 페이지". 구현 현황 페이지는 x86·x64 윈도우를 Chrome 113 부터 기본 켜짐으로 적고 있었고, 그것을 읽은 것은 본 것과 끝내 같지 않았다. 돌아온 답과 일치하며, 그건 알아둘 값어치가 있지 이 문단이 바뀐 이유는 아니다.

ARM64 윈도우는 아직 안 쟀다. 같은 페이지가 그쪽은 여전히 #enable-unsafe-webgpu 뒤로 적는다 — 그러니 "윈도우는 된다"는 아직도 어느 윈도우냐에 따라 답이 둘이고, 그중 하나만 여기서 봤다.

폰과 태블릿

iOS 에서는 돈다. iPhone 13 mini 에서 측정 — 배지에 Apple 어댑터가 왔고 실제로 돌았다. 어댑터를 얻은 것에서 그치지 않고 화면에 결과가 나왔다는 뜻이고, 폰에서는 그 둘을 따로 확인할 값어치가 있다. iOS 버전은 적어두지 않아서 여기서 주장하지 않는다.

이 한 번의 측정이 데스크톱의 한 번보다 많은 것을 말한다 — iOS 의 모든 브라우저가 WebKit 을 쓴다. 시험할 두 번째 엔진이 없다. 사파리도, 다른 브라우저처럼 보이는 것들도 밑은 같은 엔진이다.

안드로이드에서도 돈다 — 갤럭시 Z 플립 6 의 크롬에서 측정. Adreno 어댑터가 왔고, 어댑터만이 아니라 화면에 결과가 나왔다. 소프트웨어가 아니라 진짜 GPU 이고, 이 페이지에서 중요한 구분은 그것이다.

다만 기기 하나이고, 안드로이드에서는 그 점이 가장 크다. 위의 iOS 줄이 iOS 전체를 덮는 것은 거기 엔진이 하나뿐이기 때문이다. 안드로이드는 브라우저도 엔진도 여럿이고, 그 밑의 GPU 계열도 셋이다 — Adreno·Mali·Xclipse. 스냅드래곤 폰의 크롬은 그 격자의 한 칸이다. 안드로이드 버전도 적어두지 않아서 주장하지 않는다.

배지에 어댑터 이름이 붙어 있는 이유

이 프로젝트의 예전 판은 WebGPU 를 못 잡으면 WebGL 로 떨어졌고, 한동안 그 저자들이 CPU 경로에서 잰 수를 GPU 의 수로 읽었다. 다른 모양으로 또 일어났다 — 845 건을 전부 통과한 골든 실행이 *"두 번째 벤더에서 확인"* 으로 기록됐는데, 어댑터는 내내 google / swiftshader 였다. 그 문장은 제대로 측정될 때까지 문서 셋에 살아 있었다.

그러니 규칙은 "CPU 에서는 절대 안 돈다" 가 아니라 "CPU 에서 잰 수가 GPU 의 수로 읽히면 안 된다" 이다. 배지에 어댑터 이름이 있는 이유, 소프트웨어 어댑터에서 배지가 꺼지는 이유가 그것이다.

이 페이지는 여기서 하루 동안 거짓을 말했고, 정정을 지우지 않고 남긴다. init() 이 소프트웨어 어댑터를 조용히 받는 대신 멈춘다고 적었었다. 멈춘 적이 없다 — borch-ts/src 에 그런 검사가 없었고, SwiftShader 에서 통과한 모든 골든 실행이 그 증거다. 알려주는 것은 배지이고, 라이브러리는 브라우저가 주는 것을 받는다.

일부러 CPU 에 물어보기

어느 어댑터를 받을지는 브라우저가 정하므로, 소프트웨어 쪽을 일부러 달라고 하는 길이 있다. CPU 경로를 보고 싶을 때, 또는 내 코드가 특정 장치에 기대고 있지 않은지 확인할 때 쓴다:

await init({ forceFallbackAdapter: true });
const { software } = await probe();   // 그리고 무엇을 받았는지 안다

덜떨어진 실행이 아니라 진짜 실행이다. 오늘 측정 — 골든 전체가 양쪽에서 통과한다. google / swiftshader 에서 3079 / 0, apple / metal-3 에서 3079 / 0. 같은 코드, 같은 값, 다른 장치. 소프트웨어 실행이 아닌 것은 속도의 출처 하나다.

GPU 를 끝내 못 얻으면

그 문장 밑에 서로 다른 두 상황이 숨어 있고 답이 다르다. 브라우저가 소프트웨어 어댑터라도 내주면 위 절이 당신의 경우다 — SwiftShader 도 WebGPU 이고, 코드는 돌고, GPU 의 것이 아닌 것은 속도뿐이다. 아무것도 안 내주면 borch.ts 가 줄 것이 없고 없는 척하는 대신 그렇다고 말한다. WebGPU 를 돌아가는 길은 없다.

파이썬 쪽은 다른 구현이다. 코어가 numpy 라 어댑터 없이 돈다 (navigator.gpu 를 지운 브라우저에서 측정: loss 17.2945 → 0.000001). 그것이 있다는 걸 아는 건 값어치가 있지만, 무엇인지는 분명히 해야 한다 — 그건 GPU 에서 CPU 로 바꾸는 스위치가 아니라 언어를 바꾸는 일이다. 같은 TypeScript 코드가 거기서 도는 것이 아니다.