mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-14 19:58:44 -04:00
docs: fix distributed-mode diagram - workers coordinate via NATS, not PostgreSQL The architecture diagram drew the worker-bound arrows from the PostgreSQL area of the control plane, implying workers connect to PostgreSQL. They do not: PostgreSQL is the frontends shared state, while workers coordinate over NATS (backend.install events) and receive LoadModel over gRPC from a frontend. Re-route the worker arrows to originate from the NATS chip. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
171 lines
8.5 KiB
HTML
171 lines
8.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,700;12..96,800&family=Archivo:wght@500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
--paper:#F3E8D2; --paper2:#ECDFC2; --ink:#211C14; --ink-soft:#5A5142;
|
|
--rust:#B43A2C; --rust-deep:#8F2C20; --cold:#3F6E73; --hi:#E7D6AE; --dim:#A99F88;
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
html,body{width:1600px;height:900px}
|
|
body{
|
|
background:var(--paper);color:var(--ink);font-family:"Archivo",sans-serif;
|
|
position:relative;overflow:hidden;
|
|
background-image:
|
|
linear-gradient(var(--paper2) 1px,transparent 1px),
|
|
linear-gradient(90deg,var(--paper2) 1px,transparent 1px);
|
|
background-size:40px 40px;
|
|
}
|
|
.frame{position:absolute;inset:26px;border:3px solid var(--ink);}
|
|
.wrap{position:absolute;inset:26px;padding:30px 56px 26px;display:flex;flex-direction:column}
|
|
header{display:flex;align-items:flex-end;justify-content:space-between;gap:30px}
|
|
.eyebrow{font-weight:700;letter-spacing:.22em;text-transform:uppercase;font-size:17px;color:var(--rust-deep)}
|
|
.eyebrow b{color:var(--ink)}
|
|
h1{font-family:"Bricolage Grotesque",sans-serif;font-weight:800;font-size:50px;line-height:.98;letter-spacing:-.015em;margin-top:6px}
|
|
h1 em{font-style:normal;color:var(--rust)}
|
|
.stamp{border:3px solid var(--ink);padding:10px 16px 8px;transform:rotate(3deg);text-align:center;background:var(--paper);box-shadow:6px 6px 0 var(--ink);flex:none}
|
|
.stamp .k{font-family:"Bricolage Grotesque";font-weight:800;font-size:21px;letter-spacing:.04em;line-height:1.05}
|
|
.stamp .s{font-weight:700;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-soft);margin-top:5px}
|
|
.stage{flex:1;margin-top:8px}
|
|
svg{width:100%;height:100%;overflow:visible}
|
|
footer{display:flex;align-items:center;justify-content:space-between;margin-top:6px;gap:24px}
|
|
.note{font-weight:600;font-size:18px;color:var(--ink-soft);line-height:1.3;max-width:1080px}
|
|
.note b{color:var(--ink)}
|
|
.url{font-family:"Bricolage Grotesque";font-weight:800;font-size:22px;color:var(--rust-deep);letter-spacing:.01em;flex:none}
|
|
.url span{color:var(--ink)}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="frame"></div>
|
|
<div class="wrap">
|
|
<header>
|
|
<div>
|
|
<div class="eyebrow">LocalAI <b>·</b> Distributed Mode</div>
|
|
<h1>One control plane, <em>many workers</em></h1>
|
|
</div>
|
|
<div class="stamp">
|
|
<div class="k">SCALE</div>
|
|
<div class="s">out</div>
|
|
</div>
|
|
</header>
|
|
<div class="stage"><svg viewBox="0 0 1480 560" id="svg"></svg></div>
|
|
<footer>
|
|
<div class="note">Stateless frontends, a shared <b>NATS/Postgres</b> plane, and generic workers running per-model backends.</div>
|
|
<div class="url">localai.io<span>/features/distributed-mode</span></div>
|
|
</footer>
|
|
</div>
|
|
<script>
|
|
const INK="#211C14", PAPER="#F3E8D2", PAPER2="#ECDFC2", HI="#E7D6AE", SOFT="#5A5142", RUST="#B43A2C", RUSTD="#8F2C20", COLD="#3F6E73", DIM="#A99F88";
|
|
function el(t,a,x){const e=document.createElementNS("http://www.w3.org/2000/svg",t);for(const k in a)e.setAttribute(k,a[k]);if(x!=null)e.textContent=x;return e;}
|
|
const svg=document.getElementById("svg");
|
|
function shadowRect(x,y,w,h,fill,stroke,sw,dash){
|
|
svg.appendChild(el("rect",{x:x+7,y:y+7,width:w,height:h,fill:INK}));
|
|
svg.appendChild(el("rect",{x,y,width:w,height:h,fill,stroke:stroke||INK,"stroke-width":sw||3.5,"stroke-dasharray":dash||"none"}));
|
|
}
|
|
function txt(x,y,s,o){o=o||{};svg.appendChild(el("text",{x,y,"font-family":o.f||"Archivo","font-weight":o.w||700,"font-size":o.sz||15,"letter-spacing":o.ls||"0","text-anchor":o.a||"start",fill:o.fill||INK},s));}
|
|
function arrow(x1,y1,x2,y2,color,dash){
|
|
const mx=(x1+x2)/2;
|
|
svg.appendChild(el("path",{d:`M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2-11} ${y2}`,fill:"none",stroke:color,"stroke-width":3.5,"stroke-linecap":"round","stroke-dasharray":dash||"none"}));
|
|
const a=7;
|
|
svg.appendChild(el("path",{d:`M ${x2-11} ${y2} l -${a+4} -${a} M ${x2-11} ${y2} l -${a+4} ${a}`,fill:"none",stroke:color,"stroke-width":3.5,"stroke-linecap":"round"}));
|
|
}
|
|
|
|
// ===================== columns =====================
|
|
// 1) LOAD BALANCER (far left)
|
|
// 2) FRONTENDS (SmartRouter x N)
|
|
// 3) STATE PLANE (center)
|
|
// 4) WORKERS (x N)
|
|
|
|
// ---------- LOAD BALANCER ----------
|
|
txt(20,30,"INGRESS",{w:700,sz:13,ls:".2em",fill:SOFT});
|
|
const LBX=20, LBW=150, LBY=222, LBH=116;
|
|
shadowRect(LBX,LBY,LBW,LBH,COLD,INK,3.5);
|
|
txt(LBX+LBW/2,LBY+50,"Load",{f:"Bricolage Grotesque",w:800,sz:24,a:"middle",fill:PAPER});
|
|
txt(LBX+LBW/2,LBY+78,"balancer",{f:"Bricolage Grotesque",w:800,sz:24,a:"middle",fill:PAPER});
|
|
|
|
// ---------- FRONTENDS ----------
|
|
txt(238,30,"STATELESS FRONTENDS",{w:700,sz:13,ls:".2em",fill:SOFT});
|
|
const FX=238, FW=232, FH=132, fY=[58,214,370];
|
|
fY.forEach((y,i)=>{
|
|
shadowRect(FX,y,FW,FH,PAPER2,INK,3.5);
|
|
txt(FX+18,y+44,"SmartRouter",{f:"Bricolage Grotesque",w:800,sz:24});
|
|
txt(FX+18,y+72,"frontend #"+(i+1),{w:700,sz:15,fill:SOFT});
|
|
txt(FX+18,y+104,"routing · API · UI",{w:600,sz:14,fill:DIM});
|
|
});
|
|
|
|
// ---------- STATE PLANE ----------
|
|
txt(560,30,"SHARED STATE PLANE",{w:700,sz:13,ls:".2em",fill:SOFT});
|
|
const SPX=560, SPW=300, SPY=46, SPH=470;
|
|
shadowRect(SPX,SPY,SPW,SPH,PAPER,INK,4);
|
|
svg.appendChild(el("rect",{x:SPX,y:SPY,width:SPW,height:58,fill:RUST}));
|
|
svg.appendChild(el("line",{x1:SPX,y1:SPY+58,x2:SPX+SPW,y2:SPY+58,stroke:INK,"stroke-width":4}));
|
|
txt(SPX+22,SPY+38,"Control plane",{f:"Bricolage Grotesque",w:800,sz:26,fill:PAPER});
|
|
// chips
|
|
const chips=[
|
|
{n:"PostgreSQL", s:"shared config & state"},
|
|
{n:"NATS", s:"jobs · messaging bus"},
|
|
{n:"S3 (optional)", s:"model & artifact store"},
|
|
];
|
|
const CHX=SPX+24, CHW=SPW-48, CHH=104; let cy=SPY+82;
|
|
chips.forEach(c=>{
|
|
svg.appendChild(el("rect",{x:CHX,y:cy,width:CHW,height:CHH,fill:HI,stroke:INK,"stroke-width":2.5}));
|
|
txt(CHX+18,cy+44,c.n,{f:"Bricolage Grotesque",w:800,sz:24});
|
|
txt(CHX+18,cy+76,c.s,{w:700,sz:14,fill:SOFT});
|
|
cy+=CHH+18;
|
|
});
|
|
|
|
// ---------- WORKERS ----------
|
|
txt(1460,30,"GENERIC WORKERS",{w:700,sz:13,ls:".2em",a:"end",fill:SOFT});
|
|
const WX=950, WW=290, WH=132, wY=[58,214,370];
|
|
const workerChips=[
|
|
["llama.cpp","vLLM","whisper"],
|
|
["llama.cpp","stable-diff"],
|
|
["MLX","vLLM","embeddings"],
|
|
];
|
|
wY.forEach((y,i)=>{
|
|
shadowRect(WX,y,WW,WH,"#EFE0BF",INK,3.5);
|
|
txt(WX+18,y+40,"Worker #"+(i+1),{f:"Bricolage Grotesque",w:800,sz:24});
|
|
txt(WX+WW-18,y+38,"per-model gRPC",{w:700,sz:12,ls:".04em",a:"end",fill:RUSTD});
|
|
// process chips
|
|
const procs=workerChips[i];
|
|
const pw=(WW-36-(procs.length-1)*10)/procs.length, ph=46, px0=WX+18, py=y+WH-ph-16;
|
|
procs.forEach((p,j)=>{
|
|
const px=px0+j*(pw+10);
|
|
svg.appendChild(el("rect",{x:px,y:py,width:pw,height:ph,fill:PAPER,stroke:INK,"stroke-width":2}));
|
|
txt(px+pw/2,py+ph/2-2,p,{f:"Bricolage Grotesque",w:700,sz:13.5,a:"middle"});
|
|
txt(px+pw/2,py+ph/2+14,"gRPC",{w:600,sz:10,ls:".06em",a:"middle",fill:DIM});
|
|
});
|
|
});
|
|
|
|
// ===================== ARROWS =====================
|
|
// LB -> each frontend
|
|
fY.forEach((y)=> arrow(LBX+LBW, LBY+LBH/2, FX, y+FH/2, INK));
|
|
// frontends -> state plane (solid, control). Target the plane left edge near matching height,
|
|
// clamped inside the plane body so arrowheads never land on the title bar or below the box.
|
|
fY.forEach((y)=>{
|
|
const ty=Math.max(SPY+90, Math.min(SPY+SPH-30, y+FH/2));
|
|
arrow(FX+FW, y+FH/2, SPX, ty, INK);
|
|
});
|
|
|
|
// NATS messaging bus -> workers (dashed). Workers coordinate via NATS;
|
|
// PostgreSQL is the frontends' shared state, not something workers connect to.
|
|
const natsY = SPY+82+CHH+18 + CHH/2; // NATS chip center y
|
|
wY.forEach((y)=> arrow(SPX+SPW, natsY, WX, y+WH/2, RUSTD, "2 8"));
|
|
// label the NATS bus arrows
|
|
const labW=140, labH=26, labX=(SPX+SPW+WX)/2-labW/2, labY=natsY-46;
|
|
svg.appendChild(el("rect",{x:labX,y:labY,width:labW,height:labH,fill:PAPER,stroke:RUSTD,"stroke-width":2}));
|
|
txt(labX+labW/2,labY+18,"backend.install",{f:"Bricolage Grotesque",w:700,sz:14,a:"middle",fill:RUSTD});
|
|
|
|
// ---- annotated arrow: frontend -> worker : LoadModel (gRPC) ----
|
|
arrow(FX+FW, fY[2]+FH-24, WX, wY[2]+WH-30, COLD, "4 7");
|
|
const lab2W=176, lab2H=26, lab2X=(FX+FW+WX)/2-lab2W/2 - 40, lab2Y=fY[2]+FH+24;
|
|
svg.appendChild(el("rect",{x:lab2X,y:lab2Y,width:lab2W,height:lab2H,fill:PAPER,stroke:COLD,"stroke-width":2}));
|
|
txt(lab2X+lab2W/2,lab2Y+18,"LoadModel (gRPC)",{f:"Bricolage Grotesque",w:700,sz:14,a:"middle",fill:COLD});
|
|
</script>
|
|
</body>
|
|
</html>
|