Meta 已开源 Rebalancer,这是一个带有 Python 接口的 C++ 库,用于求解分配问题。它决定在约束和目标下哪些对象进入哪些容器。根据 Engineering at Meta 的博文,Rebalancer 已在 Meta 内部处理资源分配超过 9 年。该版本以 Apache 2.0 许可发布,附带文档、一个 PyPI 包以及一个名为 Rebalancer Explorer 的调试 UI。

它可部署吗?可以。pip install rebalancer 会安装适用于 Python 3.12+ 的 v1.0.4,并提供适用于 Linux x86-64 和 macOS 14+ ARM64 的预构建 wheel。此外还有 .deb、.rpm 和 Homebrew 包。PyPI 仍将该项目的分类标记为 Alpha。

Rebalancer 解决了什么问题?

分配问题遍布 Meta 的整个技术栈。机架分配到数据中心,服务器分配到服务,任务分配到服务器,用户流量分配到数据中心。Meta 指出了 2 个阻碍:可用性和可扩展性。工程师们难以将策略转化为精确的公式,而且许多问题是 NP-hard 的,规模太大,商业求解器无法处理。

Rebalancer 的答案是,将问题的指定方式与求解方式分离。该设计在 OSDI 2024 论文《Optimizing Resource Allocation in Hyperscale Datacenters》中有详细说明。

规范层如何工作

规范语言有 3 层:

建模构造:维度(如 CPU 或存储等属性)、分区(对象的组)、作用域(容器的组)以及利用率。

表达式 API:用 SUM 或 MAX 聚合利用率,或用 SQUARE 等操作对其进行变换。

规范 API:数十个预定义的目标和约束,列在文档中。

Meta 的示例将任务建模为对象,服务器建模为容器,机架建模为作用域。CapacitySpec 限制每台服务器的 CPU 和存储。GroupCountSpec 保持每个机架 1 种作业类型。BalanceSpec 在两个维度上平衡每台服务器的利用率。

一个表达式图,两个求解器

Rebalancer 将规范编译为有向无环表达式图。叶节点保存利用率值;聚合和变换节点位于其上方。用户提供初始分配和停止条件。初始分配已经违反的约束会成为高优先级目标。

最优求解器:将图转换为混合整数规划,用于 FICO Xpress、Gurobi 或 HiGHS。变量聚合和对称性破缺可缩小模型。最坏情况下的模型规模仍为 O(objects × bins)。Meta 最大的问题对任何 MIP 求解器来说都太大了。

局部搜索:该求解器直接在表达式图上工作。它探索将对象移动到其他 bin 的移动,最坏情况下的邻域为 O(objects + bins)。然后应用不违反任何约束的最佳候选方案。评估被并行化,每秒可达数百万次评估,并且搜索空间会被剪枝。

Meta 对几乎所有大型问题使用局部搜索,对中小型问题使用 MIP,通常先用 MIP 做原型。

Meta 的生产数据

每天解决约 4000 万个分配问题,涵盖 30 多种不同的建模形式。

在 26.5 万个对象和 3.2 千个 bin 上,P99 求解时间为 12 秒。

超过 100 万个对象和 5 千个 bin 的问题平均耗时 171 秒,涵盖 3400 多次运行。

Rebalancer 的最佳用例

将分片、任务或容器放置到集群上:在 CPU 和内存上限下将工作分配给服务器,同时将副本分散到不同机架。Meta 的 Shard Manager 和 RAS 运行这种模式。

跨区域平衡流量和工作负载:将用户流量或作业路由到数据中心,在延迟与负载之间权衡。Taiji 为边缘流量执行此操作,Meta 按优先级平衡 ML 训练。

基础设施之外的运营分配:在容量规则下将支持工单映射到工程师、将会议映射到会议室或将工位映射到人员。Meta 这 3 种都做过。

使用 Rebalancer Explorer 进行调试

Meta 的建模人员大部分时间都花在调试求解器行为上。Rebalancer Explorer 是为此构建的 Docker 化 Web UI。它显示绑定约束、松弛效果,以及某个对象为何落在某个 bin 中。

交互式讲解器

运行局部搜索</button>

<button class="b" id="mtpStep">单步执行</button>

<button class="b" id="mtpShuf">随机糟糕起点</button>

<button class="b" id="mtpReset">重置</button>

</div>

<div class="stats">

<div class="st"><div class="k">步数</div><div class="v" id="mtpS">0</div></div>

<div class="st"><div class="k">已评估的移动</div><div class="v" id="mtpE">0</div></div>

<div class="st"><div class="k">容量溢出</div><div class="v" id="mtpV">0</div></div>

<div class="st"><div class="k">平衡目标</div><div class="v" id="mtpO">0</div></div>

</div>

<div class="racks" id="mtpRacks"></div>

<div class="spark"><div class="k">各步骤的目标值(越低越好,理想值 = 324)</div>

<svg id="mtpSpark" viewBox="0 0 800 70" width="100%" height="70" preserveAspectRatio="none"></svg>

</div>

<div class="log" id="mtpLog">服务器 S1 启动时超出 4 个 CPU 容量。按 Run。</div>

</div>

<!-- PANE 2 -->

<div class="pane" id="mtpP1">

<p class="note">Rebalancer 将规格编译成一个<b>有向无环表达式图</b>。叶子节点保存每台服务器的利用率;SQUARE、SUM 和 MAX 节点位于它们之上。当一个任务移动时,只有它触及的叶子节点及其祖先节点需要重新计算值。该图使用与标签页 1 相同的实时状态。</p>

<div class="btns">

<button class="b pri" id="mtpGMove">随机移动一个任务</button>

<button class="b" id="mtpGBest">应用最佳局部搜索移动</button>

</div>

<div class="gwrap"><svg id="mtpGraph" viewBox="0 0 820 300" width="100%"></svg></div>

<div class="log" id="mtpGLog">按一个按钮来移动任务。</div>

</div>

<!-- PANE 3 -->

<div class="pane" id="mtpP2">

<p class="note">MIP 模型大约需要每个对象每个箱子一个二元变量,因此其规模增长为 <b>O(objects × bins)</b>。局部搜索邻域的规模增长为 <b>O(objects + bins)</b>。拖动滑块或加载 Meta 公布的规模。</p>

<div class="btns">

<button class="b" data-o="500" data-bn="20">小型:500 × 20</button>

<button class="b" data-o="265000" data-bn="3200">Meta P99:265k × 3.2k</button>

<button class="b" data-o="1000000" data-bn="5000">Meta XL:1M × 5k</button>

</div>

<div class="sl"><label>对象 <b id="mtpOv"></b></label><input type="range" id="mtpOs" min="1" max="6.3" step="0.01" value="2.7"></div>

<div class="sl"><label>箱子 <b id="mtpBv"></b></label><input type="range" id="mtpBs" min="0.3" max="4" step="0.01" value="1.3"></div>

<div class="cmp">

<div class="row"><div class="t">MIP 二元变量,最坏情况 <span id="mtpMv"></span></div><div class="track"><i id="mtpMb" style="background:linear-gradient(90deg,#FF8A5A,#FF5A6E)"></i></div></div>

<div class="row"><div class="t">局部搜索邻域,最坏情况 <span id="mtpLv"></span></div><div class="track"><i id="mtpLb" style="background:linear-gradient(90deg,#0866FF,#38D6FF)"></i></div></div>

</div>

<div class="verdict" id="mtpVerdict"></div>

<p class="note" style="margin-top:10px">条形图使用对数刻度。结论中的规模区间仅为示意;Meta 声明的规则是:几乎所有大型问题使用局部搜索,中小型问题使用 MIP。</p>

</div>

<!-- PANE 4 -->

<div class="pane" id="mtpP3">

<p class="note">Meta 为 Rebalancer 公布的生产数据(Engineering at Meta,2026 年 9 月 21 日)。</p>

<div class="grid" id="mtpCards">

<div class="card"><div class="n"><span data-c="40">0</span><em>M</em></div><div class="d">每天解决的分配问题数</div></div>

<div class="card"><div class="n"><span data-c="30">0</span><em>+</em></div><div class="d">不同的问题建模</div></div>

<div class="card"><div class="n"><span data-c="12">0</span><em>s</em></div><div class="d">在 265k 个对象和 3.2k 个箱子上的 P99 求解时间</div></div>

<div class="card"><div class="n"><span data-c="171">0</span><em>s</em></div><div class="d">100 万+ 个对象和 5k 个箱子的平均求解时间</div></div>

<div class="card"><div class="n"><span data-c="3.4" data-d="1">0</span><em>k+</em></div><div class="d">在该 100 万+ 对象规模下的运行次数</div></div>

<div class="card"><div class="n"><span data-c="9">0</span><em>+ yrs</em></div><div class="d">在开源前于 Meta 内部使用的时间</div></div>

</div>

<div class="pills">

<span class="pill">Shard Manager:分片 → 服务器</span>

<span class="pill">RAS:服务器 → 服务</span>

<span class="pill">Taiji:边缘流量 → 数据中心</span>

<span class="pill">无服务器函数分组</span>

<span class="pill">ML 训练均衡</span>

<span class="pill">会议 → 会议室</span>

</div>

</div>

<div class="ft">

<span>来源:<a href="https://engineering.fb.com/2026/09/21/open-source/rebalancer-generic-high-performance-library-assignment-problems/" target="_blank" rel="noopener">Engineering at Meta</a> · <a href="https://github.com/facebook/rebalancer" target="_blank" rel="noopener">GitHub</a> · 标签页 1 至 3 为简化模拟</span>

<span class="brand">由 Marktechpost 构建</span>

</div>

</div>

<script>

(function(){

var R=document.getElementById('mtp-rebal');

function postH(){try{parent.postMessage({mtpRebalH:R.offsetHeight+40},'*');}catch(e){}}

var SIZES=[6,5,4,4,3,3,3,2,2,2,1,1], CAP=16, NS=4;

var START=[0,0,0,0,1,1,1,2,2,2,0,1];

var asg=START.slice(), step=0, evals=0, hist=[], timer=null, hot=-1;

function util(a){var u=[0,0,0,0];for(var i=0;i<a.length;i++)u[a[i]]+=SIZES[i];return u;}

function score(a){var u=util(a),v=0,o=0;for(var s=0;s<NS;s++){v+=Math.max(0,u[s]-CAP);o+=u[s]*u[s];}return [v,o];}

function better(x,y){return x[0]<y[0]||(x[0]===y[0]&&x[1]<y[1]);}

function bestMove(){

var cur=score(asg),best=null,bs=cur,n=0;

for(var i=0;i<asg.length;i++){for(var s=0;s<NS;s++){if(s===asg[i])continue;var b=asg.slice();b[i]=s;n++;var sc=score(b);if(better(sc,bs)){bs=sc;best={a:b,txt:'move t'+(i+1)+' S'+(asg[i]+1)+' → S'+(s+1),ids:[i]};}}}

for(var i2=0;i2<asg.length;i2++)for(var j=i2+1;j<asg.length;j++){if(asg[i2]===asg[j])continue;var c=asg.slice();c[i2]=asg[j];c[j]=asg[i2];n++;var sc2=score(c);if(better(sc2,bs)){bs=sc2;best={a:c,txt:'swap t'+(i2+1)+' t'+(j+1),ids:[i2,j]};}}

return {best:best,n:n,sc:bs};

}

var racksEl=document.getElementById('mtpRacks');

function rects(){var m={};racksEl.querySelectorAll('.chip').forEach(function(c){m[c.dataset.id]=c.getBoundingClientRect();});return m;}

function render(ids){

var before=rects(),u=util(asg),h='';

for(var r=0;r<2;r++){h+='<div class="rack"><div class="rl">Rack '+(r?'B':'A')+' (scope)</div><div class="srvs">';

for(var s=r*2;s<r*2+2;s++){var over=u[s]>CAP;

h+='<div class="srv'+(over?' over':'')+'"><div class="sn">S'+(s+1)+'<span>'+u[s]+' / '+CAP+' CPU</span></div><div class="bar"><i style="width:'+Math.min(100,u[s]/24*100)+'%"></i><u style="left:'+(CAP/24*100)+'%"></u></div><div class="chips">';

for(var i=0;i<asg.length;i++)if(asg[i]===s){h+='<div class="chip'+(ids&&ids.indexOf(i)>-1?' hot':'')+'" data-id="'+i+'" style="width:'+(30+SIZES[i]*8)+'px">t'+(i+1)+'·'+SIZES[i]+'</div>';}

h+='</div></div>';}

h+='</div></div>';}

racksEl.innerHTML=h;

racksEl.querySelectorAll('.chip').forEach(function(c){var b=before[c.dataset.id];if(!b)return;var a=c.getBoundingClientRect(),dx=b.left-a.left,dy=b.top-a.top;if(dx||dy){c.style.transition='none';c.style.transform='translate('+dx+'px,'+dy+'px)';requestAnimationFrame(function(){requestAnimationFrame(function(){c.style.transition='transform .55s cubic-bezier(.2,.8,.2,1)';c.style.transform='';});});}});

var sc=score(asg);

document.getElementById('mtpS').textContent=step;

document.getElementById('mtpE').textContent=evals;

var V=document.getElementById('mtpV');V.textContent=sc[0];V.className='v '+(sc[0]?'bad':'good');

var O=document.getElementById('mtpO');O.textContent=sc[1];O.className='v'+(sc[1]===324?' good':'');

spark();drawGraph(null);postH();

}

function spark(){

var s=document.getElementById('mtpSpark'),pts=hist.length?hist:[score(asg)[1]];

var mx=Math.max.apply(null,pts.concat([600])),mn=300,n=Math.max(pts.length-1,12);

var p=pts.map(function(v,i){return (i/n*790+5)+','+(65-(v-mn)/(mx-mn)*58);}).join(' ');

var ideal=65-(324-mn)/(mx-mn)*58;

s.innerHTML='<line x1="0" x2="800" y1="'+ideal+'" y2="'+ideal+'" stroke="#2BD99F" stroke-dasharray="4 4" opacity=".6"/><polyline points="'+p+'" fill="none" stroke="#38D6FF" stroke-width="2.5"/>'+pts.map(function(v,i){return '<circle cx="'+(i/n*790+5)+'" cy="'+(65-(v-mn)/(mx-mn)*58)+'" r="3.5" fill="#0866FF" stroke="#fff" stroke-width="1"/>';}).join('');

}

var logEl=document.getElementById('mtpLog');

function doStep(){

var r=bestMove();evals+=r.n;

if(!r.best){stop();logEl.textContent='Local optimum: no move or swap improves the score ('+evals+' evaluations).';render();return false;}

var old=asg;asg=r.best.a;step++;hist.push(score(asg)[1]);

var moved=r.best.ids;logEl.textContent='Step '+step+': '+r.best.txt+' (checked '+r.n+' candidates)';

render(moved);drawGraph(diff(old,asg));return true;

}

function diff(a,b){var s={};for(var i=0;i<a.length;i++)if(a[i]!==b[i]){s[a[i]]=1;s[b[i]]=1;}return Object.keys(s).map(Number);}

function stop(){if(timer){clearInterval(timer);timer=null;}document.getElementById('mtpRun').textContent=' Run local search';}

document.getElementById('mtpRun').onclick=function(){if(timer){stop();return;}this.textContent='❚❚ Pause';if(!doStep())return;timer=setInterval(function(){if(!doStep())stop();},900);};

document.getElementById('mtpStep').onclick=function(){stop();doStep();};

function resetTo(a,msg){stop();asg=a;step=0;evals=0;hist=[score(asg)[1]];logEl.textContent=msg;render();}

document.getElementById('mtpReset').onclick=function(){resetTo(START.slice(),'Server S1 starts 4 CPU over capacity. Press Run.');};

document.getElementById('mtpShuf').onclick=function(){var a=[];for(var i=0;i<12;i++)a.push(Math.random()<.55?Math.floor(Math.random()*2):Math.floor(Math.random()*4));resetTo(a,'New random start. Press Run.');};

/* expression graph */

var G=document.getElementById('mtpGraph'),gLog=document.getElementById('mtpGLog');

var LX=[110,300,490,680];

function drawGraph(live){

live=live||[];var u=util(asg),sc=score(asg),h='';

function on(s){return live.indexOf(s)>-1;}

var any=live.length>0;

for(var s=0;s<4;s++){

h+='<path class="edge'+(on(s)?' live':'')+'" d="M'+LX[s]+',232 L'+LX[s]+',172"/>';

h+='<path class="edge'+(on(s)?' live':'')+'" d="M'+LX[s]+',138 C'+LX[s]+',100 300,110 300,78"/>';

h+='<path class="edge'+(on(s)?' live':'')+'" d="M'+(LX[s]+40)+',233 C'+(LX[s]+60)+',200 650,130 650,78"/>';

}

h+='<path class="edge'+(any?' live':'')+'" d="M300,44 L300,22"/><path class="edge'+(any?' live':'')+'" d="M650,44 L650,22"/>';

function node(x,y,w,label,val,l){return '<g class="node'+(l?' live':'')+'"><rect x="'+(x-w/2)+'" y="'+(y-17)+'" width="'+w+'" height="34" rx="8"/><text x="'+x+'" y="'+(y-2)+'">'+label+'</text><text class="val" x="'+x+'" y="'+(y+11)+'">'+val+'</text></g>';}

for(var k=0;k<4;k++){h+=node(LX[k],250,96,'U(S'+(k+1)+')','= '+u[k],on(k));h+=node(LX[k],155,96,'SQUARE','= '+u[k]*u[k],on(k));}

h+=node(300,61,120,'SUM','= '+sc[1],any);h+=node(650,61,120,'MAX','= '+Math.max.apply(null,u),any);

h+='<text x="300" y="14" fill="#4C9BFF" font-size="11" font-weight="700" text-anchor="middle">BalanceSpec objective</text>';

h+='<text x="650" y="14" fill="'+(Math.max.apply(null,u)>CAP?'#FF5A6E':'#2BD99F')+'" font-size="11" font-weight="700" text-anchor="middle">CapacitySpec: MAX ≤ '+CAP+(Math.max.apply(null,u)>CAP?' (violated)':' (ok)')+'</text>';

h+='<text x="410" y="292" fill="#93A3BF" font-size="11" text-anchor="middle">Leaves: utilization per server · nodes recomputed this move: '+(any?(live.length*2+2):0)+' of 10</text>';

G.innerHTML=h;

}

document.getElementById('mtpGMove').onclick=function(){stop();var i=Math.floor(Math.random()*12),s;do{s=Math.floor(Math.random()*4);}while(s===asg[i]);var old=asg;asg=asg.slice();asg[i]=s;evals++;step++;hist.push(score(asg)[1]);render([i]);var d=diff(old,asg);drawGraph(d);gLog.textContent='Moved t'+(i+1)+' S'+(old[i]+1)+' → S'+(s+1)+'. Only '+(d.length*2+2)+' of 10 nodes needed new values.';};

document.getElementById('mtpGBest').onclick=function(){stop();var r=bestMove();evals+=r.n;if(!r.best){gLog.textContent='Local optimum reached. Try a random move first.';render();return;}var old=asg;asg=r.best.a;step++;hist.push(score(asg)[1]);render(r.best.ids);var d=diff(old,asg);drawGraph(d);gLog.textContent='Best of '+r.n+' candidates: '+r.best.txt+'. Recomputed '+(d.length*2+2)+' of 10 nodes.';};

/* solver picker */

var Os=document.getElementById('mtpOs'),Bs=document.getElementById('mtpBs');

function fmt(n){if(n>=1e9)return (n/1e9).toFixed(n>=1e10?0:1)+'B';if(n>=1e6)return (n/1e6).toFixed(n>=1e7?0:1)+'M';if(n>=1e3)return (n/1e3).toFixed(n>=1e4?0:1)+'k';return Math.round(n)+'';}

function pick(){

var o=exactO||Math.round(Math.pow(10,+Os.value)),b=exactB||Math.round(Math.pow(10,+Bs.value)),m=o*b,l=o+b;exactO=exactB=0;

document.getElementById('mtpOv').textContent=fmt(o);document.getElementById('mtpBv').textContent=fmt(b);

document.getElementById('mtpMv').textContent='≈ '+fmt(m);document.getElementById('mtpLv').textContent='≈ '+fmt(l);

document.getElementById('mtpMb').style.width=Math.min(100,Math.log10(m)/11*100)+'%';

document.getElementById('mtpLb').style.width=Math.min(100,Math.log10(l)/11*100)+'%';

var v=document.getElementById('mtpVerdict'),t;

if(m<=1e6){t='<b>最优(MIP)求解器是个不错的起点。</b> 小模型:交给 HiGHS、Gurobi 或 FICO Xpress,就能得到一个可证明最优的分配方案。';v.style.borderColor='#2BD99F';}

else if(m<=1e8){t='<b>先用 MIP 做原型,再转向局部搜索。</b> Meta 说这是常见路径:先用最优求解器找到一个强基线,然后迁移。';v.style.borderColor='#4C9BFF';}

else{t='<b>局部搜索。</b> 在最坏情况下,MIP 需要约 '+fmt(m)+' 个二元变量,而每个局部搜索邻域都保持在 '+fmt(l)+' 附近。Meta 几乎所有大规模问题都是这样跑的。';v.style.borderColor='#38D6FF';}

v.innerHTML=t;postH();

}

var exactO=0,exactB=0;Os.oninput=pick;Bs.oninput=pick;

document.querySelectorAll('#mtpP2 button[data-o]').forEach(function(btn){btn.onclick=function(){exactO=+btn.dataset.o;exactB=+btn.dataset.bn;Os.value=Math.log10(+btn.dataset.o);Bs.value=Math.log10(+btn.dataset.bn);pick();};});

/* 统计 */

var counted=false;

function count(){

var cards=document.querySelectorAll('#mtpCards .card');

cards.forEach(function(c,i){c.classList.remove('in');setTimeout(function(){c.classList.add('in');},90*i);});

document.querySelectorAll('#mtpCards [data-c]').forEach(function(el){var tgt=+el.dataset.c,dp=+(el.dataset.d||0),t0=null;

function f(ts){if(!t0)t0=ts;var p=Math.min(1,(ts-t0)/1100),e=1-Math.pow(1-p,3);el.textContent=(tgt*e).toFixed(dp);if(p<1)requestAnimationFrame(f);}requestAnimationFrame(f);});

}

/* 标签页 */

var tabs=R.querySelectorAll('.tab');

tabs.forEach(function(t){t.onclick=function(){tabs.forEach(function(x){x.classList.remove('on');});t.classList.add('on');

R.querySelectorAll('.pane').forEach(function(p){p.classList.remove('on');});document.getElementById('mtpP'+t.dataset.p).classList.add('on');

if(t.dataset.p==='3')count();if(t.dataset.p==='1')drawGraph(null);setTimeout(postH,60);setTimeout(postH,450);};});

hist=[score(asg)[1]];render();pick();

window.addEventListener('load',postH);window.addEventListener('resize',postH);setTimeout(postH,300);

})();

</script>

</body></html>">

Rebalancer 与最接近的开源替代方案

FeatureMeta RebalancerGoogle OR-ToolsTimefold Solver (Community)LicenseApache 2.0Apache 2.0Apache 2.0 (Enterprise edition is commercial)Core languageC++C++JavaAPIsC++, PythonC++, Python, Java, C#Java, KotlinFocusGeneric assignment (objects to bins)Broad suite: CP-SAT, LP, MIP wrappers, routing, packing, assignmentPlanning: routing, rostering, scheduling, task assignmentLocal searchYes, parallel, on expression graphYes, in routing solver (guided local search, simulated annealing, tabu)Yes, core engine (tabu, simulated annealing, late acceptance)MIP backendsFICO Xpress, Gurobi, HiGHSWrappers for commercial and open source MIP solversNot usedDebugging UIRebalancer Explorer (Docker)Not listed in READMEBenchmarker; score analysis in commercial editionsInstallpip install rebalancerpip install ortoolsMaven, JDK 21+

OR-Tools 覆盖更多问题类别,而 Timefold 面向 JVM 调度和路由。Rebalancer 的优势在于一份 assignment spec 可同时运行在本地搜索和 MIP 上。

关键要点

Rebalancer 将任何分配问题建模为 objects、bins、constraints 和 objectives。

Specs 编译成表达式图,由本地搜索或 MIP 求解器求解。

MIP 后端包括 FICO Xpress、Gurobi 和开源 HiGHS。

Meta 每天运行约 4000 万个问题;在 26.5 万个 objects 和 3200 个 bins 上,P99 为 12s。

Apache 2.0,C++ 和 Python API,今天即可从 PyPI 安装。

查看 Paper、GitHub Repo 和 Technical details。所有功劳归于该项目的研究者。另外,欢迎在 Twitter 上关注我们,别忘了加入我们 15 万+ 的 ML SubReddit 并订阅我们的 Newsletter。等等!你在 telegram 上吗?现在你也可以在 telegram 上加入我们了。

[Sponsored] The web is the one API most agents are missing. Databases, calendars and repos have APIs. The open web mostly doesn’t. The TinyFish MCP server gives any MCP client four tools: TinySearch, TinyFetch (full pages as markdown, JavaScript included), TinyBrowser for logins and forms, and TinyAgent for multi-step jobs. Search and Fetch are free.

The post Meta AI Open-Sources Rebalancer: A C++ Assignment Solver That Runs About 40 Million Placement Problems a Day appeared first on MarkTechPost.