tiny-torch-viz / app.py
Adrian Gabriel
add model
e707779
from __future__ import annotations
import ast
import asyncio
import io
import os
import queue
import sys
import traceback
from pathlib import Path
from typing import Any, Dict
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
# Import Tracing components
from tracer import QueueSink, Tracer
from instrumentation import Instrumentor
# Import TinyTorch components
import numpy as np
from tinytorch.core.tensor import Tensor
from tinytorch.core.layers import Linear, Dropout, Layer, Sequential
from tinytorch.core.activations import ReLU, Sigmoid, Tanh, GELU, Softmax, LogSoftmax
from tinytorch.core.losses import MSELoss, CrossEntropyLoss, log_softmax
from tinytorch.core.norms import RMSNorm
# Import additional modules
from tinytorch.core.autograd import Function, enable_autograd
from tinytorch.core.optimizers import Optimizer, SGD, Adam, AdamW
from tinytorch.core.tokenization import Tokenizer, CharTokenizer, BPETokenizer, create_tokenizer, tokenize_dataset
from tinytorch.core.training import CosineSchedule, clip_grad_norm, Trainer
from tinytorch.core.embeddings import Embedding, PositionalEncoding, EmbeddingLayer, create_sinusoidal_embeddings
BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR = BASE_DIR / "static"
app = FastAPI()
@app.get("/")
async def root():
return FileResponse(STATIC_DIR / "index.html")
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
class AutoNameTransformer(ast.NodeTransformer):
"""
AST transformer that automatically wraps assignments to Tensor-like values
with a call to __auto_name__(name, value) so we can capture variable names.
Transforms:
x = Tensor([1,2,3])
Into:
x = __auto_name__("x", Tensor([1,2,3]))
"""
def visit_Assign(self, node: ast.Assign) -> ast.AST:
# Only handle simple single-target assignments like: x = ...
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
var_name = node.targets[0].id
# Skip private/dunder names
if var_name.startswith('_'):
return node
# Wrap the value in __auto_name__(name, value)
new_value = ast.Call(
func=ast.Name(id='__auto_name__', ctx=ast.Load()),
args=[
ast.Constant(value=var_name),
node.value
],
keywords=[]
)
# Create new assignment with wrapped value
new_node = ast.Assign(
targets=node.targets,
value=new_value
)
ast.copy_location(new_node, node)
ast.fix_missing_locations(new_node)
return new_node
return node
def transform_code(code: str) -> str:
"""
Transform user code to automatically capture variable names for Tensors.
"""
try:
tree = ast.parse(code)
transformer = AutoNameTransformer()
new_tree = transformer.visit(tree)
ast.fix_missing_locations(new_tree)
return ast.unparse(new_tree)
except SyntaxError:
# If parsing fails, return original code and let execution handle the error
return code
def _make_exec_env(tracer: Tracer) -> Dict[str, Any]:
"""
Execution environment for user-authored Python snippets.
Provides direct access to TinyTorch classes and tracer utilities.
"""
import builtins
# Helper to allow users to manually box things
def manual_box(label, tensors, scheme="1", parent=None):
if not isinstance(tensors, (list, tuple)):
tensors = [tensors]
tracer.box(label=label, tensors=tensors, scheme=str(scheme), parent_box=parent)
# Auto-naming helper that gets injected into transformed code
def auto_name(name: str, value: Any) -> Any:
"""Automatically names Tensor values when they're assigned to variables."""
if isinstance(value, Tensor):
tracer.name(value, name)
return value
# Start with a clean slate but include essential builtins
env = {}
# Manually add critical builtins
env['__builtins__'] = builtins.__dict__
env['__build_class__'] = builtins.__build_class__
env['__name__'] = '__main__'
env['__doc__'] = None
# Add common builtins
for name in ['print', 'len', 'range', 'int', 'float', 'str', 'list', 'dict', 'tuple',
'set', 'bool', 'type', 'isinstance', 'issubclass', 'super', 'object',
'Exception', 'ValueError', 'TypeError', 'AttributeError', 'KeyError',
'zip', 'enumerate', 'map', 'filter', 'sorted', 'reversed', 'abs',
'min', 'max', 'sum', 'round', 'pow', 'divmod', 'hash', 'id']:
env[name] = getattr(builtins, name)
# Add modules
env['math'] = __import__('math')
env['np'] = np
env['numpy'] = np
# Add TinyTorch components
tiny_torch = {
"Tensor": Tensor,
"Linear": Linear,
"Dropout": Dropout,
"Sequential": Sequential,
"Layer": Layer,
"ReLU": ReLU,
"Sigmoid": Sigmoid,
"Tanh": Tanh,
"GELU": GELU,
"Softmax": Softmax,
"LogSoftmax": LogSoftmax,
"MSELoss": MSELoss,
"CrossEntropyLoss": CrossEntropyLoss,
"log_softmax": log_softmax,
"RMSNorm": RMSNorm,
"Function": Function,
"enable_autograd": enable_autograd,
"Optimizer": Optimizer,
"SGD": SGD,
"Adam": Adam,
"AdamW": AdamW,
"Tokenizer": Tokenizer,
"CharTokenizer": CharTokenizer,
"BPETokenizer": BPETokenizer,
"create_tokenizer": create_tokenizer,
"tokenize_dataset": tokenize_dataset,
"CosineSchedule": CosineSchedule,
"clip_grad_norm": clip_grad_norm,
"Trainer": Trainer,
"Embedding": Embedding,
"PositionalEncoding": PositionalEncoding,
"EmbeddingLayer": EmbeddingLayer,
"create_sinusoidal_embeddings": create_sinusoidal_embeddings,
"tracer": tracer,
"box": manual_box,
"__auto_name__": auto_name,
}
env.update(tiny_torch)
return env
class PrintCapture(io.StringIO):
"""Captures print output and sends it to the tracer."""
def __init__(self, tracer: Tracer):
super().__init__()
self.tracer = tracer
def write(self, text: str) -> int:
# Send non-empty text to tracer
if text and text.strip():
self.tracer.print(text.rstrip('\n'))
return len(text)
def flush(self):
pass
def _run_user_code(code: str, tracer: Tracer) -> None:
# TEMPORARY: Skip transformation to debug
transformed_code = transform_code(code) # Instead of transform_code(code)
# 2. Setup Environment
env = _make_exec_env(tracer)
# 3. Redirect stdout to capture print statements
old_stdout = sys.stdout
sys.stdout = PrintCapture(tracer)
# 4. Instrument Tensor/Layer classes to talk to our tracer
with Instrumentor(tracer):
try:
# 5. Execute transformed code
exec(transformed_code, env)
except Exception:
tracer.error(traceback.format_exc())
finally:
sys.stdout = old_stdout
tracer.done()
async def _stream_queue_to_ws(ws: WebSocket, q: "queue.Queue[dict | None]") -> None:
while True:
item = await asyncio.to_thread(q.get)
if item is None:
return
await ws.send_json(item)
@app.websocket("/ws")
async def ws_endpoint(ws: WebSocket):
await ws.accept()
try:
while True:
msg = await ws.receive_json()
if not isinstance(msg, dict):
continue
action = msg.get("action")
if action != "run":
await ws.send_json({"event": "error", "message": "Unsupported action"})
continue
code = msg.get("code", "")
q: "queue.Queue[dict | None]" = queue.Queue()
tracer = Tracer(QueueSink(q))
# Reset frontend state
await ws.send_json({"event": "reset"})
sender = asyncio.create_task(_stream_queue_to_ws(ws, q))
# Run code in thread to avoid blocking async loop
await asyncio.to_thread(_run_user_code, code, tracer)
q.put(None) # Signal end of stream
await sender
except WebSocketDisconnect:
return
# Entry point for running the app
if __name__ == "__main__":
import uvicorn
# Support for Hugging Face Spaces (uses port 7860) and local development
# HF Spaces sets SPACE_ID environment variable
is_hf_space = os.environ.get("SPACE_ID") is not None
# Get host and port from environment variables, with sensible defaults
host = os.environ.get("HOST", "0.0.0.0" if is_hf_space else "127.0.0.1")
port = int(os.environ.get("PORT", "7860" if is_hf_space else "8000"))
print(f"Starting TinyTorch Visualizer on http://{host}:{port}")
if is_hf_space:
print("Running in Hugging Face Spaces mode")
else:
print("Running in local development mode")
print(f"Open http://localhost:{port} in your browser")
uvicorn.run(app, host=host, port=port)