PyTorch 의 모양,
브라우저 탭 안에서.
당신의 torch 코드가 이 탭에서 돈다 — 설치도, 서버도, 계정도 없이. 그 밑은 손으로 쓴 WGSL 로 WebGPU 위에 도는 TypeScript 런타임이고, 값은 진짜 PyTorch 의 것에 대조된다, 케이스 하나씩.
누르는 순간 네트워크로 나가는 요청은 없다. 계산은 당신의 GPU 에서 나고, 값도 여기 머문다.
내 GPU 로 확인 → 만들어 보기 → 레슨 0: 버그 고쳐 보기 → npm install borch-ts
런타임 의존성 0개 · ES 모듈 gzip 435KB(압축 전 1567KB) · 체크포인트는 safetensors
Why borch
브라우저를 런타임으로 삼으면 사라지는 것들
파이썬 ML 은 시작하기 전에 값을 치른다 — 가상환경, 버전 충돌, CUDA·cuDNN, 그리고 웹에 붙이려면 그 앞에 서버 한 겹. borch 는 그 줄을 지운다.
파이썬 환경이 없다
브라우저와 TypeScript 만으로 시작한다. 설치할 것이 없으니 버전이 충돌할 것도 없다.
Local-first
데이터도 연산도 사용자의 탭 안에 머문다. 올려 보내지 않으므로 올려 보낼 권한을 물을 일도 없다.
WebGPU 네이티브
커널을 WGSL 로 직접 썼다. TF.js 도, 다른 런타임도 거치지 않는다 — 런타임 의존성 0개.
PyTorch 를 닮았다
Tensor · Autograd · nn.Module · Optimizer. 새 개념을 발명하지 않는다. 갈리는 자리는 다섯이고 아래에 적어 두었다.
문서가 곧 실행이다
여기 있는 예시는 전부 실제로 돈다. 안 돌리는 예시는 썩고, 첫 사용자가 정확히 거기서 막힌다.
URL 하나가 곧 배포다
플레이그라운드의 공유 링크를 받은 사람은 설치 없이 같은 코드를 자기 GPU 에서 돌린다.
How it works
지나는 자리가 넷에서 둘로 준다
안에서는
TypeScript 코드 → Tensor/Autograd → 연산자 런타임 → 손으로 쓴 WGSL 셰이더 → 브라우저 GPU. 중간에 다른 런타임이 없어서, 느린 자리가 나오면 그것은 우리 셰이더다.
WebGPU 가 없으면
WebGPU 가 없으면 안 돈다. `init()` 은 다른 API 로 손을 뻗는 대신 멈춘다 — 여기 있던 TF.js 판은 WebGL 로 조용히 내려갔고 그 수가 한동안 GPU 의 것으로 읽혔다. WebGPU 의 소프트웨어 어댑터는 그것과 다르다: 같은 API, 같은 커널, 값도 맞는다 — 골든이 거기서도 통과한다. CPU 의 것은 시계뿐이고, 배지가 무엇을 받았는지 이름으로 말한다. 내 플랫폼은 무엇을 요구하나 →
내 플랫폼이 무엇을 요구하나 — macOS·iOS·안드로이드는 아무것도, NVIDIA 카드를 단 리눅스는 크롬 스위치 둘, 그리고 아직 안 쟀다 인 칸이 둘. 측정값과 각 경우에 무엇을 하는지는 여기에 있다.
await init() 이 먼저다(어댑터를 잡는 것이 비동기다) ·
값을 읽는 것이 await t.item() 이다(순방향·역방향은 동기다) ·
한 스텝을 scope() 로 감싼다(JS 의 쓰레기 수집이 GPU 메모리를 제때 안 놓는다) ·
살아남아야 하는 텐서는 keepAlive() 로 표시한다 ·
모델을 부르는 것은 model.call(x) 다(JS 는 객체를 그냥 못 부른다).
nn.Parameter(t) 는 저장소를 공유하지 않고 복사한다
(여기엔 뷰가 없다. 파이썬 바인딩도 같은 선택을 해서 GPU 두 쪽끼리는 갈리지 않는다) ·
borch.ts 에선 requiresGrad 만으로 매개변수가 된다(torch 는 감싼 것만 센다) ·
get_num_threads() 는 1 을 답한다(모든 연산이 호출
스레드에서 돈다. torch 는 기계의 코어 수를 답한다) ·
float64 와 complex128 은 없고, int32·
int16·int8·uint8·float16·
bfloat16 은 이름만 있다 — 오타와 부재가 다르게 읽히라고
둔 것이고, int64 와 float32 로 모인다 ·
conj() 는 값을 그 자리에서 뒤집어서 is_conj() 가 참인 적이
없다(torch 의 것은 지연된다).borch-ts/test/parity.ts ("here we part from
torch") 와 tests/test_subset_claims.py 에 못 박혀 있다. 골든은 torch 와
일치하는 것만 담는다 — 주석에만 적힌 분기는 다음 사람이 주석을 안 읽고 바꾸는
분기다.
배우기 · 튜토리얼 · 모델 · 비전 · API · Playground
들어가는 길 여섯, 전부 도는 것
강의를 읽으면서 읽고 있는 그 페이지에서 Run 을 누른다. 이름을 찾으면 컴파일러가 낸 시그니처를 그대로 본다. 아니면 손실 곡선과 GPU 계측이 붙은 큰 편집기를 연다. 셋 다 설치할 것이 없다.
배우기 — 열 강
텐서 → autograd → 모듈 → 학습 → 합성곱 → 저장 → 데이터 → 디버깅. 모든 코드 블록이 그 페이지에서 돌고, 돌리기 전에 고칠 수 있다.
◆튜토리얼 — 프로젝트 열
문제 하나를 끝까지: 분류기, 날텐서에서 다시 세운 학습 루프, CIFAR-10, 적대적 예제, 문자 RNN, FFT, 어텐션, 오토인코더, 최소제곱.
◆API 레퍼런스
공개된 모든 이름을 컴파일러의 선언 파일에서 뽑았다 — 시그니처, 갈래, 그리고 대응하는 torch 이름. 검색된다.
◆Playground
자바스크립트와 파이썬을 받는 큰 편집기. 손실 곡선과 dispatch 수, 그 실행이 쥔 GPU 메모리가 함께 보인다.
◆모델 — 허브
탭으로 바로 실리는 사전학습 가중치. 페이지가 하나를 받아서 발행자가 얼려둔 샘플과 눈앞에서 대조한다.
◆비전 — torchvision 의 자리
transforms, v2, 박스·마스크·키포인트의 기하, 그리고 데이터셋 디코더 — 각각이 조용히 틀어지는 방식과 함께.
넷 다 같은 런타임이다. 강의의 블록, 튜토리얼의 블록, 플레이그라운드의 편집기가 같은 코드를 돌린다 — 다른 것은 페이지에서 글이 차지하는 몫과 당신이 차지하는 몫의 비율뿐이다.
같은 커널, 파이썬 표면
교재 코드를 임포트만 바꿔 브라우저에서 돌린다
플레이그라운드에는 파이썬 모드가 있다. Pyodide 위의 borch_webgpu 가
같은 WGSL 커널을 부르므로, 자바스크립트 예시와 손실 값이 자릿수까지 같다.
Pyodide 도 numpy 도 이 서버에서 온다 — 밖으로 나가는 요청은 없다.
이렇게 쓴다
import borch_webgpu as torch
model = torch.nn.Linear(1, 1)
opt = torch.optim.SGD(model.parameters(), lr=0.1)
crit = torch.nn.MSELoss()
for step in range(201):
with torch.scope(): # 브라우저라서 늘어난 유일한 줄
opt.zero_grad()
loss = crit(model(x), y)
loss.backward()
opt.step()
await 이 없는 이유
WebGPU 에 동기 읽기가 없는데도 loss.item() 이 그냥 값을 준다 —
Pyodide 의 run_sync(JSPI)가 그 자리를 메운다. 실측된 조건이 하나 있다:
페이지가 비동기로 파이썬에 들어가야 한다. 그래서 await init()·
.call()·값 읽기의 비동기가 전부 사라지고, 남는 차이는
scope() 한 줄뿐이다.
그 한 줄을 빼면 어떻게 되는지도 이 페이지에서 볼 수 있다 — 같은 학습에서 쥐고 있는 GPU 메모리가 1.0MB 에서 228.9MB 로 간다(이 기계에서 잰 것).
처음 한 번은 Pyodide 를 올리느라 몇 초 걸리고 그 뒤로는 바로 돈다. 파이썬 페이지가 그 표면을 통째로 다룬다 — 이름을 어떻게 넘기는지, 불러오는 데 무엇이 드는지, 어디서 멈추는지. 플레이그라운드에 파이썬 예제가 다섯 있다 — 텐서, 자동미분, 선형회귀, MLP, CNN. 6강은 같은 학습 고리를 두 언어로 돌린다.
지금 있는 것 · 아직 없는 것
거절 목록이 긴 것이 의도다
목표는 "PyTorch 재현" 이 아니라 커리큘럼이 쓰는 범위 안에서의 동등성이다. 아래 표는 계획이 아니라 지금 상태다 — 없는 것은 없다고 적는다.
| 조각 | 상태 | 지금 무엇인가 |
|---|---|---|
| Tensor | 있다 | 모양·dtype·장치, 브로드캐스트, 색인·뷰, 저장소 공유까지 |
| WebGPU 백엔드 | 있다 | 손으로 쓴 WGSL 커널. 폴백 없음 |
| Autograd | 있다 | 역방향 자동미분. 값뿐 아니라 "기울기가 흐르는가" 도 따로 검사한다 |
| nn.Module | 있다 | Linear·Conv1d/2d/3d·정규화·풀링·RNN/LSTM/GRU 셀·손실 여러 종 |
| Optimizer | 있다 | SGD·Adam·RMSprop 외 여럿, LR 스케줄러 포함 |
| Playground | 있다 | 이 페이지. 코드·실행·출력·손실 곡선·GPU 계측 |
Vision (borchvision · borch.vision) | 있다 | transforms · v2 · functional · ops, 그리고 데이터셋 15개 — 값을 전부 진짜 torchvision 과 대조했다 |
| 모델 I/O | 있다 | safetensors — 파이썬 쪽 borch·numpy·HF 도구가 같은 파일을 읽는다 |
모델 카탈로그 (bimm) | 일부 | createModel(라이브러리, 이름, 인자) 로 만들어진다. npm 에 bimm-ts 로 올라가 있다. 카탈로그는 다섯 계열이고, timm 의 수백 개가 아니다 |
| Learn (인터랙티브 튜토리얼) | 있다 | 레슨 열에 튜토리얼 여섯, 모든 코드 블록이 페이지 안에서 돈다 — 텐서부터 ResNet 과 비전 트랜스포머까지 |
Hub (borch-hub) | 일부 | 매니페스트·해시 대조·환경 판정. 가중치는 브라우저에서 실제로 받아진다 — 44.7MB safetensors 에 access-control-allow-origin: *, 실측. npm 에 borch-hub 로 올라가 있다 — 이 사이트가 목차를 읽고 모델을 받아 검증한다. 개수는 여기 안 적는다, 움직이니까 |
| WebGPU 없이 — 파이썬 모드가 wasm 위에서 | 있다 | Pyodide 가 곧 wasm 이고 코어는 numpy 다. 어댑터 없이 import borch as torch 로 학습이 된다 — 브라우저에서 navigator.gpu 를 지우고 실측: loss 17.2945 → 0.000001. 그때 없는 것은 borch_webgpu 이고, 임포트가 안 되는 것으로 그렇게 말한다 |
| CUDA · 분산 · 혼합정밀도 · torch.compile | 안 한다 | 브라우저에 존재할 수 없거나, 그것을 배우려면 브라우저를 벗어나야 하는 것들 |
패키지 넷, 런타임 하나
이제 라이브러리 하나가 아니다
각자 이미 아는 자리를 하나씩 차지한다. 마지막 하나만 빼고 — 그것은 파이썬 쪽에 대응하는 것이 없다.
| 여기 | 저기 | 무엇을 들고 있나 |
|---|---|---|
| borch | torch |
텐서·autograd·모듈·옵티마이저 — 나머지 전부가 딛고 서는 런타임 |
| borchvision | torchvision |
transforms, v2, 박스·마스크 기하, 데이터셋 디코더 — 이름 663개 |
| bimm | timm |
아키텍처 카탈로그 — ResNet, EfficientNet, MobileNet, ViT |
| borch-hub | — | 발행된 매니페스트, 싣기 전의 해시 대조, 받기 전의 환경 판정 |
대응이 없는 것은 마지막 줄이다. torch.hub 는 저장소에서
받아오고 자기 카탈로그가 없다. timm 은 카탈로그를 갖고 있고 매니페스트를 발행하지
않는다. 여기서는 모델이 자기가 재현해야 하는 샘플과 함께 도착하고,
당신 브라우저가 그것을 먼저 대조한 다음에야 실렸다고 말한다.
10메가바이트쯤 들고 몇 초면 끝난다. 설치하는 것은 없다.
잰 것만 적는다
얼마나 빠른가 · 얼마나 맞는가
CIFAR ResNet-18, 배치 64 — 같은 기계, 같은 벤치
| TF.js 판 (걷어냈다) | borch.ts | borch-webgpu | |
|---|---|---|---|
| ms / step | 154.9 | 118.5 | 123.4 |
| 에폭 | 2.02분 | 1.55분 | 1.61분 |
| 시험 정확도 (10 에폭, 늘리기 켬) | 60.4% | 64.6% | 안 쟀다 |
다시 잰 값 — 2026-09-03, 같은 페이지, 어댑터 명기
| CIFAR ResNet-18, same page | batch 16 | batch 32 | batch 64 |
|---|---|---|---|
apple / metal-3 — borch.ts | 38.6 ms/step | 63.1 | 119.1 |
apple / metal-3 — TF.js 4.22.0 | 86.4 | 170.0 | 347.5 |
| 비율 | 2.2× | 2.7× | 2.9× |
nvidia / lovelace (RTX 4090, Chrome 143) — borch.ts | 28.1 | 38.7 | 69.7 |
nvidia / lovelace — TF.js 4.22.0 | 67.8 | 112.2 | 205.8 |
| 비율 | 2.4× | 2.9× | 3.0× |
borch-ts/test/compare.ts 가 같은 step 을 TF.js 4.22.0(자체 layers API·NHWC, WebGPU 백엔드, tests/browser/assets.lock 으로 바이트 고정)으로 borch.ts 바로 뒤에 같은 페이지에서 돌린다. 같게 둔 것: 구조, SGD 0.05/0.9, 교차엔트로피, 같은 시드 배치, 워밍 2회 뒤 5회 측정, 매 step 손실 readback. npm run compare:ts 로 재현.
이 라이브러리가 지는 반쪽 — 추론, 같은 페이지
| ResNet-18 (CIFAR) forward | 어댑터 | batch 1 | batch 16 |
|---|---|---|---|
borch.ts, eval() | apple / metal-3 | 4.1 ms | 10.6 ms |
borch.ts, eval() + fuse_conv_bn_eval + nn.intrinsic | apple / metal-3 | 2.9 ms | 7.8 ms |
| ONNX Runtime Web 1.29.0 | apple / metal-3 | 4.4 ms | 5.3 ms |
| 융합한 쪽 대비 ORT 가 빠른 배수 | 0.7× | 1.5× | |
borch.ts, eval() | nvidia / lovelace (RTX 4090, Linux) | 3.6 ms | 8.1 ms |
borch.ts, eval() + fuse_conv_bn_eval + nn.intrinsic | nvidia / lovelace | 2.8 ms | 4.6 ms |
| ONNX Runtime Web 1.29.0 | nvidia / lovelace | 3.4 ms | 3.5 ms |
| 융합한 쪽 대비 ORT 가 빠른 배수 | 0.8× | 1.3× |
가중치는 둘 다 같다(ResNet-18 하나를 torch 에서 tests/browser/export_resnet18.py 로 safetensors 와 ONNX 로 내보냄). WebGPU 실행기. 두 런타임이 torch 로짓을 1e-3 안에서 재현한 뒤에만 표를 찍는다 — 실측 Apple 7.5e-8 / 6.7e-8, NVIDIA 7.5e-8 / 4.5e-8. forward, 워밍 3회 뒤 20회 평균, readback 포함. eval 모드 batch norm 을 한 커널로 만들고 nn.utils.fusion.fuse_conv_bn_eval(torch 와 같은 이름)로 norm 을 앞 conv 에 접으면 forward 가 176 → 56 디스패치가 된다 — 그 전의 표는 Apple 9.9 / 18.8 ms, 4090 6.7 / 14.5 ms 였다. 세 번째는 conv 커널이었다 — 산술이 아니라 그리드: 4×4 평면의 512→512 채널은 배치 16 에서 워크그룹 32개, 배치 1 에서 8개뿐인데(SM 128개짜리 카드에서, 피크의 약 1 %), 리덕션은 4,608 길이다. forward 도 가중치 gradient 처럼 리덕션을 쪼개자(convForwardSplit) 그 층의 GPU 시간이 배치 1 에서 2.8 → 0.3 ms 가 됐다. 네 번째는 호출 자체였다 — 4090 의 5.6 ms 중 GPU 일은 3.0 ms, 디스패치 64개 — 그래서 relu 와 잔차 덧셈이 conv 의 에필로그를 타게 했다. torch 의 torch.ao.nn.intrinsic 이름(ConvReLU2d, ConvAddReLU2d, 여기서는 nn.intrinsic)이다: 디스패치 39개, 5.6 → 4.6 ms. 배치 1 의 융합 네트워크는 두 어댑터 모두에서 ORT 보다 앞서고, 배치 16 은 1.3–1.5× 안이다. 남은 것은 초기 층 — 64채널 32×32 conv 는 곱하는 것보다 읽는 것이 많다. 그리고 파일은 ONNX 로 나간다: onnx.exportOnnx(model, sample)(파이썬 바인딩은 torch.onnx.export)가 forward 한 번을 추적해 ORT Web 이 돌리는 파일을 쓴다 — 우리 forward 와 3.5e-8 안에서 일치하고, 추적하지 않은 배치에서도 그렇다. 이 페이지에서 ORT 가 돌리는 것은 융합한 네트워크가 직접 내보낸 파일이다: torch 로짓과 Apple 에서 4.5e-8, 4090 에서 5.2e-8, 시간은 3.1 / 5.3 ms 와 3.2 / 3.4 ms — torch 가 내보낸 파일과 같은 속도다. 추론만 필요하면 ORT Web 을 쓰라. 이 라이브러리에 있고 저쪽에 없는 것은 위의 학습 step 과 torch 모양의 코드다.
기계 하나이고, 그 기계가 여기 안 적혀 있다. 세 열을 서로 비교할 수 있는 것은 같은 상자에서 서로 맞대어 쟀기 때문이고 — 남의 수와는 비교할 수 없다. 어느 상자였는지 이 페이지가 말하지 않기 때문이다. 정확성은 이제 두 벤더에서 쟀고 속도는 기계 하나다. 이 줄이 그 이름을 댈 수 있게 될 때까지, 이 열들은 속도가 아니라 비율로 읽어야 한다.
오른쪽 둘은 같은 커널이다 — 차이 4.9ms 가 파이썬(Pyodide)을 한 번 지나는 값이다. 직접 쓴 WGSL 이 같은 벤치에서 TF.js 판보다 빨랐고 랭크 한계도 없어서 그쪽을 걷어냈다.
진짜 PyTorch 와 대조한 결과
| 등급 | 지금 |
|---|---|
T1 값·기울기 (allclose 1e-5) | 100% — 생성 케이스 132개 |
| T2 오류 동등 | 12/12 — 예외 종류 · 검색 가능한 메시지 9/9 |
T3 표현(repr) 동등 | 15/15 |
| dtype 승격 | 112/112 |
| 저장소 공유 (view · slice) | 13/13 |
| 흔한 API 이름 | 144/144 |
| T4 비트 동등 | 명시적 비목표 |
그 위에 골든 4744건이 세 구현을 같은 기대값에 대조한다. 진짜 torch 를 브라우저에
넣을 수 없어서, 네이티브에서 굳힌 기대값을 브라우저로 들고 간다. Apple Metal 에서 골든이 전건 같다 —
agreeing 4733 / 4733 [apple / metal-3].
NVIDIA 에서도 같다 — RTX 5080 에서 3880 / 3880 [nvidia / blackwell], 가상 프레임버퍼가
아니라 진짜 데스크톱 세션에서다. 2026-08-30 에 잰 것이고, 그때 바인딩 몫이 3880 이었다.
학습도 맞는다 — 작은 ResNet 30 스텝의 궤적, 층별 기울기,
배치정규화 버퍼, eval 까지 torch 와 대조해 64 values. 파이썬 바인딩으로 한 번,
borch.ts 자체 API 로 한 번.
몇 달 동안 두 번째 벤더로 읽혔던 그 실행은 두 번째 벤더가 아니었다. 리눅스 GPU 서버에서
845/845 로 통과했지만 어댑터가 google / swiftshader, 즉 CPU 였다. 값은 맞았고 벤더 주장은
틀렸다 — 러너가 어댑터를 출력만 하지 않고 판정하게 된 이유가 그것이다.
패키지 다섯, 그중 셋이 이 이름을 나눠 쓴다
어디서 돌릴 것인가에 따라 고른다
| 무엇 위에 | 어디서 | 천장 | |
|---|---|---|---|
pyborch — import borch | numpy | 어디서나 · Pyodide | MNIST 급 |
| borch-ts (npm) | WGSL 직접, 의존성 0 | 브라우저 안에서만 | CIFAR ResNet-18 에폭 1.5분 |
| bimm-ts (npm) | borch-ts | 브라우저 안에서만 | 아키텍처 카탈로그 — 이름 하나로 모델까지 |
| borch-hub (npm) | borch-ts, bimm-ts | 브라우저 안에서만 | 발행된 모델 — 싣기 전에 해시를 대조한다 |
| borch-webgpu (파이썬) | 위의 borch.ts | 브라우저 안에서만 | 같은 것이 에폭 1.6분 |
첫 행은 확인하기 전까지 borch (PyPI) 라고 적혀 있었다. 배포 이름은
pyborch 이고 borch 는 import 하는 이름이다. 그리고 PyPI 에
올라가 있지도 않다 — 거기 borch 는 남의 패키지(확률 프로그래밍
라이브러리)라서 pip install borch 는 모르는 사람의 코드를 받아 온다.
아래의 wheel 이 설치 방법이다.
npm 행도 borch 였고 그 패키지는 존재하지 않았다 — 레지스트리가
borsh·batch·touch 와 너무 비슷하다며
그 이름을 거부한다. borch-ts 로 올라가 있고, 이 저장소가 처음부터
그렇게 불러 온 이름이다.
TypeScript 쪽 — 이 페이지가 쓰는 것
npm install borch-ts
import { init, Tensor, nn, optim, scope, keepAlive } from "borch-ts";
await init();
파이썬 쪽
uv add ./pyborch-1.4.0-py3-none-any.whl
import borch as torch # 임포트만 바꾸면 된다
import borch_webgpu as torch # Pyodide 안에서 GPU 로
Build AI where your users already are — in the browser.
설치도, 서버도, 계정도 없다. 탭 하나면 된다.