Compare commits

..

2 Commits

Author SHA1 Message Date
7b9188ab54 It works 2026-07-10 22:19:33 +02:00
ca45b4069f towards experiments 2026-07-10 21:27:36 +02:00
9 changed files with 92 additions and 31 deletions

View File

@@ -5,6 +5,7 @@ import groovy.util.logging.Slf4j
import java.net.http.HttpClient import java.net.http.HttpClient
import java.net.http.HttpRequest import java.net.http.HttpRequest
import java.net.http.HttpResponse import java.net.http.HttpResponse
import java.nio.channels.UnresolvedAddressException
/** /**
* The DVFS client uses the deployed DVFS agents to control the operating frequency of the worker nodes. * The DVFS client uses the deployed DVFS agents to control the operating frequency of the worker nodes.
@@ -12,9 +13,15 @@ import java.net.http.HttpResponse
@Slf4j @Slf4j
class K8sDVFSClient { class K8sDVFSClient {
private HttpClient httpClient private HttpClient httpClient
private Map<String, String> ipTable
K8sDVFSClient() { K8sDVFSClient(String[] nodes, String[] ips) {
assert(nodes.length == ips.length)
this.httpClient = HttpClient.newBuilder().build() this.httpClient = HttpClient.newBuilder().build()
this.ipTable = new HashMap<>()
for (int i = 0; i < nodes.length; i++) {
this.ipTable.put(nodes[i], ips[i])
}
} }
OptionalLong getNodeCurrentFrequency(String node) { OptionalLong getNodeCurrentFrequency(String node) {
@@ -62,28 +69,34 @@ class K8sDVFSClient {
} }
OptionalLong getMemoryAmount(String node) { OptionalLong getMemoryAmount(String node) {
return getInt(HttpRequest.newBuilder() return getLong(HttpRequest.newBuilder()
.uri(new URI("http://${agentAddress(node)}/mem/amount")) .uri(new URI("http://${agentAddress(node)}/mem/amount"))
.GET() .GET()
.build()) .build())
} }
private OptionalLong getLong(HttpRequest request) { private OptionalLong getLong(HttpRequest request) {
try {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
log.error("Request GET ${request.uri().toString()} returned ${response.statusCode()}") log.error("Request GET ${request.uri().toString()} returned ${response.statusCode()}")
return OptionalLong.empty() return OptionalLong.empty()
} }
try {
long parsed = Long.parseLong(response.body()) long parsed = Long.parseLong(response.body())
return OptionalLong.of(parsed) return OptionalLong.of(parsed)
} catch (NumberFormatException ex) { } catch (NumberFormatException ex) {
log.error("Unexpected response ${response.body()} - ${ex.message}") log.error("Unexpected response ${response.body()} - ${ex.message}")
return OptionalLong.empty() return OptionalLong.empty()
} catch (ConnectException ex) {
log.error("Request failed ${request.uri().toString()}: ${ex.message}")
return OptionalLong.empty()
} catch (UnresolvedAddressException ex) {
log.error("Failed to resolve address ${request.uri().toString()}: ${ex.message}")
return OptionalLong.empty()
} }
} }
private String agentAddress(String node) { private String agentAddress(String node) {
return K8sNodeInitDeployer.buildPodName(node) return ipTable.get(node) + ":8080"
} }
} }

View File

@@ -99,17 +99,24 @@ class K8sExecutor extends Executor implements ExtensionPoint {
this.runtimeRecorder = new K8sRuntimeRecorder(k8sConfig.recordTaskRuntimes, k8sConfig.runtimeRecordPath) this.runtimeRecorder = new K8sRuntimeRecorder(k8sConfig.recordTaskRuntimes, k8sConfig.runtimeRecordPath)
this.runtimeEstimator = new K8sRuntimeEstimator(k8sConfig.runtimeRecordPath) this.runtimeEstimator = new K8sRuntimeEstimator(k8sConfig.runtimeRecordPath)
String[] nodes = getNodeList()
K8sSchedulingStrategy strategy = null K8sSchedulingStrategy strategy = null
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(this.runtimeEstimator, new K8sDVFSClient()) String[] ips = new String[nodes.length]
for (int i = 0; i < nodes.length; i++) {
ips[i] = client.getPodIpAddress(K8sNodeInitDeployer.buildPodName(nodes[i]))
log.info "[K8s] node ${nodes[i]} -> ${ips[i]}"
}
strategy = new K8sDVFSSchedulingStrategy(this.runtimeEstimator, new K8sDVFSClient(nodes, ips))
} 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()
} }
this.taskScheduler = new K8sTaskScheduler(getNodeList(), strategy) this.taskScheduler = new K8sTaskScheduler(nodes, strategy, k8sConfig.schedulerInterval)
this.schedulerThread = new Thread(this.taskScheduler) this.schedulerThread = new Thread(this.taskScheduler)
this.schedulerThread.start() this.schedulerThread.start()
} }

View File

@@ -136,6 +136,7 @@ exit 0
.withPrivileged(true) .withPrivileged(true)
.withHostMounts(mounts) .withHostMounts(mounts)
.withPodName(buildPodName(nodeName)) .withPodName(buildPodName(nodeName))
.withPort(8080)
.build() .build()
} }

View File

@@ -34,7 +34,7 @@ class K8sRuntimeEstimator {
K8sRuntimeEstimator(String dataFilePath) { K8sRuntimeEstimator(String dataFilePath) {
HashMap<String, ArrayList<Tuple2<Long, Long>>> data = new HashMap<>(); HashMap<String, ArrayList<Tuple2<Long, Long>>> data = new HashMap<>();
try { try {
BufferedReader reader = new BufferedReader(dataFilePath) BufferedReader reader = new BufferedReader(new FileReader(dataFilePath))
String line = reader.readLine() String line = reader.readLine()
while (line != null) { while (line != null) {
// <task-name>,<input-size>,<runtime-ms> // <task-name>,<input-size>,<runtime-ms>
@@ -91,7 +91,7 @@ class K8sRuntimeEstimator {
double estimate(String taskName, long inputSize) { double estimate(String taskName, long inputSize) {
Function estimator = estimators.get(taskName) Function estimator = estimators.get(taskName)
if (estimator == null) { if (estimator == null) {
log.warn "[K8s] Unable to estimate take ${taskName}: unknown task" //log.warn "[K8s] Unable to estimate take ${taskName}: unknown task"
return Double.POSITIVE_INFINITY return Double.POSITIVE_INFINITY
} }
return estimator.estimate(inputSize) return estimator.estimate(inputSize)

View File

@@ -4,20 +4,21 @@ import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j import groovy.util.logging.Slf4j
import nextflow.k8s.client.K8sClient import nextflow.k8s.client.K8sClient
import nextflow.util.Duration
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
@Slf4j @Slf4j
@CompileStatic @CompileStatic
class K8sTaskScheduler implements Runnable { class K8sTaskScheduler implements Runnable {
private final K8sExecutor executor private final Duration interval
private final K8sSchedulingStrategy strategy private final K8sSchedulingStrategy strategy
private final LinkedBlockingQueue<K8sSchedulingRequest> queue = new LinkedBlockingQueue<>() private final LinkedBlockingQueue<K8sSchedulingRequest> queue = new LinkedBlockingQueue<>()
private String[] nodes private String[] nodes
private synchronized boolean shouldStop private synchronized boolean shouldStop
K8sTaskScheduler(String[] nodes, K8sSchedulingStrategy strategy) { K8sTaskScheduler(String[] nodes, K8sSchedulingStrategy strategy, Duration interval) {
this.executor = executor this.interval = interval
this.strategy = strategy this.strategy = strategy
this.nodes = nodes this.nodes = nodes
} }
@@ -73,7 +74,7 @@ class K8sTaskScheduler implements Runnable {
* */ * */
void run() { void run() {
this.shouldStop = false this.shouldStop = false
def interval = this.executor.getK8sConfig().schedulerInterval final interval = this.interval
while (!shouldStop) { while (!shouldStop) {
sleep(interval.toMillis()) sleep(interval.toMillis())
drain() drain()

View File

@@ -332,6 +332,12 @@ class K8sClient {
return new K8sResponseJson(resp.text) return new K8sResponseJson(resp.text)
} }
String getPodIpAddress(String podName) {
assert podName
final K8sResponseJson resp = podStatus0(podName)
(resp?.status as Map)?.podIP as String
}
/* /*
* https://v1-8.docs.kubernetes.io/docs/api-reference/v1.8/#read-status-69 * https://v1-8.docs.kubernetes.io/docs/api-reference/v1.8/#read-status-69
*/ */

View File

@@ -128,6 +128,8 @@ class PodSpecBuilder {
String nodeName String nodeName
Integer port = null
/** /**
* @return A sequential volume unique identifier * @return A sequential volume unique identifier
*/ */
@@ -406,6 +408,11 @@ class PodSpecBuilder {
return this return this
} }
PodSpecBuilder withPort(int value) {
this.port = new Integer(value)
return this
}
@PackageScope List<Map> createPullSecret() { @PackageScope List<Map> createPullSecret() {
def result = new ArrayList(1) def result = new ArrayList(1)
def entry = new LinkedHashMap(1) def entry = new LinkedHashMap(1)
@@ -443,6 +450,12 @@ class PodSpecBuilder {
if( imagePullPolicy ) if( imagePullPolicy )
container.imagePullPolicy = imagePullPolicy container.imagePullPolicy = imagePullPolicy
if( this.port ) {
List<Map> ports = []
ports << [containerPort: this.port.intValue()]
container.ports = ports
}
final secContext = new LinkedHashMap(10) final secContext = new LinkedHashMap(10)
if( privileged ) { if( privileged ) {
// note: privileged flag needs to be defined in the *container* securityContext // note: privileged flag needs to be defined in the *container* securityContext

View File

@@ -20,6 +20,15 @@ import nextflow.util.ArrayTuple
@CompileStatic @CompileStatic
class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy { class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy {
private static long getTaskMemoryRequirment(TaskRun task) {
return task.config.getMemory() ? task.config.getMemory().bytes : 64 * 1024 * 1024
}
private static int getTaskCPURequirement(TaskRun task) {
return task.config.hasCpus() ? task.config.getCpus() : 1
}
@Slf4j
private class WorkerNode { private class WorkerNode {
private class AssignedTask { private class AssignedTask {
TaskRun task TaskRun task
@@ -73,24 +82,29 @@ class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy {
for (AssignedTask t : tasks) { for (AssignedTask t : tasks) {
max = Long.max(t.frequency, max) max = Long.max(t.frequency, max)
} }
log.info "[K8s] node ${name} running at ${max} Hz / ${maxFrequency} Hz ${((double)max/(double)maxFrequency) * 100.0}%"
dvfsClient.setNodeFrequency(name, (int)max) dvfsClient.setNodeFrequency(name, (int)max)
this.currentFrequency = max this.currentFrequency = max
} }
void assignTask(TaskRun task, long frequency, K8sDVFSClient dvfsClient) { void assignTask(TaskRun task, long frequency, K8sDVFSClient dvfsClient) {
final long reqBytes = task.config.getMemory().bytes final long reqBytes = getTaskMemoryRequirment(task)
final int reqCPUs = task.config.hasCpus() ? task.config.getCpus() : 1 final int reqCPUs = getTaskCPURequirement(task)
this.allocatedMemory += reqBytes this.allocatedMemory += reqBytes
this.allocatedCPUs += reqCPUs this.allocatedCPUs += reqCPUs
log.info "[K8s] node ${name}: task ${task.name} assigned, now have ${availableMemory} bytes and ${availableCPUs} cpus [${reqBytes}, ${reqCPUs}]: at ${frequency}/${maxFrequency}"
this.tasks.add(new AssignedTask(task, frequency)) this.tasks.add(new AssignedTask(task, frequency))
updateFrequency(dvfsClient) updateFrequency(dvfsClient)
} }
void taskFinished(TaskRun task, K8sDVFSClient dvfsClient) { void taskFinished(TaskRun task, K8sDVFSClient dvfsClient) {
final long reqBytes = task.config.getMemory().bytes final long reqBytes = getTaskMemoryRequirment(task)
final int reqCPUs = task.config.hasCpus() ? task.config.getCpus() : 1 final int reqCPUs = getTaskCPURequirement(task)
this.allocatedMemory -= reqBytes this.allocatedMemory -= reqBytes
this.allocatedCPUs -= reqCPUs this.allocatedCPUs -= reqCPUs
log.info "[K8s] node ${name}: task ${task.name} finished, now have ${availableMemory} bytes and ${availableCPUs} cpus [${reqBytes}, ${reqCPUs}]"
this.tasks.removeIf {it.task == task} this.tasks.removeIf {it.task == task}
updateFrequency(dvfsClient) updateFrequency(dvfsClient)
} }
@@ -176,7 +190,7 @@ class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy {
ArrayList<WorkerNode> suitableNodes = filterNodes(req.task) ArrayList<WorkerNode> suitableNodes = filterNodes(req.task)
if (suitableNodes.size() == 0) { if (suitableNodes.size() == 0) {
if (!anyNode(req.task)) { if (!anyNode(req.task)) {
log.error "[K8s] unable to schedule task ${req.task} - no node satisfies resource requirements" //log.error "[K8s] unable to schedule task ${req.task} - no node satisfies resource requirements"
return null return null
} }
/* No node can currently execute this task, but it should be possible in the future */ /* No node can currently execute this task, but it should be possible in the future */
@@ -244,6 +258,8 @@ class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy {
continue continue
} }
globalMaxFrequency = Long.min(globalMaxFrequency, max.asLong) globalMaxFrequency = Long.min(globalMaxFrequency, max.asLong)
log.info "[K8s] node ${node}: ${cpus.asLong} CPUs, ${mem.asLong} bytes RAM ${min.asLong} Hz - ${max.asLong} Hz current ${cur.asLong}"
this.nodes.add(new WorkerNode(node, max.asLong, min.asLong, cur.asLong, cpus.asLong, mem.asLong)) this.nodes.add(new WorkerNode(node, max.asLong, min.asLong, cur.asLong, cpus.asLong, mem.asLong))
} }
return !this.nodes.isEmpty() return !this.nodes.isEmpty()
@@ -252,19 +268,21 @@ class K8sDVFSSchedulingStrategy implements K8sSchedulingStrategy {
/* Returns a list of nodes that fulfill the tasks resource requirements /* Returns a list of nodes that fulfill the tasks resource requirements
*/ */
private ArrayList<WorkerNode> filterNodes(TaskRun task) { private ArrayList<WorkerNode> filterNodes(TaskRun task) {
final long reqBytes = task.config.getMemory().bytes final long reqBytes = getTaskMemoryRequirment(task)
final int reqCPUs = task.config.hasCpus() ? task.config.getCpus() : 1 final int reqCPUs = getTaskCPURequirement(task)
ArrayList<WorkerNode> suitableNodes = new ArrayList<>() ArrayList<WorkerNode> suitableNodes = new ArrayList<>()
for (WorkerNode n : nodes) { for (WorkerNode n : nodes) {
if (n.availableMemory >= reqBytes && n.availableCPUs >= reqCPUs) if (n.availableMemory > reqBytes && n.availableCPUs > reqCPUs) {
log.info "[K8s] task ${task.name}: ${reqBytes} bytes ${reqCPUs} cpus: node ${n.name} has ${n.availableMemory} bytes, ${n.availableCPUs} cpus"
suitableNodes.add(n) suitableNodes.add(n)
} }
}
return suitableNodes return suitableNodes
} }
private boolean anyNode(TaskRun task) { private boolean anyNode(TaskRun task) {
final long reqBytes = task.config.getMemory().bytes final long reqBytes = getTaskMemoryRequirment(task)
final int reqCPUs = task.config.hasCpus() ? task.config.getCpus() : 1 final int reqCPUs = getTaskCPURequirement(task)
for (WorkerNode n : nodes) { for (WorkerNode n : nodes) {
if (n.memoryAmount >= reqBytes && n.availableCPUs >= reqCPUs) if (n.memoryAmount >= reqBytes && n.availableCPUs >= reqCPUs)
return true return true

View File

@@ -22,16 +22,18 @@ k8s {
projectDir = '/workspace/projects' projectDir = '/workspace/projects'
cleanup = false cleanup = false
nextflowImage = 'gitea.kleine.eulenhexe.de/kevin/ma/nextflow-dvfs:0.5.5' nextflowImage = 'gitea.kleine.eulenhexe.de/kevin/ma/nextflow-dvfs:0.6.18'
imagePullPolicy = 'IfNotPresent' imagePullPolicy = 'IfNotPresent'
schedulerInterval = '10s' schedulerInterval = '10s'
recordTaskRuntimes = true recordTaskRuntimes = true
schedulingStrategy = 'DVFS'
nodeInit { nodeInit {
enabled = false enabled = true
image = 'gitea.kleine.eulenhexe.de/kevin/ma/dvfs-agent:0.1' image = 'gitea.kleine.eulenhexe.de/kevin/ma/dvfs-agent:0.2.0'
command = ['/bin/agent'] command = ['/usr/local/bin/agent']
wait = 'Running' wait = 'Running'
cleanup = false cleanup = true
} }
} }