Spaces:
Running
Running
| 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() | |
| 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) | |
| 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) | |