The browser kept running out of memory before the model finished arriving.
That was useful information.
It meant the browser version could not be a web interface wrapped around a server-shaped pipeline. The pipeline itself had to change. Model shards needed to stream. Tensors needed durable local identities. The compiled graph could not carry a second copy of the weights. Memory admission had to happen before downloading gigabytes of data.
This is the design pressure behind hologram-ai: download, compile, materialize, and run the actual model pipeline in a static browser application.
Not a remote-control panel.
Not a JavaScript imitation of the native engine.
The browser is the machine.
A browser-shaped architecture is different
A server process usually has:
- a large 64-bit address space
- a filesystem with ordinary random access
- threads
- native code generation
- process-level memory controls
- long-lived background services
- room for temporary copies that are ugly but survivable
A static browser application may have:
- a 32-bit WebAssembly address space
- no ambient filesystem
- origin-scoped persistent storage
- worker constraints
- no JIT available to the WebAssembly program itself
- page lifecycle interruptions
- a user who can close the tab at any moment
Moving the same source code into WASM does not remove those differences.
flowchart LR
subgraph Server-shaped pipeline
S1[Download full model] --> S2[Load weights into memory]
S2 --> S3[Compile graph + weights]
S3 --> S4[Run]
end
subgraph Browser-shaped pipeline
B1[Stream one tensor] --> B2[Hash incrementally]
B2 --> B3[Persist by address in OPFS]
B3 --> B4[Compile weightless graph]
B4 --> B5[Materialize only when running]
end
The second pipeline is not merely a more careful implementation of the first. It has different ownership boundaries.
Storage has to exist before memory
The most important browser storage primitive in this design is OPFS: the Origin Private File System.
A download worker can stream safetensors shards tensor by tensor. Each tensor is hashed incrementally and written under a content-derived path such as:
tensors/<address>.bin
The browser does not need to retain the full shard, the full model, and the final store representation in memory at the same time.
sequenceDiagram
participant H as Hugging Face
participant D as Download worker
participant O as OPFS store
participant C as Compiler
H-->>D: streamed shard bytes
loop tensor by tensor
D->>D: parse tensor header
D->>D: hash bytes incrementally
D->>O: persist tensors/address.bin
end
D->>C: tensor manifest + model config
C-->>O: weightless .holo archive
This reverses the usual assumption that persistent storage is an optimization after the in-memory representation exists.
In the browser, persistent content-addressed storage is how the in-memory representation becomes possible.
The graph should not contain the weights twice
A compiled model artifact often bundles structure and parameters together.
That is convenient for distribution. It is expensive when the weights already live in a local content-addressed store.
The browser pipeline compiles a weightless .holo archive. The archive describes the graph and references the required tensors by address. At runtime, materialization resolves those addresses against OPFS and verifies each buffer by re-hashing it.
flowchart TD
M[Model config] --> G[Structural graph]
T[Tensor manifest] --> G
G --> H[Weightless .holo]
H --> R[Runtime materializer]
O[OPFS address store] --> R
R --> V{Re-hash matches address?}
V -- yes --> E[Inference session]
V -- no --> X[Refuse corrupted buffer]
This has several useful consequences:
- the graph can be small relative to the model
- duplicate tensors are stored once
- the same weights can back multiple compiled views
- a corrupted local buffer fails materialization
- the artifact can state its dependencies without embedding them
The archive becomes a structural object rather than a transport container for every byte it may ever use.
Memory admission belongs before download
A browser should not discover that a model is too large after transferring it.
The model’s configuration already contains enough information to estimate the working set:
- parameter count and element widths
- layer count
- hidden dimensions
- vocabulary size
- context length
- activation buffers
- runtime overhead
- materialization strategy
A simplified estimate is:
where is the selected context length.
Admission succeeds only if:
The important word is before.
flowchart TD
C[Fetch config and tensor metadata] --> E[Estimate memory by context]
E --> B{Fits browser budget?}
B -- no --> R[Refuse before weight transfer]
B -- yes --> S[Choose safe context]
S --> D[Begin streamed download]
The product can reduce context length, recommend a smaller model, or explain why the environment is insufficient. It should not leave the user with a half-downloaded model and a tab that vanished.
A resource guard is not a warning banner. It is part of the execution contract.
Workers define the concurrency model
Browser UI code should not hash gigabytes of weights or run token generation on the main thread.
The hologram-ai web application separates responsibility across workers:
- a download worker owns persistent ingestion and compilation
- a generation worker owns materialization and inference
- the main thread owns interaction and presentation
This keeps the interface responsive, but it also creates real distributed-systems concerns inside one origin:
- message schemas
- cancellation
- worker restarts
- partial download recovery
- ownership of OPFS handles
- progress reporting
- error serialization
- version compatibility between the UI and WASM binding
flowchart LR
UI[Main thread UI] -->|commands| D[Download worker]
UI -->|generate| G[Generation worker]
D --> O[OPFS]
G --> O
D -->|progress / result| UI
G -->|streamed tokens| UI
“Client-side” does not mean “single component.” It means the distributed boundary is local to the user’s machine.
The real pipeline should run in both places
One of the easiest ways for a browser product to drift is to create a JavaScript-specific implementation that approximates the native engine.
Then the browser and CLI share branding, not behavior.
The stronger design is to compile the same Rust core to WebAssembly and keep the TypeScript layer as an adapter over the real binding. The browser invokes the same compile, describe, materialize, run, and address operations as the native surface.
That does not guarantee identical performance. It makes semantic drift harder.
flowchart TB
Core[Rust hologram-ai core]
Core --> N[Native CLI / library]
Core --> W[WASM binding]
W --> B[Browser command adapter]
N --> C[Same archive and address contracts]
B --> C
The browser journey can then be tested against the same conceptual contract rather than a mock backend.
A real browser test is part of the architecture
Unit tests can verify the memory estimator. Rust tests can verify archive materialization. Neither proves that Chromium can complete the user journey under the actual worker, storage, and WASM boundaries.
The browser test needs to do the work:
- resolve a model
- stream its tensors
- persist them to OPFS
- compile the graph
- materialize the archive
- run generation
- verify the expected protocol behavior
That is why hologram-ai treats the browser journey as a deployment gate rather than a demo run after release.
The browser is a target platform. Target-platform conformance belongs in CI.
Static hosting is a useful constraint
The web application is deployable as static assets.
That removes an entire category of ambiguity. If inference works, it is not because an unmentioned backend quietly performed the expensive step. The network is used to fetch model artifacts; the computation remains local.
Static hosting also makes limitations visible:
- no server-side secret handling
- no hidden fallback for unsupported models
- no backend session to preserve state after the tab closes
- no server memory to absorb a bad estimate
Those constraints improve the honesty of the architecture.
Local does not automatically mean private
Running the model in the browser reduces the need to send prompts to an inference service. It does not make every surrounding interaction private.
The application may still contact:
- a model registry
- a CDN
- an update service
- telemetry, if someone adds it
- external links or embedded assets
A local-AI product should be specific about which bytes leave the origin and why.
The cleanest version can make a strong claim:
After model retrieval, prompts and generated tokens remain inside the browser origin.
That claim should be tested at the network boundary, not inferred from the presence of WASM.
The 32-bit address space keeps the design honest
WebAssembly memory on the common browser target remains constrained. Large models and large contexts will not fit merely because the desktop has more physical RAM.
That is not a temporary annoyance to hide. It is a design boundary.
It pushes the system toward:
- streaming rather than staging
- deduplication rather than copying
- addressed tensors rather than monolithic bundles
- configurable context rather than a fixed maximum
- up-front admission rather than optimistic allocation
- explicit model support rather than “try it and see”
Those are good properties on native systems too. The browser makes them non-negotiable.
The thin-client model leaves capability on the table
A browser has durable local storage, cryptography, workers, WebAssembly, offline capability, and a mature security boundary around origins.
Treating it only as a terminal wastes those capabilities and preserves a server dependency that may not be necessary.
The better question is not “can this desktop pipeline be squeezed into a tab?”
It is:
What would the pipeline look like if the user’s browser were the primary computer from the beginning?
For hologram-ai, the answer is a content-addressed local store, a weightless graph, worker-owned streaming, guarded admission, and the same runtime contract used natively.
That is a real computer architecture.
It just happens to have a URL bar.