Meta가 오픈소스로 공개했습니다 리밸런서, 할당 문제(assignment problems)를 풀기 위한 Python 인터페이스를 갖춘 C++ 라이브러리입니다. 이 라이브러리는 제약 조건과 목표에 따라 어떤 객체를 어느 빈(bin)에 넣을지 결정합니다. 다음에 따르면 Meta의 Engineering 게시물, Rebalancer는 9년 넘게 Meta 전반의 리소스 할당을 처리해 왔습니다. 이번 릴리스는 Apache 2.0 라이선스로 배포되며 문서, 그리고 PyPI 패키지 와 Rebalancer Explorer라는 디버깅 UI가 있습니다.
배포 가능한가요? 네. pip install rebalancer 은(는) Python 3.12+용 v1.0.4를 설치하며, Linux x86-64와 macOS 14+ ARM64용 사전 빌드 휠을 제공합니다. .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 솔버로도 너무 큽니다.
국소 탐색: 이 솔버는 표현식 그래프를 직접 다룹니다. 객체를 다른 빈으로 옮기는 이동을 탐색하며, 최악의 이웃 크기는 O(objects + bins)입니다. 그런 다음 어떤 제약 조건도 깨지 않는 최적의 후보를 적용합니다. 평가는 병렬화되어 초당 수백만 건의 평가에 도달하며, 탐색 공간은 가지치기됩니다.
Meta는 거의 모든 대규모 문제에 국소 탐색을 사용하고, 소규모~중간 규모 문제에는 MIP를 사용하며, 종종 먼저 MIP로 프로토타이핑합니다.
Meta의 프로덕션 수치
- 하루 약 40 million 건의 할당 문제가 30개 이상의 고유한 정식화(formulation)에서 해결됩니다.
- 265k 개체와 3.2k 빈에서 P99 해결 시간은 12초입니다.
- 1 million 개체와 5k 빈을 넘는 문제는 3.4k 회 이상의 실행에서 평균 171초가 걸립니다.
Rebalancer의 최적 활용 사례
- 클러스터에 샤드, 태스크 또는 컨테이너 배치: CPU와 메모리 상한을 지키면서 복제본을 랙에 분산시키며 서버에 작업을 할당합니다. Meta의 Shard Manager 와 RAS 가 이 패턴을 실행합니다.
- 리전 간 트래픽 및 워크로드 균형 조정: 사용자 트래픽이나 작업을 데이터센터로 라우팅하며, 지연 시간과 부하를 절충합니다. Taiji 는 엣지 트래픽에 대해 이를 수행하고, Meta는 우선순위에 따라 ML 학습의 균형을 맞춥니다.
- 인프라를 넘어선 운영 할당: 지원 티켓을 엔지니어에게, 회의를 회의실에, 또는 책상을 사람에게 용량 규칙에 따라 매핑합니다. Meta는 이 3가지를 모두 수행했습니다.
Rebalancer Explorer를 활용한 디버깅
Meta의 모델러들은 대부분의 시간을 솔버 동작 디버깅에 보냈습니다. Rebalancer Explorer 는 이를 위해 만들어진 Dockerized 웹 UI입니다. 바인딩 제약 조건, relaxation 효과, 그리고 객체가 특정 빈에 배치된 이유를 보여줍니다.
인터랙티브 설명
로컬 탐색 실행</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 초과하여 시작합니다. 실행을 누르세요.</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">1M+개 객체 및 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">그 1M+ 객체 규모에서의 실행 횟수</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">Built by 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">랙 '+(r?'B':'A')+' (스코프)</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='지역 최적해: 이동이나 교환으로는 점수가 더 이상 개선되지 않습니다 ('+evals+'회 평가).';render();return false;} var old=asg;asg=r.best.a;step++;hist.push(score(asg)[1]); var moved=r.best.ids;logEl.textContent='단계 '+step+': '+r.best.txt+' (후보 '+r.n+'개 확인)'; 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=' 지역 탐색 실행';} document.getElementById('mtpRun').onclick=function(){if(timer){stop();return;}this.textContent='❚❚ 일시정지';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(),'서버 S1이 4 CPU 초과 용량 상태로 시작합니다. 실행을 누르세요.');}; 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,'새로운 무작위 시작입니다. 실행을 누르세요.');}; /* 표현식 그래프 */ 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 목적함수</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?' (위반)':' (적합)')+'</text>'; h+='<text x="410" y="292" fill="#93A3BF" font-size="11" text-anchor="middle">리프: 서버별 사용률 · 이번 이동에서 재계산된 노드: 10개 중 '+(any?(live.length*2+2):0)+'개</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='t'+(i+1)+'을(를) S'+(old[i]+1)+' → S'+(s+1)+'(으)로 이동했습니다. 10개 노드 중 '+(d.length*2+2)+'개만 새 값이 필요했습니다.';}; document.getElementById('mtpGBest').onclick=function(){stop();var r=bestMove();evals+=r.n;if(!r.best){gLog.textContent='국소 최적해에 도달했습니다. 먼저 무작위 이동을 시도해 보세요.';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=r.n+'개 후보 중 최선: '+r.best.txt+'. 10개 노드 중 '+(d.length*2+2)+'개를 재계산했습니다.';}; /* 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>">리밸런서 vs 가장 유사한 오픈 소스 대안
| 기능 | Meta Rebalancer | Google OR-Tools | Timefold Solver (Community) |
|---|---|---|---|
| 라이선스 | Apache 2.0 | Apache 2.0 | Apache 2.0 (엔터프라이즈 에디션은 상용) |
| 핵심 언어 | C++ | C++ | Java |
| API | C++, Python | C++, Python, Java, C# | Java, Kotlin |
| 초점 | 범용 할당 (객체를 빈에 할당) | 광범위한 스위트: CP-SAT, LP, MIP 래퍼, 라우팅, 패킹, 할당 | 플래닝: 라우팅, 근무표 작성, 스케줄링, 작업 할당 |
| 로컬 서치 | 예, 병렬, 표현식 그래프 상에서 | 예, 라우팅 솔버 내에서 (guided local search, simulated annealing, tabu) | 예, 핵심 엔진 (tabu, simulated annealing, late acceptance) |
| MIP 백엔드 | FICO Xpress, Gurobi, HiGHS | 상용 및 오픈 소스 MIP 솔버용 래퍼 | 사용되지 않음 |
| UI 디버깅 | 리밸런서 탐색기 (Docker) | README에 기재되지 않음 | Benchmarker; 상용 에디션에서 점수 분석 |
| 설치 | pip install rebalancer | pip install ortools | Maven, JDK 21+ |
OR-Tools는 더 많은 문제 클래스를 다루고, Timefold는 JVM 스케줄링과 라우팅을 목표로 합니다. Rebalancer의 강점은 로컬 서치와 MIP 양쪽에서 모두 실행되는 하나의 할당 명세입니다.
핵심 요약
- Rebalancer는 모든 할당 문제를 객체, 빈, 제약 조건 및 목표로 모델링합니다.
- 명세는 표현식 그래프로 컴파일되어 로컬 서치 또는 MIP 솔버로 해결됩니다.
- MIP 백엔드에는 FICO Xpress, Gurobi 및 오픈 소스 HiGHS가 포함됩니다.
- Meta는 하루 약 40M 문제를 처리하며, 265k 객체와 3.2k 빈에서 P99는 12초입니다.
- Apache 2.0 라이선스, C++ 및 Python API, 오늘 PyPI에서 설치 가능.
다음을 확인해 보세요: 논문, GitHub 저장소 그리고 기술 세부 사항. 이 프로젝트의 연구자에게 모든 공이 돌아갑니다. 또한 저희를 Twitter 에서 팔로우하고 저희의 150k+ML SubReddit 에 참여하고 저희 뉴스레터를 구독하는 것도 잊지 마세요. 잠깐! 텔레그램을 사용하시나요? 이제 텔레그램에서도 저희와 함께할 수 있습니다.
이 글 Meta AI, 하루 약 40백만 건의 배치 문제를 처리하는 C++ 할당 솔버 Rebalancer 오픈소스 공개 은 다음에서 처음 게시되었습니다: MarkTechPost.