|
1 | | -# CLI |
| 1 | +# CLI reference |
2 | 2 |
|
3 | | -This page describes the diffuse command line interface. It covers joining a |
4 | | -mesh, serving a slice of a model, and running an interactive chat. |
| 3 | +Every command Diffuse ships today, with the flags that exist and the behavior you |
| 4 | +should expect. Anything not listed here does not exist yet. |
5 | 5 |
|
6 | | -The examples assume the daemon binary is on your path. Build it from the |
7 | | -repository root with cargo if it is not. |
| 6 | +## Install |
8 | 7 |
|
9 | | -## Overview |
| 8 | +One command, Linux x86_64: |
10 | 9 |
|
11 | | -Diffuse is run as a daemon. The daemon joins the gossip network, optionally |
12 | | -serves a slice of layers, and exposes a local chat. A node needs to know at |
13 | | -least one sentinel to reach the rest of the mesh. |
| 10 | +```bash |
| 11 | +curl -fsSL https://raw.githubusercontent.com/UnlikedOne/diffuse/main/install.sh | bash |
| 12 | +``` |
| 13 | + |
| 14 | +This fetches the `diffuse` binary, sets up the Python worker in `~/.diffuse/worker`, |
| 15 | +and puts everything in place. No account, no API key, no server of your own. |
| 16 | + |
| 17 | +The script is [install.sh](https://github.com/UnlikedOne/diffuse/blob/main/install.sh) |
| 18 | +if you would rather read it before piping it to a shell. |
14 | 19 |
|
15 | | -Ports used by a node are 9440 for gossip and 10440 for compute. The compute port |
16 | | -is always the gossip port plus 1000. Make sure both are reachable if you run a |
17 | | -node with a public IP. |
| 20 | +## How a node is put together |
18 | 21 |
|
19 | | -## Global behavior |
| 22 | +Every command starts a daemon. The daemon joins the gossip network, supervises a |
| 23 | +local Python worker on port 50051 bound to localhost, and speaks to other nodes |
| 24 | +over gRPC. You never start the worker yourself. |
20 | 25 |
|
21 | | -Every invocation starts the daemon, which supervises the local Python worker on |
22 | | -port 50051 bound to localhost. You do not start the worker yourself. The daemon |
23 | | -launches it when a slice needs to be served and talks to it over gRPC. |
| 26 | +Two ports matter. Gossip runs on 9440. Compute runs on 10440, which is always the |
| 27 | +gossip port plus 1000. If you run a node with a public IP, both need to be |
| 28 | +reachable. |
24 | 29 |
|
25 | | -## Joining a mesh |
| 30 | +## diffuse chat |
26 | 31 |
|
27 | | -To join the network through the live sentinel: |
| 32 | +Interactive chat against a model served on the network. |
28 | 33 |
|
| 34 | +```bash |
| 35 | +diffuse chat |
29 | 36 | ``` |
30 | | -diffuse join --sentinel 204.168.151.107 |
| 37 | + |
| 38 | +You pick a model from the ones currently live, and the client builds an encrypted |
| 39 | +route to the nodes serving it. Your machine tokenizes the prompt and runs the |
| 40 | +first layers locally, so the prompt itself is never transmitted. Tokens stream |
| 41 | +back one at a time as they are generated. |
| 42 | + |
| 43 | +To join through a specific sentinel rather than the built-in ones: |
| 44 | + |
| 45 | +```bash |
| 46 | +diffuse chat --bootstrap http://204.168.151.107:9440 |
31 | 47 | ``` |
32 | 48 |
|
33 | | -This connects to the sentinel, exchanges signed gossip, and builds a local view |
34 | | -of which slices are live. Joining alone does not serve any layers. It is enough |
35 | | -to act as a client that runs prompts against models served by others. |
| 49 | +## diffuse query |
36 | 50 |
|
37 | | -## Serving a slice |
| 51 | +A single question, no interactive session: |
38 | 52 |
|
39 | | -To contribute a slice of layers to a model: |
| 53 | +```bash |
| 54 | +diffuse query --prompt "Explain black holes in two sentences." --model <model>/<model> |
| 55 | +``` |
| 56 | + |
| 57 | +With an explicit model and sentinel: |
40 | 58 |
|
| 59 | +```bash |
| 60 | +diffuse query \ |
| 61 | + --prompt "Explain black holes in two sentences." \ |
| 62 | + --model Qwen/Qwen2.5-0.5B-Instruct \ |
| 63 | + --bootstrap http://204.168.151.107:9440 |
41 | 64 | ``` |
42 | | -diffuse serve --model Qwen2.5-7B-Instruct --layers 8:16 --sentinel 204.168.151.107 |
| 65 | + |
| 66 | +Useful for scripting, and for reading the latency instrumentation on a single |
| 67 | +request without the chat UI in the way. |
| 68 | + |
| 69 | +## diffuse models |
| 70 | + |
| 71 | +List the models currently served by the network: |
| 72 | + |
| 73 | +```bash |
| 74 | +diffuse models |
43 | 75 | ``` |
44 | 76 |
|
45 | | -The layers argument is a half open range, so 8:16 serves layers 8 through 15. |
46 | | -The daemon loads only those layers into the worker, announces the slice over |
47 | | -gossip, and starts accepting encrypted hidden states for it. |
| 77 | +Shows each live model, the layer coverage, and how many replicas hold each slice. |
| 78 | +A model is only usable when its slices are fully covered. |
48 | 79 |
|
49 | | -For the model to be usable end to end, every slice must be covered by at least |
50 | | -one live node. If a slice has no server, clients see a dead replica. |
| 80 | +## diffuse host |
51 | 81 |
|
52 | | -Serving from a machine with a public IP works directly. Serving from a machine |
53 | | -behind NAT relies on the relay, which is not yet reliable across independent |
54 | | -networks. See [Architecture](ARCHITECTURE.md) for the current status. |
| 82 | +Contribute your machine. The daemon measures how much of the model your hardware |
| 83 | +can hold, takes the slice the network needs most, loads it, and starts serving. |
55 | 84 |
|
56 | | -## Running a chat |
| 85 | +```bash |
| 86 | +diffuse host --model Qwen/Qwen2.5-0.5B-Instruct |
| 87 | +``` |
57 | 88 |
|
58 | | -To open an interactive chat against a model that is fully served on the mesh: |
| 89 | +Against a specific sentinel: |
59 | 90 |
|
| 91 | +```bash |
| 92 | +diffuse host --model Qwen/Qwen2.5-0.5B-Instruct --bootstrap http://204.168.151.107:9440 |
60 | 93 | ``` |
61 | | -diffuse chat --model Qwen2.5-7B-Instruct --sentinel 204.168.151.107 |
| 94 | + |
| 95 | +The model argument is a Hugging Face model id. The slice assignment is automatic: |
| 96 | +you do not choose a layer range, the daemon picks one based on your available |
| 97 | +memory and where the network is thin. |
| 98 | + |
| 99 | +To keep contributing after you close the terminal: |
| 100 | + |
| 101 | +```bash |
| 102 | +nohup diffuse host --model Qwen/Qwen2.5-0.5B-Instruct > ~/.diffuse/host.log 2>&1 & |
62 | 103 | ``` |
63 | 104 |
|
64 | | -The client runs the first layers of the model locally, so your prompt is never |
65 | | -sent in the clear. Tokens stream back one at a time as they are decoded. |
| 105 | +Follow it with `tail -f ~/.diffuse/host.log`, stop it with `pkill diffuse`. For a |
| 106 | +node that survives a reboot, see `deploy/README.md` in the repository for the |
| 107 | +systemd unit. |
| 108 | + |
| 109 | +## What the encryption actually does |
| 110 | + |
| 111 | +This is the part worth reading carefully, because it is the reason the project |
| 112 | +exists. |
| 113 | + |
| 114 | +Session keys are established with **X25519**, an elliptic-curve Diffie-Hellman |
| 115 | +exchange. Each client generates an ephemeral keypair per session, so two requests |
| 116 | +from the same person are not trivially linkable by a shared key. The public key of |
| 117 | +every node is published in gossip and **signed with Ed25519**, so a client can |
| 118 | +verify it is negotiating with the node it intends to, and forged or tampered peer |
| 119 | +records are rejected. |
| 120 | + |
| 121 | +Traffic is encrypted with **ChaCha20-Poly1305**, an authenticated cipher: a node |
| 122 | +cannot silently alter a hidden state in flight without the tampering being |
| 123 | +detected on decryption. There is no certificate authority anywhere in the design; |
| 124 | +keys are bound to node identities directly. |
| 125 | + |
| 126 | +The encryption is genuinely end to end between your machine and each serving node. |
| 127 | +A node in the middle of the pipeline decrypts only the hidden state addressed to |
| 128 | +it, runs its slice, and re-encrypts the result for the next hop. It never holds a |
| 129 | +key that would let it read another hop's traffic. |
66 | 130 |
|
67 | | -The chat prints latency instrumentation that separates compute time from network |
68 | | -time, so you can see where time is going on your connection and hardware. |
| 131 | +Two honest caveats. This is a hand-rolled protocol built on sound primitives, and |
| 132 | +a hand-rolled protocol has not had the review a standard transport has. And |
| 133 | +encryption protects the wire, not the computation: the node running your slice |
| 134 | +sees a plaintext tensor of activations in its own memory. What that does and does |
| 135 | +not reveal is covered in [Privacy](PRIVACY.md). |
69 | 136 |
|
70 | | -## Running your own sentinel |
| 137 | +## Reaching nodes behind NAT |
71 | 138 |
|
72 | | -A sentinel is a node with a public IP that other nodes use as an entry point. To |
73 | | -run one, start the daemon on a public host and let others point their sentinel |
74 | | -flag at its address. On the live deployment this runs as a systemd service named |
75 | | -diffuse on the Hetzner host. |
| 139 | +A node with a public IP is dialed directly and works today. A node behind NAT has |
| 140 | +no address a client can dial, so Diffuse routes it through a sentinel relay |
| 141 | +instead. |
76 | 142 |
|
77 | | -Make sure ports 9440 and 10440 are open to the internet on the sentinel host, or |
78 | | -nodes will not be able to join or compute through it. |
| 143 | +That relay path works in a single-machine test but is **not yet reliable between |
| 144 | +independent networks**. If a node on one network hosts a model and a client on |
| 145 | +another network tries to reach it, the client currently fails with `all replicas |
| 146 | +dead` or a failure to pre-connect to `0.0.0.0:10440`. Serving from behind NAT |
| 147 | +across networks should be treated as non-functional until this is fixed. |
| 148 | +Consuming from behind NAT works fine. The design and current status are in |
| 149 | +[NAT relay design](nat-relay-design.md). |
79 | 150 |
|
80 | 151 | ## Known rough edges |
81 | 152 |
|
82 | | -The startup banner still prints version 0.1.0 even on a v0.2.0 build. The version |
83 | | -string is hardcoded and does not yet reflect the release tag. |
| 153 | +The startup banner still prints `version 0.1.0` on a v0.2.0 build. The version |
| 154 | +string is hardcoded and does not yet track the release tag. |
84 | 155 |
|
85 | | -Pressing Ctrl+C during generation currently tears down the whole chat rather |
86 | | -than interrupting only the current generation, and can leave the Python worker |
87 | | -process orphaned. Both are known and tracked. |
| 156 | +Pressing `Ctrl+C` during generation tears down the whole chat session instead of |
| 157 | +interrupting only the current generation, and can leave an orphaned Python worker |
| 158 | +process behind. |
88 | 159 |
|
89 | | -Very small models such as a 0.5B model run greedy decoding with no sampling, so |
90 | | -they can loop on repetitive output. Sampling is planned. |
| 160 | +Very small models such as a 0.5B run greedy decoding with no sampling, so they can |
| 161 | +loop on repetitive output. Sampling is planned. This is a property of the model |
| 162 | +and the decoding strategy, not of the network carrying it. |
0 commit comments