diff --git a/OneDotNet.sln b/OneDotNet.sln index 0814b2a3..68e0deae 100644 --- a/OneDotNet.sln +++ b/OneDotNet.sln @@ -122,6 +122,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SerilogLab", "codelab\Seril EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xceed.Wpf.Toolkit", "third_party\ExtendedWPFToolkit\Xceed.Wpf.Toolkit\Xceed.Wpf.Toolkit.csproj", "{6F79C3CF-633E-4645-A3B1-D9D6520526CD}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskAssigner", "TaskAssigner\TaskAssigner.csproj", "{02544C21-1FEB-4B34-9F01-288EE78B3630}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -329,6 +331,11 @@ Global {6F79C3CF-633E-4645-A3B1-D9D6520526CD}.DebugNonWindows|Any CPU.Build.0 = Debug|Any CPU {6F79C3CF-633E-4645-A3B1-D9D6520526CD}.Release|Any CPU.ActiveCfg = Release|Any CPU {6F79C3CF-633E-4645-A3B1-D9D6520526CD}.Release|Any CPU.Build.0 = Release|Any CPU + {02544C21-1FEB-4B34-9F01-288EE78B3630}.Debug|Any CPU.ActiveCfg = Debug|x64 + {02544C21-1FEB-4B34-9F01-288EE78B3630}.Debug|Any CPU.Build.0 = Debug|x64 + {02544C21-1FEB-4B34-9F01-288EE78B3630}.DebugNonWindows|Any CPU.ActiveCfg = Release|x64 + {02544C21-1FEB-4B34-9F01-288EE78B3630}.DebugNonWindows|Any CPU.Build.0 = Release|x64 + {02544C21-1FEB-4B34-9F01-288EE78B3630}.Release|Any CPU.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -372,6 +379,7 @@ Global {9578F6A5-5C44-46F3-9965-DEF4D39FC5D5} = {BB7EFF62-B03A-4996-9798-17DB422DD96F} {B0AF0ADB-08FE-4312-BDDE-3C3562FB7E37} = {9D7C39A1-EF36-4491-8CC0-2D45C3B51580} {6F79C3CF-633E-4645-A3B1-D9D6520526CD} = {E307CA64-13F5-446A-BCA5-C611A235A2E4} + {02544C21-1FEB-4B34-9F01-288EE78B3630} = {9D7C39A1-EF36-4491-8CC0-2D45C3B51580} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E0F2E2F1-944D-46B4-BA89-EE1F0BBD57A1} diff --git a/TaskAssigner/Assigner.cs b/TaskAssigner/Assigner.cs new file mode 100644 index 00000000..fad9f0e9 --- /dev/null +++ b/TaskAssigner/Assigner.cs @@ -0,0 +1,26 @@ +using Google.OrTools.LinearSolver; +using System; + +namespace TaskAssigner +{ + public class Assigner + { + private readonly Solver solver_; + + public Assigner(Solver solver) + { + this.solver_ = solver ?? throw new ArgumentNullException(nameof(solver)); + } + + public AssignmentDescription Solve(AssignmentDescription assignment) + { + if (assignment is null) + { + throw new ArgumentNullException(nameof(assignment)); + } + + var assignerCore = new AssignerCore(this.solver_, assignment); + return assignerCore.Run(); + } + } +} diff --git a/TaskAssigner/AssignerCore.cs b/TaskAssigner/AssignerCore.cs new file mode 100644 index 00000000..e5e6de72 --- /dev/null +++ b/TaskAssigner/AssignerCore.cs @@ -0,0 +1,146 @@ +using Google.OrTools.LinearSolver; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TaskAssigner +{ + public class AssignerCore + { + private readonly Solver solver_; + private readonly AssignmentDescription assignment_; + private readonly int[,] costs_; + private readonly Variable[,] variables_; + + // TODO(zhangshuai.ds): Pass in 3 policies. + public AssignerCore(Solver solver, AssignmentDescription assignment) + { + this.solver_ = solver ?? throw new ArgumentNullException(nameof(solver)); + this.assignment_ = assignment ?? throw new ArgumentNullException(nameof(assignment)); + this.costs_ = this.ComputeCosts(assignment); + this.variables_ = this.solver_.MakeBoolVarMatrix(assignment.Nodes.Count, assignment.Tasks.Count); + } + + public AssignmentDescription Run() + { + this.AddObjective(); + this.AddConstraints(); + + Solver.ResultStatus status = this.solver_.Solve(); + if (status == Solver.ResultStatus.OPTIMAL) + { + var result = new AssignmentDescription(); + + foreach (NodeDescription n in this.assignment_.Nodes) + { + result.Nodes.Add(n); + } + + foreach (TaskDescription t in this.assignment_.Tasks) + { + result.Tasks.Add(t); + } + + for (int i = 1; i < this.assignment_.Nodes.Count; i++) + { + for (int j = 0; j < this.assignment_.Tasks.Count; j++) + { + if (this.variables_[i, j].SolutionValue() > 0) + { + if (!result.Assignments.TryGetValue(i, out ISet tasks)) + { + tasks = new HashSet(); + result.Assignments.Add(i, tasks); + } + tasks.Add(j); + Console.WriteLine($"Task {j} assigned to node {i} with cost {this.costs_[i, j]}."); + } + } + } + Console.WriteLine($"Total cost is {this.solver_.Objective().Value()}"); + + return result; + } + + throw new NotImplementedException($"Not implemented for status: {status}"); + } + + protected int[,] ComputeCosts(AssignmentDescription assignment) + { + int[,] costs = new int[assignment.Nodes.Count, assignment.Tasks.Count]; + for (int j = 0; j < assignment.Tasks.Count; j++) + { + costs[0, j] = 500; // UNASSIGNED cost + if (assignment.Assignments[0].Contains(j)) // Not assigned yet. + { + for (int i = 1; i < assignment.Nodes.Count; i++) + { + costs[i, j] = 0; // No fee for assignment. + } + } + else + { + for (int i = 1; i < assignment.Nodes.Count; i++) + { + if (assignment.Assignments[i].Contains(j)) + { + costs[i, j] = 0; // Don't move. + } + else + { + costs[i, j] = 200; // Moving cost. + } + } + } + } + return costs; + } + + protected void AddObjective() + { + var movingCosts = new LinearExpr(); + for (int i = 0; i < this.assignment_.Nodes.Count; i++) + { + for (int j = 0; j < this.assignment_.Tasks.Count; j++) + { + movingCosts += this.costs_[i, j] * this.variables_[i, j]; + } + } + + // TODO(zhangshuai.ds): Computes it. + var imbalanceCosts = new LinearExpr(); + + // TODO(zhangshuai.ds): Add coefficients for the two costs. + this.solver_.Minimize(movingCosts + imbalanceCosts); + } + + protected void AddConstraints() + { + // Each task is assigned to exactly one node (including UNASSIGNED virtual node). + for (int j = 0; j < this.assignment_.Tasks.Count; j++) + { + LinearExpr expr = Enumerable.Range(0, this.assignment_.Nodes.Count) + .Select(i => this.variables_[i, j]) + .ToArray() + .Sum(); + this.solver_.Add(expr == 1); + } + + // Each worker cannot exceed its capacity. + for (int i = 0; i < this.assignment_.Nodes.Count; i++) + { + var cpuCores = new LinearExpr(); + var memoryMiB = new LinearExpr(); + + for (int j = 0; j < this.assignment_.Tasks.Count; j++) + { + cpuCores += this.variables_[i, j] * this.assignment_.Tasks[j].CpuCores; + memoryMiB += this.variables_[i, j] * this.assignment_.Tasks[j].MemoryMiB; + } + + this.solver_.Add(cpuCores <= this.assignment_.Nodes[i].CpuCores); + this.solver_.Add(memoryMiB <= this.assignment_.Nodes[i].MemoryMiB); + } + } + } +} diff --git a/TaskAssigner/AssignmentDescription.cs b/TaskAssigner/AssignmentDescription.cs new file mode 100644 index 00000000..7deb866d --- /dev/null +++ b/TaskAssigner/AssignmentDescription.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace TaskAssigner +{ + public class AssignmentDescription + { + // Nodes[0] means UNASSIGNED + public IList Nodes { get; } = new List(); + + public IList Tasks { get; } = new List(); + + public IDictionary> Assignments { get; } = + new Dictionary>(); + } +} diff --git a/TaskAssigner/NodeDescription.cs b/TaskAssigner/NodeDescription.cs new file mode 100644 index 00000000..99f49978 --- /dev/null +++ b/TaskAssigner/NodeDescription.cs @@ -0,0 +1,11 @@ +namespace TaskAssigner +{ + public class NodeDescription + { + public int Id { get; set; } + + public int CpuCores { get; set; } + + public int MemoryMiB { get; set; } + } +} diff --git a/TaskAssigner/Program.cs b/TaskAssigner/Program.cs new file mode 100644 index 00000000..ce0c6d2e --- /dev/null +++ b/TaskAssigner/Program.cs @@ -0,0 +1,63 @@ +using Google.OrTools.LinearSolver; +using System.Collections.Generic; + +namespace TaskAssigner +{ + class Program + { + static void Main() + { + using var solver = Solver.CreateSolver( + "SimpleMipProgram", + "CBC_MIXED_INTEGER_PROGRAMMING"); + + var assigner = new Assigner(solver); + + var assignment = new AssignmentDescription(); + assignment.Nodes.Add(new NodeDescription + { + Id = 0, + CpuCores = int.MaxValue, + MemoryMiB = int.MaxValue, + }); + for (int i = 1; i <= 3; i++) + { + assignment.Nodes.Add(new NodeDescription + { + Id = i, + CpuCores = 48, + MemoryMiB = 64 << 10, // 64 GiB + }); + } + for (int i = 0; i < 13; i++) + { + assignment.Tasks.Add(new TaskDescription + { + Id = i, + CpuCores = 8, + MemoryMiB = 32 << 10, // 32 GiB, CPU:MEM=1:4 + }); + } + for (int i = 0; i < 17; i++) + { + assignment.Tasks.Add(new TaskDescription + { + Id = 13 + i, + CpuCores = 16, + MemoryMiB = 32 << 10, // 16 GiB, CPU:MEM=1:2 + }); + } + assignment.Assignments[0] = new HashSet(); + for (int i = 0; i < assignment.Tasks.Count; i++) + { + assignment.Assignments[0].Add(i); + } + for (int i = 1; i < assignment.Nodes.Count; i++) + { + assignment.Assignments[i] = new HashSet(); + } + + assignment = assigner.Solve(assignment); + } + } +} diff --git a/TaskAssigner/TaskAssigner.csproj b/TaskAssigner/TaskAssigner.csproj new file mode 100644 index 00000000..856865dc --- /dev/null +++ b/TaskAssigner/TaskAssigner.csproj @@ -0,0 +1,13 @@ + + + + Exe + $(NetfxCurrentTargetFramework) + x64 + + + + + + + diff --git a/TaskAssigner/TaskDescription.cs b/TaskAssigner/TaskDescription.cs new file mode 100644 index 00000000..770d608f --- /dev/null +++ b/TaskAssigner/TaskDescription.cs @@ -0,0 +1,11 @@ +namespace TaskAssigner +{ + public class TaskDescription + { + public int Id { get; set; } + + public int CpuCores { get; set; } + + public int MemoryMiB { get; set; } + } +}