Multi-agent workflows have changed the shape of local inference. A lead agent decomposes a task and spawns subagents. What looked like one user request becomes dozens of independent model calls. Pointed at a single local engine, those calls compete for the same execution slots. The queue grows while a workstation, laptop, or DGX Spark on the same network sits idle.
NVIDIA Personal AI Router (PAIR) targets exactly that bottleneck. Announced this week, PAIR is a virtual inference router. It discovers compatible machines on a home network and schedules independent inference requests across them. It is not a new inference engine. Ollama or LM Studio still executes the model on whichever node PAIR selects.
Is it deployable? Yes. PAIR ships today as a public beta (v0.1.1) with signed installers for Windows, macOS, and Linux, and the full source is on GitHub under Apache 2.0. It runs entirely on the local network, with internet needed only to download models.
No new API
The design decision that matters most is that PAIR introduces no cluster API. It proxies the Ollama-compatible and LM Studio-compatible interfaces agents already speak, taking over the default port each engine uses. If a harness listens elsewhere, the proxy port is configurable in PAIR’s engine settings. The repository also exposes OpenAI-compatible proxy endpoints.
The consequence: existing agent harnesses need no changes. The agent decides what work to request. PAIR decides where it runs.
Discovery, pairing, and transport
PAIR uses mDNS to find nearby systems automatically. A node can be added by IP address when discovery fails. Trust is established by a six-digit PIN shown on the inviting machine and entered on the invited one. All node-to-node communication is blocked until that pairing completes. Traffic between paired nodes is then secured with mTLS using generated certificates.
Each node runs Ollama or LM Studio. PAIR can install an engine and start model downloads on paired systems, removing most cross-machine setup work.
How the scheduler picks a node
A node becomes eligible for a request only when the required engine is enabled and the exact requested model is present. Models do not need to be identical across the cluster, different systems can hold different models, and PAIR routes according to model location. Loading the same tag on more nodes simply widens the eligible pool.
For each request the scheduler weighs five signals. Is the node online and ready. Is a supported engine enabled. Is the exact model present. What is the current node and engine job load. What is existing GPU utilization.
This is workload-level concurrency, and the boundary is explicit. PAIR assigns each request to one eligible node, where it stays for its lifetime. It does not pool VRAM, merge GPUs into one larger accelerator, or shard a single request across machines.
function eligible(i){
if(!mode && i!==0) return false;
return st[i].online && st[i].model;
}
function log(txt,cls){
var d=document.createElement(‘div’);
if(cls) d.className=cls;
d.innerHTML=txt; elLog.appendChild(d); elLog.scrollTop=elLog.scrollHeight; report();
}
function pick(){
var c=[];
for(var i=0;i<3;i++) if(eligible(i)) c.push(i);
if(!c.length) return -1;
c.sort(function(a,b){
var sa=st[a].busy*40+st[a].util, sb=st[b].busy*40+st[b].util;
return sa-sb;
});
return c[0];
}
function fly(i,cb){
if(reduce||window.innerWidth<641){ cb(); return; }
var nd=document.getElementById(‘nd’+i),sr=elStage.getBoundingClientRect(),nr=nd.getBoundingClientRect();
var p=document.createElement(‘div’); p.className=”pkt”;
var x0=100,y0=60,x1=nr.left-sr.left+nr.width/2,y1=nr.top-sr.top+16;
p.style.left=x0+’px’; p.style.top=y0+’px’; elStage.appendChild(p);
var s=null;
function step(ts){
if(!s) s=ts; var k=Math.min(1,(ts-s)/520);
p.style.left=(x0+(x1-x0)*k)+’px’; p.style.top=(y0+(y1-y0)*k)+’px’;
if(k<1) requestAnimationFrame(step); else { p.remove(); cb(); }
}
requestAnimationFrame(step);
}
function dispatch(n){
if(n>5) return;
elRouter.classList.add(‘hot’);
setTimeout(function(){ elRouter.classList.remove(‘hot’); },260);
var i=pick();
if(i<0){ log(‘Request ‘+n+’ → no eligible node. Every candidate is offline or missing the model.’,’no’);
var ch0=elPend.children; if(ch0[n-1]) ch0[n-1].classList.remove(‘q’);
resolved++; if(resolved===5) finish();
setTimeout(function(){dispatch(n+1);},420); return; }
var chips=elPend.children; if(chips[n-1]) chips[n-1].classList.remove(‘q’);
log(‘Request ‘+n+’ → ‘+NODES[i].nm+’‘+(st[i].busy>0?’ (queued behind ‘+st[i].busy+’)’:”),’ok’);
fly(i,function(){
st[i].busy++; st[i].jobs++; used[i]=1; st[i].util=Math.min(97,st[i].util+30); paint();
var dur=(1500*NODES[i].speed)+(st[i].busy-1)*1500;
setTimeout(function(){
st[i].busy–; st[i].util=Math.max(NODES[i].base,st[i].util-30); done++;
document.getElementById(‘sD’).textContent=done+’/5′; paint();
resolved++; if(resolved===5) finish();
},dur);
});
setTimeout(function(){dispatch(n+1);}, mode?360:200);
}
function finish(){
running=false; clearInterval(timer); elRun.disabled=false;
var el=((Date.now()-t0)/1000).toFixed(1);
log(done+’ of 5 requests completed in ‘+el+’s across ‘+Object.keys(used).length+’ node(s).’,’hdln’);
report();
}
function start(){
if(running) return;
running=true; elRun.disabled=true; done=0; resolved=0; used={};
document.getElementById(‘sD’).textContent=”0/5″;
elLog.innerHTML=”; elPend.innerHTML=”;
for(var k=0;k<5;k++){ var c=document.createElement(‘span’); c.className=”chip q”; elPend.appendChild(c); }
log(mode?’PAIR on. Scheduler filters on readiness, engine, exact model, job load and GPU utilization.’
:’PAIR off. Every request targets the one local engine and queues there.’,’hdln’);
t0=Date.now();
timer=setInterval(function(){
document.getElementById(‘sT’).textContent=((Date.now()-t0)/1000).toFixed(1)+’s’;
},100);
dispatch(1);
}
function reset(){
if(running) return;
done=0; used={}; elLog.innerHTML=”; elPend.innerHTML=”;
document.getElementById(‘sD’).textContent=”0/5″;
document.getElementById(‘sT’).textContent=”0.0s”;
build();
}
document.getElementById(‘run’).addEventListener(‘click’,start);
document.getElementById(‘reset’).addEventListener(‘click’,reset);
document.getElementById(‘mOn’).addEventListener(‘click’,function(){
if(running) return; mode=true;
this.setAttribute(‘aria-pressed’,’true’);
document.getElementById(‘mOff’).setAttribute(‘aria-pressed’,’false’); paint();
});
document.getElementById(‘mOff’).addEventListener(‘click’,function(){
if(running) return; mode=false;
this.setAttribute(‘aria-pressed’,’true’);
document.getElementById(‘mOn’).setAttribute(‘aria-pressed’,’false’); paint();
});
function report(){
try{
var h=document.body.offsetHeight+40;
parent.postMessage({pairHeight:h},’*’);
}catch(e){}
}
build();
log(‘Ready. Send five subagent requests to see placement.’,’hdln’);
window.addEventListener(‘resize’,report);
setTimeout(report,120); setTimeout(report,600);
})();


