Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Settings page: GPUs, ports, privacy, storage

Part of the developer notes; the index is in CLAUDE.md.

The Settings page (last in the sidebar; pages/Settings.svelte, CHO-5) groups what isn’t about starting a model, so the Server page only has the model, its context window and the pre-flight list. Every change is saved to settings.json at once.

  • Server: the GPUs (below; locked while a backend runs) and Web search for clients, moved here from the Server page.
  • Several GPUs in one machine (CHO-24; experimental: written and unit-tested on a machine with one card, never run on one with several):
    • Setting: settings.gpus is the list of GPU indices the server may use, null for every GPU in the machine (the default on a new install). A settings.json from before this, which names one card in gpu_index, keeps that one card (Settings::from_json), so an update doesn’t move a working setup. gpu_index is still written (the first selected card) for older versions.
    • Choosing: Settings shows one checkbox per card when there are several (index, name, total and free memory; at least one stays ticked; ticking them all saves null, so a card added later is used too). set_gpus; headless, oppxs gpus lists them and oppxs gpus all / oppxs gpus 0,2 chooses. Refused while a backend runs.
    • A model that fits one card runs on one card: of the selected ones, the one with the most free memory (server::best_gpu). That is the single-GPU path everything else here was tested on, so a machine with several cards changes nothing for a model that fits one, except that the fullest card is no longer picked by index.
    • A model that doesn’t fit one card is spread over the selected ones (server::local_split): until now that was “Won’t fit”. The layers are shared out by the cluster split’s planner (split::plan_gpus, one stage per card in index order, by free memory), and the container gets --gpus "device=0,2", SGLANG_PP_LAYER_PARTITION and --pp-size N with the two settings every rank must share (--chunked-prefill-size, --max-mamba-cache-size). No --nnodes: read in SGLang 0.5.21 (engine._calculate_rank_ranges), with one node every pipeline rank runs on this machine, rank r on the r-th visible card; its parser accepts these flags (prepare_server_args in the image: pp_size 2, nnodes 1). --mem-fraction-static is one value for all ranks, the smallest any stage allows.
    • What shows it: the pre-flight VRAM row turns amber with “Doesn’t fit one GPU, so it is spread over GPU 0, GPU 1 (experimental, not yet tried on real hardware): GPU 0: 40 layers (…) · GPU 1: 24 layers (…)”, or says why it can’t be. The Models page counts the selected cards together (the pool donut, titled “This machine · N GPUs (experimental)”), each card paying its own runtime memory (vram::split_across), and the context slider’s maximum follows (server::max_context_over in oppxs).
    • With a cluster: a member offers and uses only its selected cards (its report lists those; its split worker takes the one with the most free memory). A host that splits across the cluster gives one card, as before; spreading one cluster stage over a machine’s cards is not done.
    • Not done: tensor parallel (--tp-size, faster on identical cards); Force load over several cards (it forces the best single card); per-card load and temperature in the charts for the cards in use; and any run at all on real hardware. A first run should check that both ranks come up, the KV cache size SGLang reports, and a long prompt.
    • The single-card start still works on this code: the lifecycle example on the 3090 (Qwen2.5-Coder-14B at 32k: image rebuilt, ready in 226 s, pair, token checks, chat, revoke). A cluster split start, where the member now picks its card through the selection, hasn’t been run.
    • Tested: core unit tests (settings: all by default, a chosen list, the old file’s one card, an empty list refused; server: the card with the most free memory, nothing spread when the model fits one, the plan over two cards and its fraction, the reason when it doesn’t fit, the longest context over several; docker: the container arguments), and oppxs gpus on the 3090 with scratch settings (the list, and the refusal while the backend runs).
  • Privacy: Hide IP addresses (settings.hide_ips): addresses show with their first five digits hidden (192.168.1.74 → xxx.xx8.1.74; the dashed form 100-125-91-43 too; IPv6 is left alone, since its hex looks like the fingerprints beside it). It works on the screen, not on the data (lib/privacy.ts): a MutationObserver masks text and title tooltips as they appear or change, so server labels, Rust-built messages, log lines, chart labels and the pairing command are all covered without each component knowing. Copy buttons read the data and copy the real address; anything inside data-real is left as is. Turning it off puts the real text back. Checked in a harness page: on, off, and text Svelte updates after masking; versions (0.6.1), dates, model names and fingerprints are untouched.
  • Ports: each port the server uses, what it’s for, who reaches it (laptops, this machine only, other servers), when it’s open, and who listens on it now (ports::overview, from ss -H -ltnup: “this app”, “Docker (a container’s published port)”, another process by name and pid, or “not visible” for another user’s, which is the backend’s own process on a split model’s host network), refreshed every 10 s. Every row has a Change button (set_port, edited in place), greyed out with the reason as its tooltip when the port can’t move:
    • Movable (ports::Movable, all used on this machine only or by clients): the client API (settings.agent_port, default 9090: published by Docker and passed as AGENT_PORT, shown in the endpoint and pairing command, read by oppx pair self from the container; laptops pair again with the new address), the admin API (settings.admin_port, 9091: published on loopback and passed as ADMIN_PORT; the app’s AdminClient takes the port from the running container’s environment, ContainerInfo.admin_port, so a backend adopted after a restart or started by hand is reached where it really listens), and the oppxs control API (settings.control_port, 9094: read by oppxs run and its commands at start). Client and admin are locked while a backend is starting or running; control is refused while something holds it (a running oppxs).
    • Checked (ports::port_problem): from 1024 up, not a fixed port (9092, 9093, 9096, 9098, 9100–9116), and not where another movable port already is.
    • Fixed: the cluster API (9092) and discovery (9093/udp): every server broadcasts to 9093 and announces its cluster address with 9092, so moving either on one machine would hide it from the others; it would need a cluster-wide setting. The split model’s ports (9096, 9098, 9100–9116) stay fixed for now; the host sends the rendezvous address in its orders, so they could become host-movable later.
  • Storage: where settings, downloaded models, the Hugging Face cache, backend state and cluster state live, with a copy button.
  • Appearance: theme System / Light / Dark (settings.theme; the sidebar’s switch saves there too).
  • Settings stick: settings.json (~/.config/openphalanx/) is never touched by an update or by closing the app, and only a change on this page (or an oppxs command) writes it. Loading is per field (Settings::from_json): a value that doesn’t read (a wrong type, written by hand or by another version) falls back alone instead of resetting every setting, which the old all-or-nothing parse did on the next save; a file that isn’t JSON is copied to settings.json.unreadable before anything overwrites it; keys from a newer version are kept and written back (extra). The theme moved from the webview’s localStorage into settings.json; localStorage only paints the first frame before the backend answers.
  • Headless: oppxs port (the three movable ports), oppxs port <n> / port admin <n> / port control <n>, oppxs ports, oppxs web-search on|off, with the same rules (tried on scratch settings).
  • Tested: core unit tests for the port overview, the port checks (fixed ports, one movable port onto another, a moved control port being the one watched) and the per-field loading; the page in a Vite harness with stubbed Tauri calls (light, dark, 460 px, server running: client and admin greyed, a clash refused, a move saved, masking on and off, the theme saved). Not yet seen in the running app, and a backend hasn’t been started on moved ports yet.