Skip to content

Commit 1088bfd

Browse files
committed
First version of multi node tasks
1 parent 89d6f84 commit 1088bfd

45 files changed

Lines changed: 1257 additions & 277 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/hyperqueue/src/client/commands/submit/command.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{fs, io};
66
use anyhow::anyhow;
77
use bstr::BString;
88
use clap::Parser;
9-
use tako::common::resources::{CpuRequest, GenericResourceAmount};
9+
use tako::common::resources::{CpuRequest, GenericResourceAmount, NumOfNodes};
1010
use tako::messages::common::{ProgramDefinition, StdioDef};
1111
use tako::messages::gateway::{GenericResourceRequest, ResourceRequest};
1212

@@ -134,6 +134,18 @@ impl From<PinModeArg> for PinMode {
134134
*/
135135
#[derive(Parser)]
136136
pub struct SubmitJobConfOpts {
137+
/// Number of nodes; 0
138+
/// [default: 0]
139+
#[clap(
140+
long,
141+
conflicts_with("pin"),
142+
conflicts_with("cpus"),
143+
conflicts_with("time-request")
144+
)]
145+
nodes: Option<NumOfNodes>,
146+
/* Other resource configurations is not yet supported in combination of nodes,
147+
remove conflict_with as support is done
148+
*/
137149
/// Number and placement of CPUs for each job
138150
/// [default: 1]
139151
#[clap(long)]
@@ -241,6 +253,7 @@ impl SubmitJobConfOpts {
241253
};
242254

243255
SubmitJobConfOpts {
256+
nodes: self.nodes.or(other.nodes),
244257
cpus: self.cpus.or(other.cpus),
245258
resource,
246259
time_request: self.time_request.or(other.time_request),
@@ -330,6 +343,7 @@ impl JobSubmitOpts {
330343
.collect();
331344

332345
ResourceRequest {
346+
n_nodes: self.conf.nodes.unwrap_or(0),
333347
cpus: self
334348
.conf
335349
.cpus
@@ -407,6 +421,7 @@ pub async fn submit_computation(
407421
directives: _,
408422
conf:
409423
SubmitJobConfOpts {
424+
nodes: _,
410425
cpus: _,
411426
resource: _,
412427
time_request: _,
@@ -462,6 +477,13 @@ pub async fn submit_computation(
462477
stdin,
463478
};
464479

480+
// Force task_dir for multi node tasks (for a place where to create node file)
481+
let task_dir = if resources.n_nodes > 0 {
482+
true
483+
} else {
484+
task_dir
485+
};
486+
465487
let task_desc = TaskDescription {
466488
program: program_def,
467489
resources,

crates/hyperqueue/src/client/output/cli.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::{JobId, JobTaskCount, WorkerId};
2323
use chrono::{DateTime, Local, SubsecRound, Utc};
2424
use core::time::Duration;
2525
use humantime::format_duration;
26+
use std::borrow::Cow;
2627

2728
use std::path::Path;
2829
use std::time::SystemTime;
@@ -169,12 +170,12 @@ impl CliOutput {
169170
.iter()
170171
.filter_map(|t: &JobTaskInfo| match &t.state {
171172
JobTaskState::Failed {
172-
started_data: StartedTaskData { worker_id, .. },
173+
started_data: StartedTaskData { worker_ids, .. },
173174
error,
174175
..
175176
} => Some(vec![
176177
t.task_id.cell(),
177-
format_worker(*worker_id, worker_map).cell(),
178+
format_workers(worker_ids, worker_map).cell(),
178179
error.to_owned().cell().foreground_color(Some(Color::Red)),
179180
]),
180181
_ => None,
@@ -573,9 +574,9 @@ impl Output for CliOutput {
573574
job_rows.append(&mut vec![
574575
task.task_id.cell().justify(Justify::Right),
575576
task_status_to_cell(get_task_status(&task.state)),
576-
match task.state.get_worker() {
577-
Some(worker) => format_worker(worker, &worker_map),
578-
_ => "",
577+
match task.state.get_workers() {
578+
Some(workers) => format_workers(workers, &worker_map),
579+
_ => "".into(),
579580
}
580581
.cell(),
581582
multiline_cell(vec![
@@ -960,6 +961,9 @@ fn task_status_to_cell(status: Status) -> CellStruct {
960961
}
961962

962963
fn format_resource_request(rq: &ResourceRequest) -> String {
964+
if rq.n_nodes > 0 {
965+
return format!("nodes: {}", rq.n_nodes);
966+
}
963967
let mut result = format_cpu_request(&rq.cpus);
964968
for grq in &rq.generic {
965969
result.push_str(&format!("\n{}: {}", grq.resource, grq.amount))
@@ -987,14 +991,15 @@ pub fn format_job_workers(tasks: &[JobTaskInfo], worker_map: &WorkerMap) -> Stri
987991
// BTreeSet is used to both filter duplicates and keep a stable order
988992
let worker_set: BTreeSet<_> = tasks
989993
.iter()
990-
.filter_map(|task| task.state.get_worker())
994+
.filter_map(|task| task.state.get_workers())
995+
.flatten()
991996
.collect();
992997
let worker_count = worker_set.len();
993998

994999
let mut result = worker_set
9951000
.into_iter()
9961001
.take(MAX_DISPLAYED_WORKERS)
997-
.map(|id| format_worker(id, worker_map))
1002+
.map(|id| format_worker(*id, worker_map))
9981003
.collect::<Vec<_>>()
9991004
.join(", ");
10001005

@@ -1012,6 +1017,21 @@ fn format_worker(id: WorkerId, worker_map: &WorkerMap) -> &str {
10121017
.unwrap_or_else(|| "N/A")
10131018
}
10141019

1020+
fn format_workers<'a>(ids: &[WorkerId], worker_map: &'a WorkerMap) -> Cow<'a, str> {
1021+
if ids.len() == 1 {
1022+
format_worker(ids[0], worker_map).into()
1023+
} else {
1024+
assert!(!ids.is_empty());
1025+
let mut result = String::new();
1026+
//result.push_str(format_worker(ids[0], worker_map));
1027+
for id in ids {
1028+
result.push_str(format_worker(*id, worker_map));
1029+
result.push('\n');
1030+
}
1031+
result.into()
1032+
}
1033+
}
1034+
10151035
fn get_task_time(state: &JobTaskState) -> (Option<DateTime<Utc>>, Option<DateTime<Utc>>) {
10161036
match state {
10171037
JobTaskState::Canceled {

crates/hyperqueue/src/client/output/json.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ impl Output for JsonOutput {
115115
},
116116
resources:
117117
ResourceRequest {
118+
n_nodes,
118119
cpus,
119120
generic,
120121
min_time,
@@ -135,6 +136,7 @@ impl Output for JsonOutput {
135136
"stdout": format_stdio_def(&stdout),
136137
});
137138
json["resources"] = json!({
139+
"n_nodes": n_nodes,
138140
"cpus": format_cpu_request(cpus),
139141
"generic": generic,
140142
"min_time": format_duration(min_time)
@@ -225,7 +227,16 @@ impl Output for JsonOutput {
225227

226228
fn fill_task_started_data(dict: &mut Value, data: StartedTaskData) {
227229
dict["started_at"] = format_datetime(data.start_date);
228-
dict["worker"] = data.worker_id.as_num().into();
230+
if data.worker_ids.len() == 1 {
231+
dict["worker"] = data.worker_ids[0].as_num().into();
232+
} else {
233+
dict["workers"] = data
234+
.worker_ids
235+
.iter()
236+
.map(|worker_id| worker_id.as_num().into())
237+
.collect::<Vec<Value>>()
238+
.into();
239+
}
229240
}
230241

231242
fn fill_task_paths(dict: &mut Value, map: &TaskToPathsMap, task_id: JobTaskId) {

crates/hyperqueue/src/common/env.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ pub const HQ_PIN: &str = create_hq_env!("PIN");
2222
pub const HQ_TASK_DIR: &str = create_hq_env!("TASK_DIR");
2323
pub const HQ_ERROR_FILENAME: &str = create_hq_env!("ERROR_FILENAME");
2424
pub const HQ_CPUS: &str = create_hq_env!("CPUS");
25+
pub const HQ_NODE_FILE: &str = create_hq_env!("NODE_FILE");

crates/hyperqueue/src/server/autoalloc/process.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,7 @@ mod tests {
15841584
cwd: Default::default(),
15851585
};
15861586
let resources = ResourceRequest {
1587+
n_nodes: 0,
15871588
cpus: Default::default(),
15881589
generic: vec![],
15891590
min_time,

crates/hyperqueue/src/server/client/submit.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ mod tests {
478478
cwd: Default::default(),
479479
},
480480
resources: ResourceRequest {
481+
n_nodes: 0,
481482
cpus: CpuRequest::Compact(cpu_count),
482483
generic: vec![GenericResourceRequest {
483484
resource: "a".to_string(),

crates/hyperqueue/src/server/job.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::transfer::messages::{
99
use crate::worker::start::RunningTaskContext;
1010
use crate::{JobId, JobTaskCount, JobTaskId, Map, TakoTaskId, WorkerId};
1111
use chrono::{DateTime, Utc};
12+
use smallvec::SmallVec;
1213
use std::path::PathBuf;
1314
use tako::common::index::ItemId;
1415
use tako::common::Set;
@@ -23,7 +24,7 @@ use tokio::sync::oneshot;
2324
pub struct StartedTaskData {
2425
pub start_date: DateTime<Utc>,
2526
pub context: RunningTaskContext,
26-
pub worker_id: WorkerId,
27+
pub worker_ids: SmallVec<[WorkerId; 1]>,
2728
}
2829

2930
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -58,8 +59,8 @@ impl JobTaskState {
5859
}
5960
}
6061

61-
pub fn get_worker(&self) -> Option<WorkerId> {
62-
self.started_data().map(|data| data.worker_id)
62+
pub fn get_workers(&self) -> Option<&[WorkerId]> {
63+
self.started_data().map(|data| data.worker_ids.as_slice())
6364
}
6465
}
6566

@@ -274,7 +275,7 @@ impl Job {
274275
pub fn set_running_state(
275276
&mut self,
276277
tako_task_id: TakoTaskId,
277-
worker: WorkerId,
278+
workers: SmallVec<[WorkerId; 1]>,
278279
context: SerializedTaskContext,
279280
) {
280281
let (_, state) = self.get_task_state_mut(tako_task_id);
@@ -287,7 +288,7 @@ impl Job {
287288
started_data: StartedTaskData {
288289
start_date: Utc::now(),
289290
context,
290-
worker_id: worker,
291+
worker_ids: workers,
291292
},
292293
};
293294
self.counters.n_running_tasks += 1;

crates/hyperqueue/src/server/state.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,16 @@ impl State {
206206
log::debug!("Task id={} updated {:?}", msg.id, msg.state);
207207
let (mut job_id, mut is_job_terminated): (Option<JobId>, bool) = (None, false);
208208
match msg.state {
209-
TaskState::Running { worker_id, context } => {
209+
TaskState::Running {
210+
worker_ids,
211+
context,
212+
} => {
210213
let job = self.get_job_mut_by_tako_task_id(msg.id).unwrap();
211-
job.set_running_state(msg.id, worker_id, context);
212-
self.event_storage.on_task_started(msg.id, worker_id);
214+
job.set_running_state(msg.id, worker_ids.clone(), context);
215+
216+
// TODO: Prepare it for multi-node tasks
217+
// This (incomplete) version just takes the first worker as "the worker" for task
218+
self.event_storage.on_task_started(msg.id, worker_ids[0]);
213219
}
214220
TaskState::Finished => {
215221
let job = self.get_job_mut_by_tako_task_id(msg.id).unwrap();

crates/hyperqueue/src/worker/start.rs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
use std::fmt::Write;
21
use std::fs::File;
32
use std::io;
4-
use std::io::{ErrorKind, Read};
3+
use std::io::{BufWriter, ErrorKind, Read, Write};
54
use std::path::{Path, PathBuf};
65
use std::process::ExitStatus;
76
use std::rc::Rc;
@@ -21,10 +20,10 @@ use tako::worker::launcher::{command_from_definitions, TaskLaunchData, TaskLaunc
2120
use tako::worker::state::WorkerState;
2221
use tako::worker::task::Task;
2322
use tako::worker::taskenv::{StopReason, TaskResult};
24-
use tako::{InstanceId, TaskId};
23+
use tako::{InstanceId, TaskId, WorkerId};
2524

2625
use crate::common::env::{
27-
HQ_CPUS, HQ_ERROR_FILENAME, HQ_INSTANCE_ID, HQ_PIN, HQ_SUBMIT_DIR, HQ_TASK_DIR,
26+
HQ_CPUS, HQ_ERROR_FILENAME, HQ_INSTANCE_ID, HQ_NODE_FILE, HQ_PIN, HQ_SUBMIT_DIR, HQ_TASK_DIR,
2827
};
2928
use crate::common::placeholders::{
3029
fill_placeholders_in_paths, CompletePlaceholderCtx, ResolvablePaths,
@@ -107,12 +106,23 @@ impl TaskLauncher for HqTaskLauncher {
107106
.to_string()
108107
.into(),
109108
);
109+
if !task.node_list.is_empty() {
110+
let filename = task_dir.path().join("hq-nodelist");
111+
write_node_file(state, &task.node_list, &filename)?;
112+
program.env.insert(
113+
HQ_NODE_FILE.into(),
114+
filename.to_string_lossy().to_string().into(),
115+
);
116+
}
110117
Some(task_dir)
111118
} else {
112119
None
113120
};
114121

115-
insert_resources_into_env(state, task, allocation, &mut program);
122+
// Do not insert resources for multi-node tasks, semantics has to be cleared
123+
if task.node_list.is_empty() {
124+
insert_resources_into_env(state, task, allocation, &mut program);
125+
}
116126

117127
let submit_dir: PathBuf = program.env[<&BStr>::from(HQ_SUBMIT_DIR)].to_string().into();
118128
program
@@ -195,6 +205,21 @@ fn get_custom_error_filename(task_dir: &TempDir) -> PathBuf {
195205
task_dir.path().join("hq-error")
196206
}
197207

208+
fn write_node_file(
209+
state: &WorkerState,
210+
node_list: &[WorkerId],
211+
path: &Path,
212+
) -> std::io::Result<()> {
213+
let file = File::create(path)?;
214+
let mut file = BufWriter::new(file);
215+
for worker_id in node_list {
216+
file.write_all(state.worker_hostname(*worker_id).unwrap().as_bytes())?;
217+
file.write_all(b"\n")?;
218+
}
219+
file.flush()?;
220+
Ok(())
221+
}
222+
198223
fn insert_resources_into_env(
199224
state: &WorkerState,
200225
task: &Task,
@@ -326,6 +351,7 @@ async fn run_task(
326351

327352
/// Provide a more detailed error message when a process fails to be spawned.
328353
fn map_spawn_error(error: std::io::Error, program: &ProgramDefinition) -> tako::Error {
354+
use std::fmt::Write;
329355
let context = match &error.kind() {
330356
ErrorKind::NotFound => {
331357
let file = &program.args[0];

crates/tako/benches/benchmarks/scheduler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ impl Comm for NullComm {
9191
fn send_client_task_started(
9292
&mut self,
9393
_task_id: TaskId,
94-
_worker_id: WorkerId,
94+
_worker_id: &[WorkerId],
9595
_context: SerializedTaskContext,
9696
) {
9797
}

0 commit comments

Comments
 (0)