borch

PyTorch 의 모양,
브라우저 탭 안에서.

당신의 torch 코드가 이 탭에서 돈다 — 설치도, 서버도, 계정도 없이. 그 밑은 손으로 쓴 WGSL 로 WebGPU 위에 도는 TypeScript 런타임이고, 값은 진짜 PyTorch 의 것에 대조된다, 케이스 하나씩.

hero.py — 학습 루프, 이 페이지에서 돈다 장치 확인 중…


        

누르는 순간 네트워크로 나가는 요청은 없다. 계산은 당신의 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

지나는 자리가 넷에서 둘로 준다

서버형 Browser HTTP Python API PyTorch GPU
borch Browser borch WebGPU GPU

안에서는

TypeScript 코드 → Tensor/Autograd → 연산자 런타임 → 손으로 쓴 WGSL 셰이더 → 브라우저 GPU. 중간에 다른 런타임이 없어서, 느린 자리가 나오면 그것은 우리 셰이더다.

WebGPU 가 없으면

WebGPU 가 없으면 안 돈다. `init()` 은 다른 API 로 손을 뻗는 대신 멈춘다 — 여기 있던 TF.js 판은 WebGL 로 조용히 내려갔고 그 수가 한동안 GPU 의 것으로 읽혔다. WebGPU 의 소프트웨어 어댑터는 그것과 다르다: 같은 API, 같은 커널, 값도 맞는다 — 골든이 거기서도 통과한다. CPU 의 것은 시계뿐이고, 배지가 무엇을 받았는지 이름으로 말한다. 내 플랫폼은 무엇을 요구하나 →

내 플랫폼이 무엇을 요구하나 — macOS·iOS·안드로이드는 아무것도, NVIDIA 카드를 단 리눅스는 크롬 스위치 둘, 그리고 아직 안 쟀다 인 칸이 둘. 측정값과 각 경우에 무엇을 하는지는 여기에 있다.

torch 와 갈리는 다섯 자리 — 첫 열 줄에서 전부 만난다.
await init() 이 먼저다(어댑터를 잡는 것이 비동기다) · 값을 읽는 것이 await t.item() 이다(순방향·역방향은 동기다) · 한 스텝을 scope() 로 감싼다(JS 의 쓰레기 수집이 GPU 메모리를 제때 안 놓는다) · 살아남아야 하는 텐서는 keepAlive() 로 표시한다 · 모델을 부르는 것은 model.call(x) 다(JS 는 객체를 그냥 못 부른다).
그리고 이 torch 와 갈리는 다섯 자리 — 각각 검사로 못 박혀 있어서, 바꾸면 흘러가는 대신 빨개진다.
nn.Parameter(t) 는 저장소를 공유하지 않고 복사한다 (여기엔 뷰가 없다. 파이썬 바인딩도 같은 선택을 해서 GPU 두 쪽끼리는 갈리지 않는다) · borch.ts 에선 requiresGrad 만으로 매개변수가 된다(torch 는 감싼 것만 센다) · get_num_threads()1 을 답한다(모든 연산이 호출 스레드에서 돈다. torch 는 기계의 코어 수를 답한다) · float64complex128 은 없고, int32· int16·int8·uint8·float16· bfloat16이름만 있다 — 오타와 부재가 다르게 읽히라고 둔 것이고, int64float32 로 모인다 · conj() 는 값을 그 자리에서 뒤집어서 is_conj() 가 참인 적이 없다(torch 의 것은 지연된다).
borch-ts/test/parity.ts ("here we part from torch") 와 tests/test_subset_claims.py 에 못 박혀 있다. 골든은 torch 와 일치하는 것만 담는다 — 주석에만 적힌 분기는 다음 사람이 주석을 안 읽고 바꾸는 분기다.

배우기 · 튜토리얼 · 모델 · 비전 · API · Playground

들어가는 길 여섯, 전부 도는 것

강의를 읽으면서 읽고 있는 그 페이지에서 Run 을 누른다. 이름을 찾으면 컴파일러가 낸 시그니처를 그대로 본다. 아니면 손실 곡선과 GPU 계측이 붙은 큰 편집기를 연다. 셋 다 설치할 것이 없다.

넷 다 같은 런타임이다. 강의의 블록, 튜토리얼의 블록, 플레이그라운드의 편집기가 같은 코드를 돌린다 — 다른 것은 페이지에서 글이 차지하는 몫과 당신이 차지하는 몫의 비율뿐이다.

강의 시작 → 아니면 만들어 보기 내 GPU 로 확인

같은 커널, 파이썬 표면

교재 코드를 임포트만 바꿔 브라우저에서 돌린다

플레이그라운드에는 파이썬 모드가 있다. 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 안 한다 브라우저에 존재할 수 없거나, 그것을 배우려면 브라우저를 벗어나야 하는 것들

패키지 넷, 런타임 하나

이제 라이브러리 하나가 아니다

각자 이미 아는 자리를 하나씩 차지한다. 마지막 하나만 빼고 — 그것은 파이썬 쪽에 대응하는 것이 없다.

여기저기무엇을 들고 있나
borchtorch 텐서·autograd·모듈·옵티마이저 — 나머지 전부가 딛고 서는 런타임
borchvisiontorchvision transforms, v2, 박스·마스크 기하, 데이터셋 디코더 — 이름 663
bimmtimm 아키텍처 카탈로그 — ResNet, EfficientNet, MobileNet, ViT
borch-hub 발행된 매니페스트, 싣기 전의 해시 대조, 받기 전의 환경 판정

대응이 없는 것은 마지막 줄이다. torch.hub 는 저장소에서 받아오고 자기 카탈로그가 없다. timm 은 카탈로그를 갖고 있고 매니페스트를 발행하지 않는다. 여기서는 모델이 자기가 재현해야 하는 샘플과 함께 도착하고, 당신 브라우저가 그것을 먼저 대조한 다음에야 실렸다고 말한다.

모델을 받아서 검증해 보기 →

10메가바이트쯤 들고 몇 초면 끝난다. 설치하는 것은 없다.

잰 것만 적는다

얼마나 빠른가 · 얼마나 맞는가

CIFAR ResNet-18, 배치 64 — 같은 기계, 같은 벤치

TF.js 판 (걷어냈다)borch.tsborch-webgpu
ms / step154.9118.5123.4
에폭2.02분1.55분1.61분
시험 정확도 (10 에폭, 늘리기 켬)60.4%64.6%안 쟀다

다시 잰 값 — 2026-09-03, 같은 페이지, 어댑터 명기

CIFAR ResNet-18, same pagebatch 16batch 32batch 64
apple / metal-3 — borch.ts38.6 ms/step63.1119.1
apple / metal-3 — TF.js 4.22.086.4170.0347.5
비율2.2×2.7×2.9×
nvidia / lovelace (RTX 4090, Chrome 143) — borch.ts28.138.769.7
nvidia / lovelace — TF.js 4.22.067.8112.2205.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 1batch 16
borch.ts, eval()apple / metal-34.1 ms10.6 ms
borch.ts, eval() + fuse_conv_bn_eval + nn.intrinsicapple / metal-32.9 ms7.8 ms
ONNX Runtime Web 1.29.0apple / metal-34.4 ms5.3 ms
융합한 쪽 대비 ORT 가 빠른 배수0.7×1.5×
borch.ts, eval()nvidia / lovelace (RTX 4090, Linux)3.6 ms8.1 ms
borch.ts, eval() + fuse_conv_bn_eval + nn.intrinsicnvidia / lovelace2.8 ms4.6 ms
ONNX Runtime Web 1.29.0nvidia / lovelace3.4 ms3.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 였다. 값은 맞았고 벤더 주장은 틀렸다 — 러너가 어댑터를 출력만 하지 않고 판정하게 된 이유가 그것이다.

패키지 다섯, 그중 셋이 이 이름을 나눠 쓴다

어디서 돌릴 것인가에 따라 고른다

무엇 위에어디서천장
pyborchimport borchnumpy어디서나 · PyodideMNIST 급
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.

설치도, 서버도, 계정도 없다. 탭 하나면 된다.