-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
4412 lines (3962 loc) · 278 KB
/
Copy pathindex.html
File metadata and controls
4412 lines (3962 loc) · 278 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NVIDIA DGX: At-Home AI Stack — Two-Node Clustered Setup Guide</title>
<meta name="description" content="Two-node clustered self-hosted AI server stack on dual Nvidia DGX Spark (arm64). vLLM TP=2 over Ray, LiteLLM, Hermes Agent, n8n, Open WebUI, Telegram gateway.">
<meta property="og:title" content="NVIDIA DGX: At-Home AI Stack — Two-Node Clustered">
<meta property="og:description" content="Two-node clustered self-hosted AI server stack on dual Nvidia DGX Spark (arm64). vLLM TP=2 over Ray, with separated backend/frontend services.">
<meta property="og:type" content="website">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #ffffff;
--surface: #f8f9fa;
--border: #e5e7eb;
--text: #111827;
--text2: #374151;
--text3: #6b7280;
--accent: #2563eb;
--accent-bg: #eff6ff;
--green: #16a34a;
--green-bg: #f0fdf4;
--amber: #92400e;
--amber-bg: #fffbeb;
--amber-border: #fcd34d;
--red: #dc2626;
--red-bg: #fef2f2;
--mono: 'JetBrains Mono', monospace;
--sans: 'Inter', sans-serif;
}
:root[data-theme="dark"] {
--bg: #0d1117;
--surface: #161b22;
--border: #30363d;
--text: #e6edf3;
--text2: #c9d1d9;
--text3: #8b949e;
--accent: #58a6ff;
--accent-bg: #0d1f3a;
--green: #3fb950;
--green-bg: #0f2417;
--amber: #f0b429;
--amber-bg: #2a1f08;
--amber-border: #6b4f10;
--red: #f85149;
--red-bg: #2a0f10;
}
html { scroll-behavior: smooth; overflow-x: hidden; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--sans);
font-size: 14px;
line-height: 1.7;
overflow-x: hidden;
}
.shell {
display: grid;
grid-template-columns: 220px 1fr;
min-height: 100vh;
width: 100%;
}
aside {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
border-right: 1px solid var(--border);
padding: 32px 0;
}
.sidebar-logo {
position: relative;
padding: 0 20px 24px;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
}
.sidebar-logo .wordmark { font-size: 14px; font-weight: 600; color: var(--text); line-height: 1.4; }
.sidebar-logo .sub { font-size: 11px; color: var(--text3); margin-top: 2px; }
.theme-toggle {
position: absolute;
right: 16px;
bottom: 16px;
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text2);
cursor: pointer;
padding: 0;
font-size: 13px;
line-height: 1;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.theme-toggle:hover { background: var(--surface); color: var(--text); }
.theme-toggle .icon-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .icon-sun { display: inline; }
:root[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
.nav-section { padding: 0 20px; margin-bottom: 20px; }
.nav-label {
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text3);
margin-bottom: 6px;
font-weight: 500;
}
.nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 8px;
margin: 1px 0;
border-radius: 5px;
text-decoration: none;
color: var(--text3);
font-size: 12.5px;
transition: all 0.12s;
}
.nav-item:hover, .nav-item.active { color: var(--accent); background: var(--accent-bg); }
.nav-item .num { font-size: 10px; min-width: 18px; color: var(--text3); font-family: var(--mono); }
main { padding: 48px 64px 100px; min-width: 0; }
.hero { margin-bottom: 48px; padding-bottom: 40px; border-bottom: 1px solid var(--border); }
h1 { font-size: 30px; font-weight: 600; color: var(--text); letter-spacing: -0.5px; margin-bottom: 12px; line-height: 1.2; }
.hero-desc { font-size: 14px; color: var(--text2); line-height: 1.75; margin-bottom: 20px; }
.stack-pills { display: flex; flex-wrap: wrap; gap: 6px; }
.pill {
font-size: 11.5px;
padding: 3px 10px;
border-radius: 4px;
border: 1px solid var(--border);
color: var(--text3);
background: var(--surface);
font-family: var(--mono);
}
.pill.blue { color: var(--accent); border-color: #bfdbfe; background: var(--accent-bg); }
.pill.green { color: var(--green); border-color: #bbf7d0; background: var(--green-bg); }
.diagram-section { margin-bottom: 48px; padding-bottom: 40px; border-bottom: 1px solid var(--border); }
.diagram-section h2 { margin-bottom: 20px; }
.diagram-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 24px;
overflow-x: auto;
}
.diagram-wrap svg { display: block; max-width: 900px; height: auto; margin: 0 auto; }
.assumptions {
background: var(--surface);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 6px;
padding: 20px 24px;
margin-bottom: 48px;
}
.assumptions h3 { font-size: 11px; font-weight: 600; color: var(--accent); letter-spacing: 0.06em; text-transform: uppercase; margin-bottom: 12px; }
.assumptions ul { list-style: none; display: flex; flex-direction: column; gap: 5px; }
.assumptions li { position: relative; padding-left: 18px; font-size: 13px; color: var(--text2); }
.assumptions li::before { content: '—'; color: var(--text3); position: absolute; left: 0; top: 0; }
.step { margin-bottom: 56px; padding-bottom: 56px; border-bottom: 1px solid var(--border); }
.step:last-of-type { border-bottom: none; }
.step-header { display: flex; align-items: center; gap: 14px; margin-bottom: 20px; }
.step-num {
font-family: var(--mono);
font-size: 10px;
font-weight: 500;
color: var(--text3);
background: var(--surface);
border: 1px solid var(--border);
padding: 3px 8px;
border-radius: 4px;
white-space: nowrap;
}
h2 { font-size: 20px; font-weight: 600; color: var(--text); letter-spacing: -0.3px; }
h3 { font-size: 13px; font-weight: 600; color: var(--text2); margin: 24px 0 10px; }
p { font-size: 13.5px; color: var(--text2); line-height: 1.75; margin-bottom: 14px; }
a { color: var(--accent); }
.code-block { position: relative; margin: 12px 0 20px; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }
.code-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 14px;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.code-lang { font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text3); }
.copy-btn {
font-family: var(--mono);
font-size: 10px;
color: var(--text3);
background: none;
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
transition: all 0.12s;
}
.copy-btn:hover { color: var(--accent); border-color: #bfdbfe; }
.copy-btn.copied { color: var(--green); border-color: #bbf7d0; }
pre { padding: 16px; overflow-x: auto; background: var(--surface); margin: 0; }
pre code { font-family: var(--mono); font-size: 12.5px; line-height: 1.65; color: var(--text); white-space: pre; }
p code, li code, td code { font-family: var(--mono); font-size: 11.5px; color: var(--accent); background: var(--accent-bg); padding: 1px 5px; border-radius: 3px; }
.callout { border-radius: 6px; padding: 12px 16px; margin: 12px 0 20px; display: flex; gap: 10px; font-size: 13px; line-height: 1.65; }
.callout-icon { flex-shrink: 0; }
.callout.note { background: var(--accent-bg); border: 1px solid #bfdbfe; color: var(--text2); }
.callout.warning { background: var(--amber-bg); border: 1px solid var(--amber-border); color: var(--text2); }
.table-wrap { overflow-x: auto; margin: 12px 0 20px; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; padding: 8px 14px; background: var(--surface); color: var(--text3); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; border-bottom: 1px solid var(--border); font-weight: 500; }
td { padding: 9px 14px; border-bottom: 1px solid var(--border); color: var(--text2); vertical-align: top; }
tr:last-child td { border-bottom: none; }
.verify { position: relative; padding-left: 20px; font-size: 12.5px; color: var(--text3); margin: 10px 0 6px; font-family: var(--mono); overflow-wrap: break-word; word-break: break-word; }
.verify::before { content: '✓'; color: var(--green); position: absolute; left: 0; top: 0; }
.tool-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 12px 0 20px; }
.tool-group { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 14px 16px; }
.tool-group h4 { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 8px; font-weight: 600; }
.tool-group.enable h4 { color: var(--green); }
.tool-group.disable h4 { color: var(--red); }
.tool-group ul { list-style: none; display: flex; flex-direction: column; gap: 4px; }
.tool-group li { font-size: 12px; color: var(--text2); display: flex; align-items: center; gap: 7px; }
.tool-group.enable li::before { content: '+'; color: var(--green); font-weight: 600; font-family: var(--mono); }
.tool-group.disable li::before { content: '−'; color: var(--red); font-family: var(--mono); }
.port-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin: 12px 0 20px; }
.port-card { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 14px; }
.port-num { font-family: var(--mono); font-size: 18px; font-weight: 600; color: var(--accent); margin-bottom: 4px; }
.port-name { font-size: 12px; color: var(--text); margin-bottom: 3px; font-weight: 500; }
.port-note { font-size: 11px; color: var(--text3); }
.file-tree { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 16px; font-family: var(--mono); font-size: 12.5px; line-height: 1.9; margin: 12px 0 20px; color: var(--text2); }
.file-tree .dir { color: var(--accent); }
.file-tree .comment { color: var(--text3); }
.issue { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--amber-border); border-radius: 6px; padding: 14px 16px; margin-bottom: 12px; }
.issue-title { font-size: 13px; font-weight: 600; color: var(--amber); margin-bottom: 6px; }
.issue-sym, .issue-fix { font-size: 12.5px; color: var(--text2); display: flex; gap: 8px; margin-bottom: 3px; }
.issue-sym span:first-child { color: var(--text3); min-width: 56px; flex-shrink: 0; font-weight: 500; }
.issue-fix span:first-child { color: var(--green); min-width: 56px; flex-shrink: 0; font-weight: 500; }
footer { border-top: 1px solid var(--border); padding: 24px 64px; display: flex; align-items: center; justify-content: space-between; color: var(--text3); font-size: 11.5px; }
/* Architecture selector cards */
.arch-picker { display: flex; gap: 14px; margin: 0 0 28px; flex-wrap: wrap; }
details.arch-card {
flex: 1;
min-width: 220px;
border: 2px solid var(--border);
border-radius: 8px;
overflow: hidden;
transition: border-color 0.15s;
}
details.arch-card[open] { border-color: var(--accent); }
details.arch-card.dual[open] { border-color: #7c3aed; }
details.arch-card summary {
list-style: none;
cursor: pointer;
padding: 14px 16px;
background: var(--surface);
user-select: none;
display: flex;
flex-direction: column;
gap: 4px;
}
details.arch-card summary::-webkit-details-marker { display: none; }
details.arch-card summary .arch-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
details.arch-card summary .arch-badge {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 2px 7px;
border-radius: 3px;
background: var(--green-bg);
color: var(--green);
border: 1px solid #bbf7d0;
}
details.arch-card.dual summary .arch-badge {
background: #f5f3ff;
color: #6d28d9;
border-color: #c4b5fd;
}
details.arch-card summary .arch-desc {
font-size: 12px;
color: var(--text3);
}
details.arch-card summary .arch-best {
font-size: 11.5px;
color: var(--text3);
margin-top: 2px;
}
details.arch-card summary .arch-best strong { color: var(--text2); }
details.arch-card summary .arch-warn { font-size: 11px; color: var(--amber); margin-top: 2px; }
details.arch-card .arch-body {
padding: 16px;
border-top: 1px solid var(--border);
background: var(--bg);
}
.mem-table { width: 100%; border-collapse: collapse; font-size: 12.5px; margin: 12px 0 20px; }
.mem-table th { background: var(--surface); padding: 7px 12px; text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text3); border-bottom: 1px solid var(--border); font-weight: 500; }
.mem-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); color: var(--text2); }
.mem-table td code { font-family: var(--mono); font-size: 11px; }
.mem-table tr:last-child td { border-bottom: none; }
.validation-checklist { list-style: none; display: flex; flex-direction: column; gap: 10px; margin: 12px 0 20px; }
.validation-checklist li { display: flex; align-items: flex-start; gap: 10px; font-size: 13px; color: var(--text2); line-height: 1.6; }
.validation-checklist li::before { content: '☐'; font-size: 15px; color: var(--text3); flex-shrink: 0; line-height: 1.5; }
/* Network worksheet */
.worksheet { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 24px; margin: 0 0 24px; }
.worksheet-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin: 16px 0 8px; }
.worksheet-col { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 16px; }
.worksheet-col.you { border-left: 3px solid #3b82f6; }
.worksheet-col.client { border-left: 3px solid #7c3aed; }
.worksheet-col h4 { font-size: 11px; font-weight: 600; color: var(--text3); letter-spacing: 0.06em; text-transform: uppercase; margin: 0 0 10px; }
.worksheet-col.you h4 { color: #1d4ed8; }
.worksheet-col.client h4 { color: #5b21b6; }
.worksheet-row { display: grid; grid-template-columns: 130px 1fr; gap: 8px; align-items: center; margin-bottom: 6px; }
.worksheet-row label { font-size: 11.5px; color: var(--text3); font-family: var(--mono); }
.worksheet-row input {
font-family: var(--mono); font-size: 12.5px; padding: 5px 8px;
border: 1px solid var(--border); border-radius: 4px; background: var(--bg); color: var(--text);
width: 100%; outline: none; transition: border-color 0.12s;
}
.worksheet-row input:focus { border-color: var(--accent); }
.worksheet-row input::placeholder { color: var(--text3); opacity: 0.7; font-size: 11.5px; }
.worksheet-shared { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 14px 16px; margin-top: 14px; }
.worksheet-shared .worksheet-row { grid-template-columns: 180px 1fr; }
.worksheet-actions { display: flex; gap: 10px; margin-top: 16px; }
.ws-btn {
font-family: var(--mono); font-size: 11.5px; padding: 6px 12px; border-radius: 4px;
border: 1px solid var(--border); background: var(--bg); color: var(--text2);
cursor: pointer; transition: all 0.12s;
}
.ws-btn:hover { color: var(--accent); border-color: #bfdbfe; }
.ws-btn.reset:hover { color: var(--red); border-color: #fecaca; }
.ws-status { font-size: 11px; color: var(--text3); margin-left: auto; align-self: center; }
.ws-status.saved { color: var(--green); }
/* Highlight live-substituted placeholders so users can see what's been filled in */
pre code .placeholder-filled {
background: #fef3c7; color: #92400e; padding: 0 2px; border-radius: 2px; font-weight: 500;
}
pre code .placeholder-unfilled {
background: #fee2e2; color: #991b1b; padding: 0 2px; border-radius: 2px;
}
/* TL;DR side-by-side scripts */
.tldr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 14px 0 8px; }
.tldr-col { display: flex; flex-direction: column; min-width: 0; }
.tldr-col-head { display: flex; align-items: center; justify-content: space-between;
padding: 8px 12px; border: 1px solid var(--border); border-bottom: none;
border-radius: 6px 6px 0 0; background: var(--bg); }
.tldr-col-head.you { border-left: 3px solid #3b82f6; }
.tldr-col-head.client { border-left: 3px solid #7c3aed; }
.tldr-col-title { font-size: 12px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
.tldr-col-head.you .tldr-col-title { color: #1d4ed8; }
.tldr-col-head.client .tldr-col-title { color: #5b21b6; }
.tldr-col .code-block { border-radius: 0 0 6px 6px; max-height: 520px; overflow: auto; }
.tldr-col pre { margin: 0; }
pre code .placeholder-secret {
background: #ede9fe; color: #5b21b6; padding: 0 2px; border-radius: 2px; font-weight: 500;
}
.opt-row { display: flex; align-items: center; gap: 8px; font-size: 13px; padding: 5px 0; cursor: pointer; user-select: none; }
.opt-row input[type="checkbox"] { margin: 0; cursor: pointer; }
.opt-row span.opt-port { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text3); margin-left: 4px; }
.opt-row input:not(:checked) ~ span { color: var(--text3); text-decoration: line-through; text-decoration-color: var(--text3); }
/* ── Dark-mode overrides ── */
[data-theme="dark"] .callout.note { border-color: #1e3a5f; }
[data-theme="dark"] details.arch-card.dual summary .arch-badge { background: #1a1030; color: #a78bfa; border-color: #4c1d95; }
[data-theme="dark"] .worksheet-col.you h4 { color: #60a5fa; }
[data-theme="dark"] .worksheet-col.client h4 { color: #a78bfa; }
[data-theme="dark"] .tldr-col-head.you .tldr-col-title { color: #60a5fa; }
[data-theme="dark"] .tldr-col-head.client .tldr-col-title { color: #a78bfa; }
[data-theme="dark"] pre code .placeholder-filled { background: #3b2a05; color: #fbbf24; }
[data-theme="dark"] pre code .placeholder-unfilled { background: #2d0f10; color: #f87171; }
[data-theme="dark"] pre code .placeholder-secret { background: #1a1030; color: #a78bfa; }
/* Inline SVG presentation-attribute overrides (specificity beats attr defaults) */
/* White/near-white rect fills → dark bg */
[data-theme="dark"] svg rect[fill="#ffffff"],
[data-theme="dark"] svg rect[fill="#fff"] { fill: var(--bg); stroke: var(--border); }
/* Light-blue tint (spark-01 header pills, worksheet node box) */
[data-theme="dark"] svg rect[fill="#eff6ff"] { fill: #0d1f3a; stroke: #3b82f6; }
/* Light-purple tint (spark-02 header pill, worksheet client box) */
[data-theme="dark"] svg rect[fill="#f5f3ff"] { fill: #12082a; stroke: #7c3aed; }
/* Gray pill backgrounds (localhost:8000 etc.) */
[data-theme="dark"] svg rect[fill="#f8f9fa"] { fill: var(--surface); stroke: var(--border); }
/* Amber DAC label pill */
[data-theme="dark"] svg rect[fill="#fff7ed"] { fill: #1c1005; stroke: #ea580c; }
[data-theme="dark"] svg rect[fill="#fffaf0"] { fill: #1c1005; stroke: var(--amber-border); }
/* SVG inline text fill overrides */
[data-theme="dark"] svg text[fill="#6b7280"] { fill: var(--text3); }
[data-theme="dark"] svg text[fill="#1d4ed8"] { fill: #60a5fa; }
[data-theme="dark"] svg text[fill="#5b21b6"] { fill: #a78bfa; }
[data-theme="dark"] svg text[fill="#0f766e"] { fill: #2dd4bf; }
[data-theme="dark"] svg text[fill="#9a3412"] { fill: #fb923c; }
[data-theme="dark"] svg text[fill="#16a34a"] { fill: var(--green); }
[data-theme="dark"] svg line[stroke="#ea580c"] { stroke: #fb923c; }
@media (max-width: 860px) {
.tldr-grid { grid-template-columns: 1fr; }
}
@media (max-width: 860px) {
.worksheet-grid { grid-template-columns: 1fr; }
.shell { grid-template-columns: 1fr; }
aside { display: none; }
main { padding: 32px 20px 80px; }
h1 { font-size: 24px; }
.port-grid { grid-template-columns: repeat(2, 1fr); }
.tool-grid { grid-template-columns: 1fr; }
footer { padding: 20px; flex-direction: column; gap: 6px; }
.arch-picker { flex-direction: column; }
}
</style>
</head>
<body>
<div class="shell">
<aside>
<div class="sidebar-logo">
<div class="wordmark">NVIDIA DGX<br>At-Home AI Stack</div>
<div class="sub">Split-Trust Shared Compute · arm64 · v3.2</div>
<button type="button" class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode" title="Toggle dark mode">
<span class="icon-moon">☾</span><span class="icon-sun">☀</span>
</button>
</div>
<nav class="nav-section">
<div class="nav-label">Overview</div>
<a href="#diagram" class="nav-item"><span class="num">—</span> Architecture</a>
<a href="#trust-model" class="nav-item"><span class="num">—</span> Trust model</a>
<a href="#topology" class="nav-item"><span class="num">—</span> Hardware topology</a>
<a href="#worksheet" class="nav-item"><span class="num">—</span> Network worksheet</a>
<a href="#tldr" class="nav-item"><span class="num">—</span> TL;DR — one-shot scripts</a>
<a href="#assumptions" class="nav-item"><span class="num">—</span> Prerequisites</a>
</nav>
<nav class="nav-section">
<div class="nav-label">Steps</div>
<a href="#step1" class="nav-item"><span class="num">01</span> vLLM clustered (TP=2)</a>
<a href="#step2" class="nav-item"><span class="num">02</span> Your LiteLLM (spark-01)</a>
<a href="#step3" class="nav-item"><span class="num">03</span> Client LiteLLM (spark-02)</a>
<a href="#step4" class="nav-item"><span class="num">04</span> Your Open WebUI (spark-01)</a>
<a href="#step5" class="nav-item"><span class="num">05</span> Client Open WebUI (spark-02)</a>
<a href="#step6" class="nav-item"><span class="num">06</span> Tailscale (both nodes)</a>
<a href="#step7" class="nav-item"><span class="num">07</span> Your Hermes (spark-01)</a>
<a href="#step8" class="nav-item"><span class="num">08</span> Your n8n (spark-01)</a>
<a href="#validation-checklist" class="nav-item"><span class="num">—</span> Validation checklist</a>
</nav>
<nav class="nav-section">
<div class="nav-label">Reference</div>
<a href="#ports" class="nav-item"><span class="num">—</span> Ports</a>
<a href="#files" class="nav-item"><span class="num">—</span> File locations</a>
<a href="#issues" class="nav-item"><span class="num">—</span> Cluster issues</a>
<a href="#known-issues" class="nav-item"><span class="num">—</span> Other known issues</a>
</nav>
<nav class="nav-section">
<div class="nav-label">MCP Servers</div>
<a href="#obsidian-vault-sync" class="nav-item"><span class="num">—</span> Obsidian — architecture</a>
<a href="#obsidian-vault-setup" class="nav-item"><span class="num">—</span> Obsidian — LXC setup</a>
<a href="#obsidian-vault-bugs" class="nav-item"><span class="num">—</span> Obsidian — bugs & fixes</a>
<a href="#brave-search-mcp" class="nav-item"><span class="num">—</span> Brave Search — setup</a>
<a href="#playwright-mcp" class="nav-item"><span class="num">—</span> Playwright — browser automation</a>
<a href="#home-assistant-mcp" class="nav-item"><span class="num">—</span> Home Assistant MCP</a>
</nav>
<nav class="nav-section">
<div class="nav-label">Appendix</div>
<a href="#appendix-ha" class="nav-item"><span class="num">—</span> Clustered Open WebUI / n8n</a>
<a href="#litellm-ui" class="nav-item"><span class="num">—</span> LiteLLM Admin UI</a>
<a href="#changelog" class="nav-item"><span class="num">—</span> Changelog</a>
</nav>
</aside>
<main>
<div class="hero">
<h1>NVIDIA DGX<br>At-Home AI Stack <span style="font-size:18px;color:var(--text3);font-weight:500;letter-spacing:0;">— split-trust shared compute</span></h1>
<p class="hero-desc">Two clustered Nvidia DGX Spark nodes (arm64, Ubuntu 24.04) sharing a 256 GB unified memory pool through tensor parallelism (TP=2) over Ray on a 200 Gb/s direct-attach copper interconnect — but with <strong>split ownership</strong>. <code>spark-01</code> is <strong>your</strong> node (your LiteLLM, your Open WebUI, your Hermes Agent, your n8n, your Tailscale). <code>spark-02</code> is the <strong>client's</strong> node (their LiteLLM, their Open WebUI, their n8n, their Tailscale). Both LiteLLM proxies talk to the shared vLLM endpoint at <code>spark-01:8000</code> over the DAC; neither application stack sees the other. Read the <a href="#trust-model">Trust model</a> section before deploying — this architecture has specific properties at the API layer that you should understand explicitly.</p>
<div class="stack-pills">
<span class="pill blue">2× DGX Spark</span>
<span class="pill blue">vLLM TP=2 · Ray</span>
<span class="pill blue">Qwen3.5-122B-A10B-FP8</span>
<span class="pill">LiteLLM × 2</span>
<span class="pill">Open WebUI × 2</span>
<span class="pill">n8n × 2</span>
<span class="pill">Hermes Agent</span>
<span class="pill green">Tailscale × 2 (separate tailnets)</span>
<span class="pill green">DAC interconnect</span>
<span class="pill">arm64 native</span>
</div>
</div>
<div class="diagram-section" id="diagram">
<h2>Architecture</h2>
<p style="margin:0 0 16px;font-size:13.5px;color:var(--text2);">Two physically separate DGX Spark nodes share a single tensor-parallel vLLM cluster (TP=2 over Ray on a 200 Gb/s DAC link) — but each node runs its own independent application stack owned by a different party. The diagram shows three logical layers: <strong>application stacks</strong> (top, separate per owner), <strong>LiteLLM proxies</strong> (middle, one per side, separate keys and logs), and the <strong>shared compute pool</strong> (bottom, TP=2 across both nodes, served by the vLLM head on <code>spark-01:8000</code>). Tailscale sits as a separate overlay on each node — the DAC link is its own private hardware and does not traverse Tailscale.</p>
<div class="diagram-wrap">
<svg width="100%" viewBox="0 0 1100 870" role="img" xmlns="http://www.w3.org/2000/svg" aria-labelledby="arch-desc">
<desc id="arch-desc">Two-layer split-trust architecture. Top: two independent application stacks. Left = spark-01 (your node) with Your Open WebUI, Your n8n, Your Hermes, and Your LiteLLM, on your Tailscale tailnet. Right = spark-02 (client node) with Client Open WebUI, Client n8n, and Client LiteLLM, on a separate client Tailscale tailnet. Both LiteLLM proxies feed into a single shared compute pool below: vLLM head (Ray master, TP rank 0) on spark-01:8000, vLLM Ray worker (TP rank 1) on spark-02, the Qwen model in 256 GB unified memory, connected via the DAC link (200 Gb/s, enp1s0f0np0, 198.51.100.0/30). Your LiteLLM calls vLLM over localhost:8000; client's LiteLLM calls it over the DAC at 198.51.100.1:8000. NCCL collectives between head and worker flow on the DAC. Tailscale overlays carry application traffic only; the DAC is private physical hardware not routed through either tailnet.</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
</defs>
<style>
.users-rect { fill:var(--surface); stroke:var(--border); stroke-width:1; }
.app-blue { fill:var(--accent-bg); stroke:#bfdbfe; stroke-width:1; }
.app-teal { fill:var(--green-bg); stroke:#5eead4; stroke-width:1; }
.litellm-rect { fill:var(--accent-bg); stroke:#c4b5fd; stroke-width:1.5; }
.vllm-rect { fill:var(--amber-bg); stroke:var(--amber-border); stroke-width:1.5; }
.arrow-line { stroke:var(--text3); stroke-width:1.3; fill:none; }
.title-text { font-family:'Inter',sans-serif; font-size:13px; font-weight:600; fill:var(--text); }
.sub-text { font-family:'Inter',sans-serif; font-size:11px; fill:var(--text3); }
.mono-text { font-family:'JetBrains Mono',monospace; font-size:11px; fill:var(--text2); }
.your-boundary { fill:var(--bg); stroke:#3b82f6; stroke-width:1.5; stroke-dasharray:8 4; }
.client-boundary { fill:var(--bg); stroke:#7c3aed; stroke-width:1.5; stroke-dasharray:8 4; }
.pool-boundary { fill:var(--amber-bg); stroke:var(--amber-border); stroke-width:1.5; stroke-dasharray:8 4; }
.ts-badge { fill:var(--green-bg); stroke:#5eead4; stroke-width:1; stroke-dasharray:4 3; }
</style>
<!-- ============ Top: users / clients ============ -->
<rect x="80" y="14" width="380" height="44" rx="6" class="users-rect"/>
<text class="title-text" x="270" y="32" text-anchor="middle" dominant-baseline="central">Your users</text>
<text class="sub-text" x="270" y="48" text-anchor="middle" dominant-baseline="central">browser · Telegram · VS Code · over your tailnet</text>
<rect x="640" y="14" width="380" height="44" rx="6" class="users-rect"/>
<text class="title-text" x="830" y="32" text-anchor="middle" dominant-baseline="central">Client's users</text>
<text class="sub-text" x="830" y="48" text-anchor="middle" dominant-baseline="central">browser · over their tailnet</text>
<!-- Users → node (single arrow per side, lands on the boundary header) -->
<line x1="270" y1="58" x2="270" y2="90" class="arrow-line" marker-end="url(#arrow)"/>
<line x1="830" y1="58" x2="830" y2="90" class="arrow-line" marker-end="url(#arrow)"/>
<!-- ============ spark-01 boundary (your node) ============ -->
<rect x="20" y="100" width="520" height="358" rx="10" class="your-boundary"/>
<!-- Header rect overlapping top edge -->
<rect x="32" y="92" width="216" height="22" rx="4" fill="#eff6ff" stroke="#3b82f6" stroke-width="1"/>
<text x="40" y="107" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#1d4ed8">spark-01 · YOUR NODE</text>
<!-- Small white pill behind the IP label so the dashed boundary doesn't slice through the text -->
<rect x="306" y="92" width="222" height="22" rx="4" fill="#ffffff" stroke="#e5e7eb" stroke-width="1"/>
<text x="522" y="107" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#6b7280">192.0.2.21 (mgmt) · 198.51.100.1 (DAC)</text>
<!-- Tailscale label (light fill, doesn't cover anything) -->
<rect x="32" y="124" width="496" height="26" rx="5" class="ts-badge"/>
<text x="280" y="141" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="600" fill="#0f766e" dominant-baseline="central">Your Tailscale tailnet · private overlay · ACLs you control</text>
<!-- Your apps row -->
<rect x="40" y="166" width="156" height="58" rx="6" class="app-blue"/>
<text class="title-text" x="118" y="188" text-anchor="middle" dominant-baseline="central">Your Open WebUI</text>
<text class="sub-text" x="118" y="208" text-anchor="middle" dominant-baseline="central">:8080 · your data</text>
<rect x="206" y="166" width="156" height="58" rx="6" class="app-blue"/>
<text class="title-text" x="284" y="188" text-anchor="middle" dominant-baseline="central">Your n8n</text>
<text class="sub-text" x="284" y="208" text-anchor="middle" dominant-baseline="central">:5678 · your flows</text>
<rect x="372" y="166" width="156" height="58" rx="6" class="app-teal"/>
<text class="title-text" x="450" y="188" text-anchor="middle" dominant-baseline="central">Your Hermes</text>
<text class="sub-text" x="450" y="208" text-anchor="middle" dominant-baseline="central">Telegram · skills · memory</text>
<!-- App → LiteLLM arrows (straight down) -->
<line x1="118" y1="224" x2="118" y2="254" class="arrow-line" marker-end="url(#arrow)"/>
<line x1="284" y1="224" x2="284" y2="254" class="arrow-line" marker-end="url(#arrow)"/>
<line x1="450" y1="224" x2="450" y2="254" class="arrow-line" marker-end="url(#arrow)"/>
<!-- Your LiteLLM -->
<rect x="40" y="254" width="488" height="124" rx="6" class="litellm-rect"/>
<text class="title-text" x="284" y="280" text-anchor="middle" dominant-baseline="central">Your LiteLLM · :8001</text>
<text class="sub-text" x="284" y="304" text-anchor="middle" dominant-baseline="central">your master_key · your SQLite log corpus</text>
<text class="mono-text" x="284" y="324" text-anchor="middle" dominant-baseline="central">api_base = http://localhost:8000/v1</text>
<text class="sub-text" x="284" y="354" text-anchor="middle" dominant-baseline="central" fill="#16a34a">↑ from your apps ↓ to shared vLLM</text>
<!-- ============ spark-02 boundary (client node) ============ -->
<rect x="560" y="100" width="520" height="358" rx="10" class="client-boundary"/>
<!-- Header rect -->
<rect x="572" y="92" width="232" height="22" rx="4" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1"/>
<text x="580" y="107" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#5b21b6">spark-02 · CLIENT NODE</text>
<!-- Small white pill behind the IP label so the dashed boundary doesn't slice through the text -->
<rect x="846" y="92" width="222" height="22" rx="4" fill="#ffffff" stroke="#e5e7eb" stroke-width="1"/>
<text x="1062" y="107" text-anchor="end" font-family="JetBrains Mono,monospace" font-size="10" fill="#6b7280">192.0.2.22 (mgmt) · 198.51.100.2 (DAC)</text>
<!-- Tailscale label -->
<rect x="572" y="124" width="496" height="26" rx="5" class="ts-badge"/>
<text x="820" y="141" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="600" fill="#0f766e" dominant-baseline="central">Client's Tailscale tailnet · separate overlay · ACLs client controls</text>
<!-- Client apps -->
<rect x="580" y="166" width="240" height="58" rx="6" class="app-blue"/>
<text class="title-text" x="700" y="188" text-anchor="middle" dominant-baseline="central">Client Open WebUI</text>
<text class="sub-text" x="700" y="208" text-anchor="middle" dominant-baseline="central">:8080 · client's data</text>
<rect x="830" y="166" width="240" height="58" rx="6" class="app-blue"/>
<text class="title-text" x="950" y="188" text-anchor="middle" dominant-baseline="central">Client n8n</text>
<text class="sub-text" x="950" y="208" text-anchor="middle" dominant-baseline="central">:5678 · client's flows</text>
<!-- Client app → LiteLLM arrows -->
<line x1="700" y1="224" x2="700" y2="254" class="arrow-line" marker-end="url(#arrow)"/>
<line x1="950" y1="224" x2="950" y2="254" class="arrow-line" marker-end="url(#arrow)"/>
<!-- Client LiteLLM -->
<rect x="580" y="254" width="490" height="124" rx="6" class="litellm-rect"/>
<text class="title-text" x="825" y="280" text-anchor="middle" dominant-baseline="central">Client LiteLLM · :8001</text>
<text class="sub-text" x="825" y="304" text-anchor="middle" dominant-baseline="central">client's master_key · client's SQLite log corpus</text>
<text class="mono-text" x="825" y="324" text-anchor="middle" dominant-baseline="central">api_base = http://198.51.100.1:8000/v1 (over DAC)</text>
<text class="sub-text" x="825" y="354" text-anchor="middle" dominant-baseline="central" fill="#16a34a">↑ from client apps ↓ to shared vLLM</text>
<!-- ============ Arrows from each LiteLLM down to the shared pool ============ -->
<!-- Your LiteLLM bottom (y=378) → vLLM head top (y=496), with a label pill in the gap -->
<line x1="284" y1="378" x2="284" y2="496" class="arrow-line" marker-end="url(#arrow)"/>
<rect x="220" y="426" width="128" height="22" rx="4" fill="#ffffff" stroke="#9ca3af"/>
<text x="284" y="437" text-anchor="middle" class="mono-text" dominant-baseline="central">localhost:8000</text>
<!-- Client LiteLLM bottom (y=378) → vLLM head top (y=496), with DAC label pill -->
<line x1="825" y1="378" x2="825" y2="496" class="arrow-line" marker-end="url(#arrow)"/>
<rect x="715" y="426" width="220" height="22" rx="4" fill="#ffffff" stroke="#9ca3af"/>
<text x="825" y="437" text-anchor="middle" class="mono-text" dominant-baseline="central">198.51.100.1:8000 (over DAC)</text>
<!-- ============ Shared compute pool ============ -->
<rect x="20" y="440" width="1060" height="420" rx="10" class="pool-boundary"/>
<rect x="32" y="432" width="270" height="22" rx="4" fill="#fff7ed" stroke="#fdba74" stroke-width="1"/>
<text x="40" y="447" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#9a3412">SHARED COMPUTE POOL · TP=2 over Ray</text>
<!-- Pool intro text -->
<text x="550" y="478" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#9a3412" font-weight="500">single vLLM endpoint at 198.51.100.1:8000 — both LiteLLM proxies talk to it</text>
<!-- vLLM head -->
<rect x="60" y="496" width="980" height="76" rx="6" class="vllm-rect"/>
<text class="title-text" x="550" y="518" text-anchor="middle" dominant-baseline="central">vLLM head (Ray master) · TP rank 0 · spark-01:8000 · :6379 (Ray GCS)</text>
<text class="sub-text" x="550" y="538" text-anchor="middle" dominant-baseline="central">--tensor-parallel-size 2 · --distributed-executor-backend ray</text>
<text class="mono-text" x="550" y="556" text-anchor="middle" dominant-baseline="central">VLLM_HOST_IP=198.51.100.1 · NCCL_IB_HCA=rocep1s0f0,roceP2p1s0f0 · GPU 0</text>
<!-- vLLM worker -->
<rect x="60" y="594" width="980" height="76" rx="6" class="vllm-rect"/>
<text class="title-text" x="550" y="616" text-anchor="middle" dominant-baseline="central">vLLM Ray worker · TP rank 1 · spark-02 (no API listener)</text>
<text class="sub-text" x="550" y="636" text-anchor="middle" dominant-baseline="central">processes tensor activations only — no readable text</text>
<text class="mono-text" x="550" y="654" text-anchor="middle" dominant-baseline="central">VLLM_HOST_IP=198.51.100.2 · NCCL_IB_HCA=rocep1s0f0,roceP2p1s0f0 · GPU 1</text>
<!-- NCCL connector between head and worker (drawn outside boxes, on right) -->
<path d="M 1050 534 C 1080 534, 1080 632, 1050 632" stroke="#ea580c" stroke-width="3" fill="none"/>
<text x="1075" y="579" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#9a3412">NCCL</text>
<text x="1075" y="592" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" font-weight="700" fill="#9a3412">↕</text>
<!-- Model -->
<rect x="60" y="692" width="980" height="56" rx="6" class="vllm-rect" fill="#fffaf0"/>
<text class="title-text" x="550" y="712" text-anchor="middle" dominant-baseline="central">Qwen/Qwen3.5-122B-A10B-FP8 — 256 GB unified memory pool</text>
<text class="sub-text" x="550" y="730" text-anchor="middle" dominant-baseline="central">bootstrap fallback: Qwen/Qwen3.6-35B-A3B-FP8</text>
<!-- DAC link bar -->
<rect x="60" y="768" width="980" height="74" rx="8" fill="#fff7ed" stroke="#ea580c" stroke-width="1.5"/>
<text x="550" y="790" text-anchor="middle" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="#9a3412">DAC · 200 Gb/s direct-attach copper</text>
<text x="550" y="810" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11" fill="#9a3412">enp1s0f0np0 · MTU 9216 · 198.51.100.0/30</text>
<text x="550" y="828" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#9a3412">NCCL allreduce over RoCE/RDMA · Ray control · NOT routed through either tailnet · separate from mgmt LAN</text>
</svg>
</div>
</div>
<!-- ============ Trust Model section ============ -->
<div class="diagram-section" id="trust-model">
<h2>Trust model</h2>
<p style="margin:0 0 14px;font-size:13.5px;color:var(--text2);">Read this before deploying. The split-trust architecture has specific properties at the API layer that you should understand explicitly. None of this is a new risk introduced by the cluster — it is just the same trust profile you accept any time you use a hosted inference API, made visible.</p>
<h3>API-layer visibility — spark-01 sees all prompts</h3>
<p>The vLLM head process runs on <code>spark-01</code> and serves the OpenAI-compatible API on port 8000. <strong>Both</strong> LiteLLM proxies — yours and the client's — call this endpoint. That means the owner of <code>spark-01</code> can, in principle, observe every raw prompt and every model output that crosses the API surface. This is structurally identical to the trust profile of any commercial hosted-inference provider (OpenAI, Anthropic, Together, etc.): the entity running the API server can see traffic at the API layer.</p>
<h3>Tensor-layer isolation — spark-02 sees only floats</h3>
<p>The Ray worker on <code>spark-02</code> processes <em>tensor activations</em>, not text. It receives intermediate floating-point tensors over NCCL allreduce on the DAC link and contributes its share of the matrix multiplications. The client's node never sees readable prompts or completions; it only sees the mathematical operations its TP rank is responsible for. NCCL traffic on the DAC carries floats, not strings.</p>
<h3>Application-layer isolation — fully separate stacks</h3>
<p>Knowledge bases, chat history, RAG pipelines, vector indexes, API keys, request logs, and OAuth tokens are completely separate on each node. Your Open WebUI's database is on <code>spark-01</code>; the client's is on <code>spark-02</code>. Your LiteLLM master key is yours; the client's is the client's. Neither party has access to the other's application stack — there is no cross-mounted volume, no shared Postgres, no shared file system. The only thing that crosses the boundary is the inference call from the client's LiteLLM into <code>spark-01:8000</code>.</p>
<h3>Network isolation — separate tailnets, private DAC</h3>
<p>Each node joins its owner's Tailscale tailnet independently. ACLs on each tailnet are controlled by that owner. The DAC link (<code>198.51.100.0/30</code>) is private physical hardware between the two nodes — it is not routed through either Tailscale network and is not advertised on either tailnet. Tailscale carries application traffic only (clients reaching their own UIs); compute traffic stays on the DAC.</p>
<h3>When this architecture is appropriate</h3>
<ul style="font-size:13.5px;color:var(--text2);line-height:1.75;padding-left:20px;display:flex;flex-direction:column;gap:4px;">
<li>Both parties have a working relationship and have agreed to this arrangement.</li>
<li>The data being sent for inference is <em>not</em> regulated (HIPAA / GDPR / SOC2 / PCI / etc.).</li>
<li>Both parties accept a trust model equivalent to using any commercial hosted-inference API.</li>
</ul>
<h3>When additional agreements are required</h3>
<ul style="font-size:13.5px;color:var(--text2);line-height:1.75;padding-left:20px;display:flex;flex-direction:column;gap:4px;">
<li>Either party handles regulated data — HIPAA, GDPR, SOC2, PCI-DSS, attorney-client privileged, or similar — in which case a written data processing agreement (DPA / BAA / equivalent) and audit controls are needed before traffic flows.</li>
<li>Either party has contractual data handling requirements imposed by their own customers or regulators.</li>
<li>The relationship is not pre-existing and the trust profile of "any hosted inference API" is not acceptable.</li>
</ul>
<div class="callout warning">
<span class="callout-icon">⚠</span>
<div>vLLM on <code>spark-01:8000</code> has no authentication. Tailscale ACLs and host firewall rules are what prevent the client from bypassing their LiteLLM and hitting the unauthenticated endpoint directly. See Step 06 (Tailscale) for the ACL configuration that enforces this.</div>
</div>
</div>
<!-- ============ Hardware topology block ============ -->
<div class="diagram-section" id="topology">
<h2>Hardware topology</h2>
<div class="table-wrap">
<table>
<thead><tr><th>Node</th><th>Owner</th><th>Mgmt IP</th><th>DAC IP</th><th>Services</th></tr></thead>
<tbody>
<tr>
<td><code>spark-01</code></td>
<td>You (private)</td>
<td><code>192.0.2.21</code></td>
<td><code>198.51.100.1</code></td>
<td>vLLM head (Ray master, TP rank 0), Your LiteLLM, Your Open WebUI, Your Hermes Agent, Your n8n, Your Tailscale</td>
</tr>
<tr>
<td><code>spark-02</code></td>
<td>Client (separate ownership)</td>
<td><code>192.0.2.22</code></td>
<td><code>198.51.100.2</code></td>
<td>vLLM Ray worker (TP rank 1), Client LiteLLM, Client Open WebUI, Client n8n, Client Tailscale</td>
</tr>
</tbody>
</table>
</div>
<h3>Interconnects</h3>
<ul style="font-size:13.5px;color:var(--text2);line-height:1.75;padding-left:20px;display:flex;flex-direction:column;gap:4px;">
<li><strong>DAC interconnect</strong> — <code>enp1s0f0np0</code>, MTU 9216, point-to-point <code>198.51.100.0/30</code>. Carries NCCL for tensor-parallel collectives, Ray control, and the client LiteLLM's inference calls into <code>spark-01:8000</code>. Not routed through either Tailscale network.</li>
<li><strong>Mgmt interconnect</strong> — <code>192.0.2.0/24</code> over RJ45, default routes. Used for SSH and node-bootstrap traffic during setup.</li>
<li><strong>Tailscale (each owner)</strong> — each node independently joins its owner's tailnet. Application traffic (browser → Open WebUI, Telegram → Hermes, etc.) traverses Tailscale. The DAC link is never advertised onto either tailnet.</li>
<li><strong>SSH</strong> — passwordless both directions between <code>spark-01</code> and <code>spark-02</code> at the mgmt IPs (required for the rsync step in Step 01). After setup, this can be locked down or removed.</li>
</ul>
<h3>Architecture principles</h3>
<ol style="font-size:13.5px;color:var(--text2);line-height:1.75;padding-left:20px;display:flex;flex-direction:column;gap:8px;">
<li><strong>Shared compute, split application.</strong> The vLLM cluster is the <em>only</em> shared resource. Application stacks above it (LiteLLM, Open WebUI, Hermes, n8n, Tailscale) are duplicated and independently owned.</li>
<li><strong>Both LiteLLMs hit the same vLLM endpoint.</strong> Your LiteLLM uses <code>http://localhost:8000/v1</code>; client's LiteLLM uses <code>http://198.51.100.1:8000/v1</code> over the DAC. Neither proxy goes through the other's stack.</li>
<li><strong>Separate keys, separate logs, separate data.</strong> Each LiteLLM has its own master key; each Open WebUI has its own knowledge bases and chat history. Nothing is shared at the application layer.</li>
<li><strong>Tailscale is per-owner.</strong> Two separate tailnets, two separate ACL policies. Cross-tailnet traffic only happens if both owners explicitly configure it (which by default they do not).</li>
<li><strong>Single-instance per side.</strong> No clustered Open WebUI / clustered n8n on either node. HA modes are documented in the <a href="#appendix-ha">appendix</a> only.</li>
</ol>
</div>
<!-- ============ Network worksheet ============ -->
<div class="diagram-section" id="worksheet">
<h2>Network worksheet</h2>
<p style="margin:0 0 12px;font-size:13.5px;color:var(--text2);">Fill these in once. Every code block on this page that contains a matching placeholder (<code>YOUR_NODE1_MGMT_IP</code>, <code>YOUR_USERNAME</code>, etc.) will be live-substituted with the value you type — and a yellow highlight shows you what was filled in. Values are saved to your browser's <code>localStorage</code> so reloads keep them. Master keys, API keys, and other secrets are deliberately <em>not</em> in this worksheet — fill those into the relevant code blocks manually so they never touch <code>localStorage</code>.</p>
<div class="diagram-wrap" style="margin-bottom:14px;">
<svg width="100%" viewBox="0 0 1080 280" role="img" xmlns="http://www.w3.org/2000/svg">
<title>Network worksheet — your IP slots</title>
<desc>Two boxes representing spark-01 (your node) and spark-02 (client node) with three IP slots each: mgmt, DAC, and Tailscale. The DAC link is shown between the two nodes. The mgmt LAN and the two tailnets are shown as separate networks each node attaches to.</desc>
<defs>
<marker id="ws-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</marker>
</defs>
<!-- spark-01 box -->
<rect x="40" y="50" width="420" height="180" rx="10" fill="#eff6ff" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="8 4"/>
<text x="60" y="76" font-family="Inter,sans-serif" font-size="12.5" font-weight="700" fill="#1d4ed8">spark-01 — YOUR NODE</text>
<rect x="60" y="92" width="380" height="34" rx="5" fill="#fff" stroke="#bfdbfe"/>
<text x="74" y="106" font-family="Inter,sans-serif" font-size="10.5" fill="#6b7280">mgmt IP</text>
<text x="74" y="120" font-family="JetBrains Mono,monospace" font-size="12" fill="#1d4ed8" id="ws-svg-n1-mgmt">YOUR_NODE1_MGMT_IP</text>
<rect x="60" y="132" width="380" height="34" rx="5" fill="#fff" stroke="#bfdbfe"/>
<text x="74" y="146" font-family="Inter,sans-serif" font-size="10.5" fill="#6b7280">DAC IP</text>
<text x="74" y="160" font-family="JetBrains Mono,monospace" font-size="12" fill="#1d4ed8" id="ws-svg-n1-dac">YOUR_NODE1_DAC_IP</text>
<rect x="60" y="172" width="380" height="34" rx="5" fill="#fff" stroke="#bfdbfe"/>
<text x="74" y="186" font-family="Inter,sans-serif" font-size="10.5" fill="#6b7280">Tailscale IP</text>
<text x="74" y="200" font-family="JetBrains Mono,monospace" font-size="12" fill="#1d4ed8" id="ws-svg-n1-ts">YOUR_NODE1_TAILSCALE_IP</text>
<!-- DAC link badge -->
<line x1="460" y1="140" x2="620" y2="140" stroke="#ea580c" stroke-width="4" stroke-linecap="round"/>
<rect x="475" y="116" width="130" height="22" rx="5" fill="#fff7ed" stroke="#ea580c" stroke-width="1.2"/>
<text x="540" y="131" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" font-weight="700" fill="#9a3412">DAC · 200 Gb/s</text>
<!-- spark-02 box -->
<rect x="620" y="50" width="420" height="180" rx="10" fill="#f5f3ff" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="8 4"/>
<text x="640" y="76" font-family="Inter,sans-serif" font-size="12.5" font-weight="700" fill="#5b21b6">spark-02 — CLIENT NODE</text>
<rect x="640" y="92" width="380" height="34" rx="5" fill="#fff" stroke="#c4b5fd"/>
<text x="654" y="106" font-family="Inter,sans-serif" font-size="10.5" fill="#6b7280">mgmt IP</text>
<text x="654" y="120" font-family="JetBrains Mono,monospace" font-size="12" fill="#5b21b6" id="ws-svg-n2-mgmt">YOUR_NODE2_MGMT_IP</text>
<rect x="640" y="132" width="380" height="34" rx="5" fill="#fff" stroke="#c4b5fd"/>
<text x="654" y="146" font-family="Inter,sans-serif" font-size="10.5" fill="#6b7280">DAC IP</text>
<text x="654" y="160" font-family="JetBrains Mono,monospace" font-size="12" fill="#5b21b6" id="ws-svg-n2-dac">YOUR_NODE2_DAC_IP</text>
<rect x="640" y="172" width="380" height="34" rx="5" fill="#fff" stroke="#c4b5fd"/>
<text x="654" y="186" font-family="Inter,sans-serif" font-size="10.5" fill="#6b7280">Tailscale IP</text>
<text x="654" y="200" font-family="JetBrains Mono,monospace" font-size="12" fill="#5b21b6" id="ws-svg-n2-ts">YOUR_NODE2_TAILSCALE_IP</text>
<!-- Network labels -->
<text x="250" y="28" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#6b7280">Mgmt LAN · Your tailnet</text>
<line x1="250" y1="34" x2="250" y2="48" stroke="#9ca3af" stroke-width="1.2" marker-end="url(#ws-arrow)"/>
<text x="830" y="28" text-anchor="middle" font-family="Inter,sans-serif" font-size="10" fill="#6b7280">Mgmt LAN · Client's tailnet</text>
<line x1="830" y1="34" x2="830" y2="48" stroke="#9ca3af" stroke-width="1.2" marker-end="url(#ws-arrow)"/>
<!-- Tailnet hostname row -->
<text x="540" y="262" text-anchor="middle" font-family="Inter,sans-serif" font-size="11" fill="#0f766e" font-weight="600">Your tailnet hostname (used in n8n WEBHOOK_URL)</text>
<text x="540" y="276" text-anchor="middle" font-family="JetBrains Mono,monospace" font-size="11.5" fill="#0d9488" id="ws-svg-tailnet">YOUR_TAILNET_HOSTNAME</text>
</svg>
</div>
<div class="worksheet">
<div class="worksheet-grid">
<div class="worksheet-col you">
<h4>spark-01 — your node</h4>
<div class="worksheet-row">
<label for="ws-n1-mgmt">mgmt IP</label>
<input type="text" id="ws-n1-mgmt" data-key="YOUR_NODE1_MGMT_IP" placeholder="e.g. 192.0.2.21" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-n1-dac">DAC IP</label>
<input type="text" id="ws-n1-dac" data-key="YOUR_NODE1_DAC_IP" placeholder="e.g. 198.51.100.1" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-n1-ts">Tailscale IP</label>
<input type="text" id="ws-n1-ts" data-key="YOUR_NODE1_TAILSCALE_IP" placeholder="e.g. 100.64.0.5" autocomplete="off"/>
</div>
</div>
<div class="worksheet-col client">
<h4>spark-02 — client node</h4>
<div class="worksheet-row">
<label for="ws-n2-mgmt">mgmt IP</label>
<input type="text" id="ws-n2-mgmt" data-key="YOUR_NODE2_MGMT_IP" placeholder="e.g. 192.0.2.22" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-n2-dac">DAC IP</label>
<input type="text" id="ws-n2-dac" data-key="YOUR_NODE2_DAC_IP" placeholder="e.g. 198.51.100.2" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-n2-ts">Tailscale IP</label>
<input type="text" id="ws-n2-ts" data-key="YOUR_NODE2_TAILSCALE_IP" placeholder="e.g. 100.64.0.6" autocomplete="off"/>
</div>
</div>
</div>
<div class="worksheet-shared">
<h4 style="font-size:11px;font-weight:600;color:var(--text3);letter-spacing:0.06em;text-transform:uppercase;margin:0 0 10px;">Shared / per-host</h4>
<div class="worksheet-row">
<label for="ws-username">Linux username</label>
<input type="text" id="ws-username" data-key="YOUR_USERNAME" placeholder="e.g. ubuntu" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-tailnet">Tailnet hostname</label>
<input type="text" id="ws-tailnet" data-key="YOUR_TAILNET_HOSTNAME" placeholder="e.g. spark-01.tailnet-1234.ts.net" autocomplete="off"/>
</div>
<div class="worksheet-row" style="margin-top:10px;border-top:1px solid var(--border);padding-top:10px;">
<label for="ws-lxc-ip">Obsidian LXC IP</label>
<input type="text" id="ws-lxc-ip" data-key="YOUR_LXC_IP" placeholder="e.g. 10.10.1.50" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-lxc-hostname">Obsidian LXC hostname</label>
<input type="text" id="ws-lxc-hostname" data-key="YOUR_LXC_HOSTNAME" placeholder="e.g. obsidian-mcp" autocomplete="off"/>
</div>
</div>
<div class="worksheet-actions">
<button class="ws-btn" type="button" onclick="wsCopyAll()">copy summary</button>
<button class="ws-btn reset" type="button" onclick="wsReset()">reset</button>
<span class="ws-status" id="ws-status">Not saved</span>
</div>
<div class="callout note" style="margin-top:14px;">
<span class="callout-icon">ℹ</span>
<div><strong>Secrets stay manual.</strong> <code>YOUR_MASTER_KEY</code>, <code>YOUR_CLIENT_MASTER_KEY</code>, and <code>YOUR_BRAVE_API_KEY</code> are intentionally not in this worksheet — fill those into the relevant code blocks by hand, and don't paste them into a browser-stored field. The worksheet only handles network identifiers and your username.</div>
</div>
</div>
</div>
<div class="diagram-section" id="tldr">
<h2>TL;DR — one-shot setup scripts</h2>
<p style="margin:0 0 12px;font-size:13.5px;color:var(--text2);">Fill the table below, then run the matching script on each node. The scripts bundle every step in this guide — packages, Docker, RoCE/DAC checks, vLLM cluster image, model download + DAC rsync, LiteLLM + Postgres + Prisma, Open WebUI, n8n, Hermes, Tailscale, and host firewall — into a single idempotent run per node. <strong>spark-01 must complete through the image-copy/rsync stage before spark-02 can finish</strong>; the spark-02 script will pause and wait for the model weights to arrive.</p>
<div class="callout warning" style="margin:0 0 14px;">
<span class="callout-icon">⚠</span>
<div><strong>Secrets are session-only.</strong> Values entered below in the <em>Secrets</em> block (HF token, master keys, Tailscale auth keys) are kept in browser <code>sessionStorage</code> — they vanish when you close the tab and are <em>never</em> written to disk. Generate fresh master keys with <code>openssl rand -hex 24</code>.</div>
</div>
<div class="callout note" style="margin:0 0 14px;">
<span class="callout-icon">ℹ</span>
<div><strong>Tailscale auth-key prerequisites.</strong> Before generating each auth key, in <em>that tailnet's</em> admin console:
<ol style="margin:6px 0 0 18px;padding:0;font-size:13px;line-height:1.6;">
<li>Open <strong>Access Controls</strong> and ensure the tag is declared in <code>tagOwners</code> — <code>"tag:owner": ["you@example.com"]</code> for spark-01, <code>"tag:client-ai": ["autogroup:admin"]</code> for spark-02. The tag must exist <em>before</em> any device tries to advertise it.</li>
<li>Open <strong>Settings → Keys → Generate auth key</strong>. Toggle <strong>Tags</strong> on and select the matching tag from the list. Recommended: <em>Reusable: no</em>, <em>Ephemeral: no</em>, <em>Tags: tag:owner</em> (or <code>tag:client-ai</code>).</li>
</ol>
Auth keys without a tag selection will be rejected by <code>--advertise-tags</code> at <code>tailscale up</code> time. Tagged devices have key expiry automatically disabled, so the server won't drop off the tailnet on the 90-day timer.</div>
</div>
<div class="worksheet">
<div class="worksheet-grid">
<div class="worksheet-col you">
<h4>spark-01 — your node · secrets & choices</h4>
<div class="worksheet-row">
<label for="ws-hf-token">HF token</label>
<input type="password" id="ws-hf-token" data-secret-key="YOUR_HF_TOKEN" placeholder="hf_..." autocomplete="off" spellcheck="false"/>
</div>
<div class="worksheet-row">
<label for="ws-master-key">Owner master key</label>
<input type="password" id="ws-master-key" data-secret-key="YOUR_MASTER_KEY" placeholder="sk-owner-..." autocomplete="off" spellcheck="false"/>
</div>
<div class="worksheet-row">
<label for="ws-ts-key1">Tailscale auth key (optional)</label>
<input type="password" id="ws-ts-key1" data-secret-key="YOUR_NODE1_TS_AUTHKEY" placeholder="tskey-auth-..." autocomplete="off" spellcheck="false"/>
</div>
</div>
<div class="worksheet-col client">
<h4>spark-02 — client node · secrets & choices</h4>
<div class="worksheet-row">
<label for="ws-client-master">Client master key</label>
<input type="password" id="ws-client-master" data-secret-key="YOUR_CLIENT_MASTER_KEY" placeholder="sk-client-..." autocomplete="off" spellcheck="false"/>
</div>
<div class="worksheet-row">
<label for="ws-ts-key2">Client Tailscale auth key (optional)</label>
<input type="password" id="ws-ts-key2" data-secret-key="YOUR_NODE2_TS_AUTHKEY" placeholder="tskey-auth-..." autocomplete="off" spellcheck="false"/>
</div>
<div class="worksheet-row">
<label for="ws-client-tailnet">Client tailnet hostname</label>
<input type="text" id="ws-client-tailnet" data-secret-key="CLIENT_TAILNET_HOSTNAME" placeholder="spark-02.tailnet-abcd.ts.net" autocomplete="off"/>
</div>
</div>
</div>
<div class="worksheet-shared">
<h4 style="font-size:11px;font-weight:600;color:var(--text3);letter-spacing:0.06em;text-transform:uppercase;margin:0 0 10px;">Model + recipe (shared)</h4>
<div class="worksheet-grid">
<div class="worksheet-row">
<label for="ws-recipe">vLLM recipe</label>
<input type="text" id="ws-recipe" data-secret-key="MODEL_RECIPE" placeholder="qwen3.5-122b-fp8" value="qwen3.5-122b-fp8" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-served">Served model name</label>
<input type="text" id="ws-served" data-secret-key="SERVED_MODEL_NAME" placeholder="qwen3.5-122b" value="qwen3.5-122b" autocomplete="off"/>
</div>
<div class="worksheet-row">
<label for="ws-model-id">HF model repo</label>
<input type="text" id="ws-model-id" data-secret-key="HF_MODEL_REPO" placeholder="Qwen/Qwen3.5-122B-A10B-FP8" value="Qwen/Qwen3.5-122B-A10B-FP8" autocomplete="off"/>