feat(wip): sketch out the scheduling strategy
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
@@ -18,6 +19,11 @@ type cpuHandler struct {
|
|||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type memHandler struct {
|
||||||
|
amount int64
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
// readIntFromFile reads the given file until the end
|
// readIntFromFile reads the given file until the end
|
||||||
// and converts the contained string into an integer
|
// and converts the contained string into an integer
|
||||||
func readIntFromFile(path string) (int, error) {
|
func readIntFromFile(path string) (int, error) {
|
||||||
@@ -28,6 +34,25 @@ func readIntFromFile(path string) (int, error) {
|
|||||||
return strconv.Atoi(strings.TrimSpace(string(b)))
|
return strconv.Atoi(strings.TrimSpace(string(b)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readTotalMemoryKb() (int, error) {
|
||||||
|
b, err := os.ReadFile("/proc/meminfo")
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
s := string(b)
|
||||||
|
for line := range strings.Lines(s) {
|
||||||
|
if !strings.Contains(line, "MemTotal:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
return 0, fmt.Errorf("expected line '%s' to have at least 2 fields", line)
|
||||||
|
}
|
||||||
|
return strconv.Atoi(fields[1])
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("failed to find MemTotal in /proc/meminfo")
|
||||||
|
}
|
||||||
|
|
||||||
func enumerateCPUCores(logger *slog.Logger) ([]string, int, int, error) {
|
func enumerateCPUCores(logger *slog.Logger) ([]string, int, int, error) {
|
||||||
files, err := os.ReadDir("/sys/devices/system/cpu/")
|
files, err := os.ReadDir("/sys/devices/system/cpu/")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -135,6 +160,17 @@ func (s *cpuHandler) handleCurrent(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *cpuHandler) handleCount(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "GET" {
|
||||||
|
s.logger.Error("unexepcted method for cpu count", "method", r.Method)
|
||||||
|
writeResponse("invalid method", http.StatusBadRequest, w, s.logger)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(s.corePaths)
|
||||||
|
writeResponse(strconv.Itoa(count), http.StatusOK, w, s.logger)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *cpuHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (s *cpuHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
requestedPath := getStrippedRequestPath(r.URL, r.Pattern)
|
requestedPath := getStrippedRequestPath(r.URL, r.Pattern)
|
||||||
switch requestedPath {
|
switch requestedPath {
|
||||||
@@ -147,12 +183,25 @@ func (s *cpuHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
case "frequency/current":
|
case "frequency/current":
|
||||||
s.handleCurrent(w, r)
|
s.handleCurrent(w, r)
|
||||||
return
|
return
|
||||||
|
case "count":
|
||||||
|
s.handleCount(w, r)
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
s.logger.Error("unknown resource", "path", requestedPath)
|
s.logger.Error("unknown resource", "path", requestedPath)
|
||||||
writeResponse("not found", http.StatusNotFound, w, s.logger)
|
writeResponse("not found", http.StatusNotFound, w, s.logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *memHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestedPath := getStrippedRequestPath(r.URL, r.Pattern)
|
||||||
|
if requestedPath == "amount" {
|
||||||
|
writeResponse(strconv.FormatInt(m.amount, 10), http.StatusOK, w, m.logger)
|
||||||
|
} else {
|
||||||
|
m.logger.Error("unknown resource", "path", requestedPath)
|
||||||
|
writeResponse("not found", http.StatusNotFound, w, m.logger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
appEnv := os.Getenv("AGENT_ENV")
|
appEnv := os.Getenv("AGENT_ENV")
|
||||||
loggerOpts := &slog.HandlerOptions{
|
loggerOpts := &slog.HandlerOptions{
|
||||||
@@ -176,6 +225,18 @@ func main() {
|
|||||||
minFreq: minF,
|
minFreq: minF,
|
||||||
corePaths: freqPaths,
|
corePaths: freqPaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memKb, err := readTotalMemoryKb()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to read memory size", "err", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
memHandler := &memHandler{
|
||||||
|
amount: int64(memKb) * 1024,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
|
||||||
http.Handle("/cpu/", logRequests(logger, cpuHandler))
|
http.Handle("/cpu/", logRequests(logger, cpuHandler))
|
||||||
|
http.Handle("/mem/", logRequests(logger, memHandler))
|
||||||
logger.Info("exit", "result", http.ListenAndServe(":8080", nil))
|
logger.Info("exit", "result", http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,46 +17,34 @@ class K8sDVFSClient {
|
|||||||
this.httpClient = HttpClient.newBuilder().build()
|
this.httpClient = HttpClient.newBuilder().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionalInt getNodeCurrentFrequency(String node) {
|
OptionalLong getNodeCurrentFrequency(String node) {
|
||||||
log.debug("Getting current frequency of node ${node}")
|
log.debug("Getting current frequency of node ${node}")
|
||||||
return getNodeFrequency(node, "current")
|
return getNodeFrequency(node, "current")
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionalInt getNodeMaxFrequency(String node) {
|
OptionalLong getNodeMaxFrequency(String node) {
|
||||||
log.debug("Getting max frequency of node ${node}")
|
log.debug("Getting max frequency of node ${node}")
|
||||||
return getNodeFrequency(node, "max")
|
return getNodeFrequency(node, "max")
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionalInt getNodeMinFrequency(String node) {
|
OptionalLong getNodeMinFrequency(String node) {
|
||||||
log.debug("Getting min frequency of node ${node}")
|
log.debug("Getting min frequency of node ${node}")
|
||||||
return getNodeFrequency(node, "min")
|
return getNodeFrequency(node, "min")
|
||||||
}
|
}
|
||||||
|
|
||||||
private OptionalInt getNodeFrequency(String node, String endpoint) {
|
private OptionalLong getNodeFrequency(String node, String endpoint) {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
return getLong(HttpRequest.newBuilder()
|
||||||
.uri(new URI("http://${nodeName}/cpu/frequency/${endpoint}"))
|
.uri(new URI("http://${agentAddress(node)}/cpu/frequency/${endpoint}"))
|
||||||
.GET()
|
.GET()
|
||||||
.build()
|
.build())
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
log.error("Request GET ${request.uri().toString()} returned ${response.statusCode()}")
|
|
||||||
return OptionalInt.empty()
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
double parsed = Double.parseDouble(response.body())
|
|
||||||
return OptionalInt.of(parsed)
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
log.error("Unexpected response ${response.body()} - ${ex.message}")
|
|
||||||
return OptionalInt.empty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean setNodeFrequency(String node, int frequency) {
|
boolean setNodeFrequency(String node, int frequency) {
|
||||||
String body = String.valueOf(frequency)
|
String body = String.valueOf(frequency)
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(new URI("http://${nodeName}/cpu/frequency/current"))
|
.uri(new URI("http://${agentAddress(node)}/cpu/frequency/current"))
|
||||||
.PUT(HttpRequ3est.BodyPublishers.ofString(body))
|
.PUT(HttpRequest.BodyPublishers.ofString(body))
|
||||||
.build()
|
.build()
|
||||||
HttpResponse<Void> response = httpClient.send(request, HttpResponse.BodyHandlers.discarding())
|
HttpResponse<Void> response = httpClient.send(request, HttpResponse.BodyHandlers.discarding())
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
@@ -65,4 +53,37 @@ class K8sDVFSClient {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OptionalLong getCPUCount(String node) {
|
||||||
|
return getLong(HttpRequest.newBuilder()
|
||||||
|
.uri(new URI("http://${agentAddress(node)}/cpu/count"))
|
||||||
|
.GET()
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionalLong getMemoryAmount(String node) {
|
||||||
|
return getInt(HttpRequest.newBuilder()
|
||||||
|
.uri(new URI("http://${agentAddress(node)}/mem/amount"))
|
||||||
|
.GET()
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
private OptionalLong getLong(HttpRequest request) {
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
log.error("Request GET ${request.uri().toString()} returned ${response.statusCode()}")
|
||||||
|
return OptionalLong.empty()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long parsed = Long.parseLong(response.body())
|
||||||
|
return OptionalLong.of(parsed)
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
log.error("Unexpected response ${response.body()} - ${ex.message}")
|
||||||
|
return OptionalLong.empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String agentAddress(String node) {
|
||||||
|
return K8sNodeInitDeployer.buildPodName(node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class K8sDriverLauncher {
|
|||||||
this.k8sConfig = makeK8sConfig(config.toMap())
|
this.k8sConfig = makeK8sConfig(config.toMap())
|
||||||
this.k8sClient = makeK8sClient(k8sConfig)
|
this.k8sClient = makeK8sClient(k8sConfig)
|
||||||
this.k8sConfig.checkStorageAndPaths(k8sClient)
|
this.k8sConfig.checkStorageAndPaths(k8sClient)
|
||||||
this.initDeployer = new K8sNodeInitDeployer(k8sClient, k8sConfig, runName)
|
this.initDeployer = new K8sNodeInitDeployer(k8sClient, k8sConfig)
|
||||||
|
|
||||||
createK8sConfigMap()
|
createK8sConfigMap()
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class K8sExecutor extends Executor implements ExtensionPoint {
|
|||||||
if (k8sConfig.schedulingStrategy == "Hash") {
|
if (k8sConfig.schedulingStrategy == "Hash") {
|
||||||
strategy = new K8sHashSchedulingStrategy()
|
strategy = new K8sHashSchedulingStrategy()
|
||||||
} else if (k8sConfig.schedulingStrategy == "DVFS") {
|
} else if (k8sConfig.schedulingStrategy == "DVFS") {
|
||||||
strategy = new K8sDVFSSchedulingStrategy()
|
strategy = new K8sDVFSSchedulingStrategy(this.runtimeEstimator, new K8sDVFSClient())
|
||||||
} else {
|
} else {
|
||||||
log.error "[K8s] invalid scheduling strategy $k8sConfig.schedulingStrategy, falling back on \"Hash\""
|
log.error "[K8s] invalid scheduling strategy $k8sConfig.schedulingStrategy, falling back on \"Hash\""
|
||||||
strategy = new K8sHashSchedulingStrategy()
|
strategy = new K8sHashSchedulingStrategy()
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import nextflow.k8s.model.PodSpecBuilder
|
|||||||
class K8sNodeInitDeployer {
|
class K8sNodeInitDeployer {
|
||||||
private K8sClient client
|
private K8sClient client
|
||||||
private K8sConfig config
|
private K8sConfig config
|
||||||
private String runName
|
|
||||||
|
|
||||||
K8sNodeInitDeployer(K8sClient client, K8sConfig config, String runName) {
|
K8sNodeInitDeployer(K8sClient client, K8sConfig config) {
|
||||||
this.client = client
|
this.client = client
|
||||||
this.config = config
|
this.config = config
|
||||||
this.runName = runName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void deploy() {
|
void deploy() {
|
||||||
@@ -110,16 +108,16 @@ exit 0
|
|||||||
private List<String> getNodes() {
|
private List<String> getNodes() {
|
||||||
final resp = client.nodeList()
|
final resp = client.nodeList()
|
||||||
ArrayList<String> nodes = new ArrayList<String>()
|
ArrayList<String> nodes = new ArrayList<String>()
|
||||||
for ( Map item : resp.items ) {
|
for ( Map item: resp.items ) {
|
||||||
nodes.add(item.metadata.name as String)
|
nodes.add(item.metadata.name as String)
|
||||||
}
|
}
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildPodName(String nodeName) {
|
public static String buildPodName(String nodeName) {
|
||||||
// TODO: Remove anything that is not lowercase alpha-numeric or dash
|
// TODO: Remove anything that is not lowercase alpha-numeric or dash
|
||||||
String sanitizedNodeName = nodeName.toLowerCase()
|
String sanitizedNodeName = nodeName.toLowerCase()
|
||||||
String name = "nf-init-${runName}-${sanitizedNodeName}"
|
String name = "nf-init-${sanitizedNodeName}"
|
||||||
if ( name.length() > 63 )
|
if ( name.length() > 63 )
|
||||||
name = name.substring(0, 63)
|
name = name.substring(0, 63)
|
||||||
return name
|
return name
|
||||||
|
|||||||
@@ -14,4 +14,8 @@ class K8sSchedulingRequest {
|
|||||||
this.task = handler.task
|
this.task = handler.task
|
||||||
this.submitTimeMillis = System.currentTimeMillis()
|
this.submitTimeMillis = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getTaskName() {
|
||||||
|
return task.processor.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,10 @@ interface K8sSchedulingStrategy {
|
|||||||
* @return {@code true} if @ref schedule should immediately be called
|
* @return {@code true} if @ref schedule should immediately be called
|
||||||
*/
|
*/
|
||||||
boolean scheduleImmediately(K8sTaskScheduler scheduler, List<K8sSchedulingRequest> queue)
|
boolean scheduleImmediately(K8sTaskScheduler scheduler, List<K8sSchedulingRequest> queue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a task has finished execution.
|
||||||
|
* @param task the task
|
||||||
|
*/
|
||||||
|
void taskFinished(K8sTaskHandler task)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ class K8sTaskScheduler implements Runnable {
|
|||||||
}
|
}
|
||||||
nodeTasks.remove(handler)
|
nodeTasks.remove(handler)
|
||||||
log.info "[K8s] removed task ${handler.task.name} from node ${nodeName}"
|
log.info "[K8s] removed task ${handler.task.name} from node ${nodeName}"
|
||||||
|
|
||||||
|
strategy.taskFinished(handler)
|
||||||
|
|
||||||
|
/* If we have pending tasks, now would be a good time to schedule a new one.
|
||||||
|
* Because resources were freed right now */
|
||||||
|
if (queue.size() > 0)
|
||||||
|
drain()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected synchronized void drain() {
|
protected synchronized void drain() {
|
||||||
|
|||||||
@@ -1,22 +1,146 @@
|
|||||||
package nextflow.k8s.strategies
|
package nextflow.k8s.strategies
|
||||||
|
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import groovy.util.logging.Slf4j
|
||||||
|
import nextflow.k8s.K8sDVFSClient
|
||||||
|
import nextflow.k8s.K8sRuntimeEstimator
|
||||||
import nextflow.k8s.K8sSchedulingDecision
|
import nextflow.k8s.K8sSchedulingDecision
|
||||||
import nextflow.k8s.K8sSchedulingRequest
|
import nextflow.k8s.K8sSchedulingRequest
|
||||||
import nextflow.k8s.K8sSchedulingStrategy
|
import nextflow.k8s.K8sSchedulingStrategy
|
||||||
|
import nextflow.k8s.K8sTaskHandler
|
||||||
import nextflow.k8s.K8sTaskScheduler
|
import nextflow.k8s.K8sTaskScheduler
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a scheduling strategy utilizing dvfs to reduce the energy consumption
|
* Implements a scheduling strategy utilizing dvfs to reduce the energy consumption
|
||||||
* of workflow execution, while attempting to maintain the same makespan.
|
* of workflow execution, while attempting to maintain the same makespan.
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@CompileStatic
|
||||||
class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy {
|
class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy {
|
||||||
|
|
||||||
|
private class WorkerNode {
|
||||||
|
long maxFrequency
|
||||||
|
long minFrequency
|
||||||
|
long currentFrequency
|
||||||
|
long cpuCount
|
||||||
|
long memoryAmount
|
||||||
|
|
||||||
|
WorkerNode(long maxF, long minF, long curF, long cpus, long mem) {
|
||||||
|
this.maxFrequency = maxF
|
||||||
|
this.minFrequency = minF
|
||||||
|
this.currentFrequency = curF
|
||||||
|
this.cpuCount = cpus
|
||||||
|
this.memoryAmount = mem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchedulingRequestComparator implements Comparator<K8sSchedulingRequest> {
|
||||||
|
K8sRuntimeEstimator runtimeEstimator
|
||||||
|
long currentTime
|
||||||
|
double avgRuntime
|
||||||
|
|
||||||
|
@Override
|
||||||
|
int compare(K8sSchedulingRequest o1, K8sSchedulingRequest o2) {
|
||||||
|
// First, check if one of the tasks is (estimated to be) on the critical path
|
||||||
|
double t1 = runtimeEstimator.estimate(o1.handler)
|
||||||
|
double t2 = runtimeEstimator.estimate(o2.handler)
|
||||||
|
if (t1 > avgRuntime && t2 <= avgRuntime)
|
||||||
|
return -1
|
||||||
|
else if (t1 < avgRuntime && t2 > avgRuntime)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
// Both are not on the critical path. Sort based on the time they spent in the queue
|
||||||
|
long w1 = currentTime - o1.submitTimeMillis
|
||||||
|
long w2 = currentTime - o2.submitTimeMillis
|
||||||
|
if (w1 > w2)
|
||||||
|
return -1
|
||||||
|
else if (w2 > w1)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private K8sRuntimeEstimator runtimeEstimator
|
||||||
|
private K8sDVFSClient dvfsClient
|
||||||
|
|
||||||
|
private HashMap<String, WorkerNode> nodes
|
||||||
|
|
||||||
|
private double averageRuntime
|
||||||
|
private long finishedTaskCount
|
||||||
|
|
||||||
|
K8sDVFSSchedulingStrategy(K8sRuntimeEstimator runtimeEstimator, K8sDVFSClient dvfsClient) {
|
||||||
|
this.runtimeEstimator = runtimeEstimator
|
||||||
|
this.dvfsClient = dvfsClient
|
||||||
|
this.nodes = new HashMap<>()
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
K8sSchedulingDecision schedule(K8sTaskScheduler scheduler, List<K8sSchedulingRequest> queue) {
|
K8sSchedulingDecision schedule(K8sTaskScheduler scheduler, List<K8sSchedulingRequest> queue) {
|
||||||
|
if (nodes.isEmpty()) {
|
||||||
|
if (!initNodes(scheduler))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step 1: Sort by task priority. We will attempt to schedule tasks "in order", so that the
|
||||||
|
* highest priority tasks are assigned to nodes as soon as possible.
|
||||||
|
*
|
||||||
|
* Priority is based on a) an estimation if the task is on the critical path and b) the wait time of the task.
|
||||||
|
*/
|
||||||
|
SchedulingRequestComparator comparator = new SchedulingRequestComparator()
|
||||||
|
comparator.runtimeEstimator = runtimeEstimator
|
||||||
|
comparator.currentTime = System.currentTimeMillis()
|
||||||
|
queue.sort(comparator)
|
||||||
|
|
||||||
|
/* Step 2: For each task attempt to schedule it onto a node */
|
||||||
|
for (K8sSchedulingRequest req : queue) {
|
||||||
|
/* Step 2.1: Determine the execution frequency.
|
||||||
|
* If the task is on the critical path, just use the maximum available frequency.
|
||||||
|
*/
|
||||||
|
final double taskEstimation = runtimeEstimator.estimate(req.handler)
|
||||||
|
final boolean isCriticalPath = taskEstimation > averageRuntime
|
||||||
|
|
||||||
|
|
||||||
|
/* Step 2.2: Filter nodes based on task requirements */
|
||||||
|
|
||||||
|
/* Step 2.3: Assign to node based on "best fit" - current node frequency is closest to determined frequency */
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean scheduleImmediately(K8sTaskScheduler scheduler, List<K8sSchedulingRequest> queue) {
|
boolean scheduleImmediately(K8sTaskScheduler scheduler, List<K8sSchedulingRequest> queue) {
|
||||||
return false
|
return queue != null && queue.size() > 0 && scheduler.freeNodes.size() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void taskFinished(K8sTaskHandler task) {
|
||||||
|
/* TODO: This just uses elapsed wall-clock time, regardless of the frequency used to execute the task.
|
||||||
|
* This will skew the average towards longer runtimes, which is undesirable, because it will lead to more
|
||||||
|
* tasks classified as "critical path".
|
||||||
|
* A simple (rough) solution could be to keep track of the tasks "relative" frequency and just scale the
|
||||||
|
* elapsed time based on that.
|
||||||
|
*/
|
||||||
|
double runtime = (double)(task.getCompleteTimeMillis() - task.getStartTimeMillis())
|
||||||
|
averageRuntime = (runtime + finishedTaskCount * averageRuntime) / (finishedTaskCount + 1)
|
||||||
|
finishedTaskCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initNodes(K8sTaskScheduler scheduler) {
|
||||||
|
final nodes = scheduler.getNodes()
|
||||||
|
for (String node : nodes) {
|
||||||
|
final cur = dvfsClient.getNodeCurrentFrequency(node)
|
||||||
|
final min = dvfsClient.getNodeMinFrequency(node)
|
||||||
|
final max = dvfsClient.getNodeMaxFrequency(node)
|
||||||
|
final cpus = dvfsClient.getCPUCount(node)
|
||||||
|
final mem = dvfsClient.getMemoryAmount(node)
|
||||||
|
|
||||||
|
if (cur.empty || min.empty || max.empty || cpus.empty || mem.empty) {
|
||||||
|
log.error "[K8s] failed to query node $node information"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
this.nodes.put(node, new WorkerNode(max.asLong, min.asLong, cur.asLong, cpus.asLong, mem.asLong))
|
||||||
|
}
|
||||||
|
return !this.nodes.isEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import groovy.transform.CompileStatic
|
|||||||
import nextflow.k8s.K8sSchedulingDecision
|
import nextflow.k8s.K8sSchedulingDecision
|
||||||
import nextflow.k8s.K8sSchedulingRequest
|
import nextflow.k8s.K8sSchedulingRequest
|
||||||
import nextflow.k8s.K8sSchedulingStrategy
|
import nextflow.k8s.K8sSchedulingStrategy
|
||||||
|
import nextflow.k8s.K8sTaskHandler
|
||||||
import nextflow.k8s.K8sTaskScheduler
|
import nextflow.k8s.K8sTaskScheduler
|
||||||
|
|
||||||
@CompileStatic
|
@CompileStatic
|
||||||
@@ -30,4 +31,7 @@ class K8sHashSchedulingStrategy implements K8sSchedulingStrategy {
|
|||||||
final freeNodes = scheduler.freeNodes
|
final freeNodes = scheduler.freeNodes
|
||||||
return freeNodes.size() > 0
|
return freeNodes.size() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void taskFinished(K8sTaskHandler task) { /* nop */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,7 +644,7 @@ class K8sDriverLauncherTest extends Specification {
|
|||||||
def driver = Spy(K8sDriverLauncher)
|
def driver = Spy(K8sDriverLauncher)
|
||||||
driver.@k8sConfig = config
|
driver.@k8sConfig = config
|
||||||
driver.@runName = POD_NAME
|
driver.@runName = POD_NAME
|
||||||
driver.@initDeployer = new K8sNodeInitDeployer(driver.k8sClient, config, driver.runName)
|
driver.@initDeployer = new K8sNodeInitDeployer(driver.k8sClient, config)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
driver.shutdown()
|
driver.shutdown()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
given:
|
given:
|
||||||
def client = Mock(K8sClient)
|
def client = Mock(K8sClient)
|
||||||
def config = new K8sConfig(nodeInit: [enabled: false])
|
def config = new K8sConfig(nodeInit: [enabled: false])
|
||||||
def deployer = new K8sNodeInitDeployer(client, config, 'run-foo')
|
def deployer = new K8sNodeInitDeployer(client, config)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
deployer.deploy()
|
deployer.deploy()
|
||||||
@@ -32,7 +32,7 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
image: 'ubuntu:latest',
|
image: 'ubuntu:latest',
|
||||||
command: ['/bin/bash', '-c', 'echo init']
|
command: ['/bin/bash', '-c', 'echo init']
|
||||||
])
|
])
|
||||||
def deployer = new K8sNodeInitDeployer(client, config, 'run-foo')
|
def deployer = new K8sNodeInitDeployer(client, config)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
deployer.deploy()
|
deployer.deploy()
|
||||||
@@ -48,13 +48,13 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
then:
|
then:
|
||||||
1 * client.podCreate({ Map spec ->
|
1 * client.podCreate({ Map spec ->
|
||||||
spec.kind == 'Pod'
|
spec.kind == 'Pod'
|
||||||
spec.metadata.name == 'nf-init-run-foo-node-a'
|
spec.metadata.name == 'nf-init-node-a'
|
||||||
spec.metadata.namespace == 'default'
|
spec.metadata.namespace == 'default'
|
||||||
spec.spec.nodeName == 'node-a'
|
spec.spec.nodeName == 'node-a'
|
||||||
spec.spec.restartPolicy == 'Never'
|
spec.spec.restartPolicy == 'Never'
|
||||||
|
|
||||||
def container = spec.spec.containers[0]
|
def container = spec.spec.containers[0]
|
||||||
container.name == 'nf-init-run-foo-node-a'
|
container.name == 'nf-init-node-a'
|
||||||
container.image == 'ubuntu:latest'
|
container.image == 'ubuntu:latest'
|
||||||
container.command == ['/bin/bash', '-c', 'echo init']
|
container.command == ['/bin/bash', '-c', 'echo init']
|
||||||
container.securityContext.privileged == true
|
container.securityContext.privileged == true
|
||||||
@@ -66,11 +66,11 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
then:
|
then:
|
||||||
1 * client.podCreate({ Map spec ->
|
1 * client.podCreate({ Map spec ->
|
||||||
spec.kind == 'Pod'
|
spec.kind == 'Pod'
|
||||||
spec.metadata.name == 'nf-init-run-foo-node-b'
|
spec.metadata.name == 'nf-init-node-b'
|
||||||
spec.spec.nodeName == 'node-b'
|
spec.spec.nodeName == 'node-b'
|
||||||
|
|
||||||
def container = spec.spec.containers[0]
|
def container = spec.spec.containers[0]
|
||||||
container.name == 'nf-init-run-foo-node-b'
|
container.name == 'nf-init-node-b'
|
||||||
container.image == 'ubuntu:latest'
|
container.image == 'ubuntu:latest'
|
||||||
container.command == ['/bin/bash', '-c', 'echo init']
|
container.command == ['/bin/bash', '-c', 'echo init']
|
||||||
container.securityContext.privileged == true
|
container.securityContext.privileged == true
|
||||||
@@ -82,13 +82,12 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
def 'should lowercase and truncate generated pod names' () {
|
def 'should lowercase and truncate generated pod names' () {
|
||||||
given:
|
given:
|
||||||
def client = Mock(K8sClient)
|
def client = Mock(K8sClient)
|
||||||
def runName = 'run-name-with-a-very-long-identifier'
|
|
||||||
def config = new K8sConfig(nodeInit: [
|
def config = new K8sConfig(nodeInit: [
|
||||||
enabled: true,
|
enabled: true,
|
||||||
image: 'ubuntu:latest',
|
image: 'ubuntu:latest',
|
||||||
command: ['true']
|
command: ['true']
|
||||||
])
|
])
|
||||||
def deployer = new K8sNodeInitDeployer(client, config, runName)
|
def deployer = new K8sNodeInitDeployer(client, config)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
deployer.deploy()
|
deployer.deploy()
|
||||||
@@ -96,16 +95,16 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
then:
|
then:
|
||||||
1 * client.nodeList() >> [
|
1 * client.nodeList() >> [
|
||||||
items: [
|
items: [
|
||||||
[metadata: [name: 'NODE-WITH-A-VERY-LONG-NAME-ABCDEFGHIJKLMNOPQRSTUVWXYZ']]
|
[metadata: [name: 'NODE-WITH-A-VERY-LONG-NAME-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123']]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
then:
|
then:
|
||||||
1 * client.podCreate({ Map spec ->
|
1 * client.podCreate({ Map spec ->
|
||||||
spec.metadata.name == 'nf-init-run-name-with-a-very-long-identifier-node-with-a-very-l'
|
spec.metadata.name == 'nf-init-node-with-a-very-long-name-abcdefghijklmnopqrstuvwxyz-0'
|
||||||
spec.metadata.name.size() == 63
|
spec.metadata.name.size() == 63
|
||||||
spec.metadata.name == spec.metadata.name.toLowerCase()
|
spec.metadata.name == spec.metadata.name.toLowerCase()
|
||||||
spec.spec.nodeName == 'NODE-WITH-A-VERY-LONG-NAME-ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
spec.spec.nodeName == 'NODE-WITH-A-VERY-LONG-NAME-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123'
|
||||||
})
|
})
|
||||||
|
|
||||||
then:
|
then:
|
||||||
@@ -116,7 +115,7 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
given:
|
given:
|
||||||
def client = Mock(K8sClient)
|
def client = Mock(K8sClient)
|
||||||
def config = new K8sConfig(nodeInit: [enabled: false, cleanup: true])
|
def config = new K8sConfig(nodeInit: [enabled: false, cleanup: true])
|
||||||
def deployer = new K8sNodeInitDeployer(client, config, 'run-foo')
|
def deployer = new K8sNodeInitDeployer(client, config)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
deployer.cleanup()
|
deployer.cleanup()
|
||||||
@@ -130,7 +129,7 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
given:
|
given:
|
||||||
def client = Mock(K8sClient)
|
def client = Mock(K8sClient)
|
||||||
def config = new K8sConfig(nodeInit: [enabled: true, cleanup: false])
|
def config = new K8sConfig(nodeInit: [enabled: true, cleanup: false])
|
||||||
def deployer = new K8sNodeInitDeployer(client, config, 'run-foo')
|
def deployer = new K8sNodeInitDeployer(client, config)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
deployer.cleanup()
|
deployer.cleanup()
|
||||||
@@ -144,7 +143,7 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
given:
|
given:
|
||||||
def client = Mock(K8sClient)
|
def client = Mock(K8sClient)
|
||||||
def config = new K8sConfig(nodeInit: [enabled: true, cleanup: true])
|
def config = new K8sConfig(nodeInit: [enabled: true, cleanup: true])
|
||||||
def deployer = new K8sNodeInitDeployer(client, config, 'run-foo')
|
def deployer = new K8sNodeInitDeployer(client, config)
|
||||||
|
|
||||||
when:
|
when:
|
||||||
deployer.cleanup()
|
deployer.cleanup()
|
||||||
@@ -158,8 +157,8 @@ class K8sNodeInitDeployerTest extends Specification {
|
|||||||
]
|
]
|
||||||
|
|
||||||
then:
|
then:
|
||||||
1 * client.podDelete('nf-init-run-foo-node-a')
|
1 * client.podDelete('nf-init-node-a')
|
||||||
1 * client.podDelete('nf-init-run-foo-node-b')
|
1 * client.podDelete('nf-init-node-b')
|
||||||
0 * client._
|
0 * client._
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user