Implement job scheduling challenge

This commit is contained in:
FiveMovesAhead 2025-12-23 23:41:00 +08:00
parent bd333f8f72
commit 91574007eb
18 changed files with 2685 additions and 1 deletions

View File

@ -9,12 +9,14 @@ on:
- 'vector_search/*'
- 'hypergraph/*'
- 'neuralnet_optimizer/*'
- 'job_scheduling/*'
- 'test/satisfiability/*'
- 'test/vehicle_routing/*'
- 'test/knapsack/*'
- 'test/vector_search/*'
- 'test/hypergraph/*'
- 'test/neuralnet_optimizer/*'
- 'test/job_scheduling/*'
jobs:
init:

1
Cargo.lock generated
View File

@ -2067,6 +2067,7 @@ dependencies = [
"ndarray",
"paste",
"rand",
"rand_distr",
"serde",
"serde_json",
"statrs",

View File

@ -100,6 +100,7 @@ f"""Library not found at {so_path}:
"vector_search": "c004",
"hypergraph": "c005",
"neuralnet_optimizer": "c006",
"job_scheduling": "c007",
}
challenge_id = challenge_ids[CHALLENGE]

View File

@ -39,3 +39,5 @@ c005 = ["cudarc", "tig-challenges/c005"]
hypergraph = ["c005"]
c006 = ["cudarc", "tig-challenges/c006"]
neuralnet_optimizer = ["c006"]
c007 = ["tig-challenges/c007"]
job_scheduling = ["c007"]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
# TIG Code Submission
## Submission Details
* **Challenge Name:** job_scheduling
* **Algorithm Name:** [name of submission]
* **Copyright:** [year work created] [name of copyright owner]
* **Identity of Submitter:** [name of person or entity submitting the work to TIG]
* **Identity of Creator of Algorithmic Method:** [if applicable else null]
* **Unique Algorithm Identifier (UAI):** [if applicable else null]
## References and Acknowledgments
*(If this implementation is based on or inspired by existing work, please include citations and acknowledgments below. Remove this section if unused.)*
### 1. Academic Papers
- [Author(s)], *"[Paper Title]"*, DOI: [DOI or URL if available]
### 2. Code References
- [Author(s)] [URL]
### 3. Other
- [Author(s)] [Details or description]
## Additional Notes
*(Include any relevant context, usage notes, or implementation details here. Remove this section if unused.)*
## License
The files in this folder are under the following licenses:
* TIG Benchmarker Outbound License
* TIG Commercial License
* TIG Inbound Game License
* TIG Innovator Outbound Game License
* TIG Open Data License
* TIG THV Game License
Copies of the licenses can be obtained at:
https://github.com/tig-foundation/tig-monorepo/tree/main/docs/licenses

View File

@ -0,0 +1,50 @@
// TIG's UI uses the pattern `tig_challenges::<challenge_name>` to automatically detect your algorithm's challenge
use crate::{seeded_hasher, HashMap, HashSet};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use tig_challenges::job_scheduling::*;
#[derive(Serialize, Deserialize)]
pub struct Hyperparameters {
// Optionally define hyperparameters here. Example:
// pub param1: usize,
// pub param2: f64,
}
pub fn help() {
// Print help information about your algorithm here. It will be invoked with `help_algorithm` script
println!("No help information provided.");
}
pub fn solve_challenge(
challenge: &Challenge,
save_solution: &dyn Fn(&Solution) -> Result<()>,
hyperparameters: &Option<Map<String, Value>>,
) -> Result<()> {
// If you need random numbers, recommend using SmallRng with challenge.seed:
// use rand::{rngs::SmallRng, Rng, SeedableRng};
// let mut rng = SmallRng::from_seed(challenge.seed);
// If you need HashMap or HashSet, make sure to use a deterministic hasher for consistent runtime_signature:
// use crate::{seeded_hasher, HashMap, HashSet};
// let hasher = seeded_hasher(&challenge.seed);
// let map = HashMap::with_hasher(hasher);
// Support hyperparameters if needed:
// let hyperparameters = match hyperparameters {
// Some(hyperparameters) => {
// serde_json::from_value::<Hyperparameters>(Value::Object(hyperparameters.clone()))
// .map_err(|e| anyhow!("Failed to parse hyperparameters: {}", e))?
// }
// None => Hyperparameters { /* set default values here */ },
// };
// use save_solution(&Solution) to save your solution. Overwrites any previous solution
// return Err(<msg>) if your algorithm encounters an error
// return Ok(()) if your algorithm is finished
Err(anyhow!("Not implemented"))
}
// Important! Do not include any tests in this file, it will result in your submission being rejected

View File

@ -36,3 +36,5 @@ c005 = ["cuda", "tig-algorithms/c005", "tig-challenges/c005"]
hypergraph = ["c005"]
c006 = ["cuda", "tig-algorithms/c006", "tig-challenges/c006"]
neuralnet_optimizer = ["c006"]
c007 = ["tig-algorithms/c007", "tig-challenges/c007"]
job_scheduling = ["c007"]

View File

@ -46,8 +46,12 @@ case "$CHALLENGE" in
build_so $ALGORITHM
build_ptx $ALGORITHM
;;
job_scheduling)
echo "Building ALGORITHM '$ALGORITHM' for CHALLENGE 'job_scheduling'"
build_so $ALGORITHM
;;
*)
echo "Error: Invalid CHALLENGE value. Must be one of: satisfiability, knapsack, vehicle_routing, vector_search, hypergraph, neuralnet_optimizer"
echo "Error: Invalid CHALLENGE value. Must be one of: satisfiability, knapsack, vehicle_routing, vector_search, hypergraph, neuralnet_optimizer, job_scheduling"
exit 1
;;
esac

View File

@ -21,6 +21,7 @@ rand = { version = "0.8.5", default-features = false, features = [
"std_rng",
"small_rng",
] }
rand_distr = "0.4.3"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = { version = "1.0.113" }
statrs = { version = "0.18.0" }
@ -39,3 +40,5 @@ c005 = ["cudarc"]
hypergraph = ["c005"]
c006 = ["cudarc", "cudarc/cublas", "cudarc/cudnn"]
neuralnet_optimizer = ["c006"]
c007 = []
job_scheduling = ["c007"]

View File

@ -0,0 +1,177 @@
use crate::job_scheduling::{Challenge, Solution};
use anyhow::{anyhow, Result};
use serde_json::{Map, Value};
use std::collections::HashMap;
fn average_processing_time(operation: &HashMap<usize, u32>) -> f64 {
if operation.is_empty() {
return 0.0;
}
let sum: u32 = operation.values().sum();
sum as f64 / operation.len() as f64
}
fn earliest_end_time(
time: u32,
machine_available_time: &[u32],
operation: &HashMap<usize, u32>,
) -> u32 {
let mut earliest_end = u32::MAX;
for (&machine_id, &proc_time) in operation.iter() {
let start = time.max(machine_available_time[machine_id]);
let end = start + proc_time;
if end < earliest_end {
earliest_end = end;
}
}
earliest_end
}
pub fn solve_challenge(
challenge: &Challenge,
save_solution: &dyn Fn(&Solution) -> Result<()>,
_hyperparameters: &Option<Map<String, Value>>,
) -> Result<()> {
let num_jobs = challenge.num_jobs;
let num_machines = challenge.num_machines;
let mut job_products = Vec::with_capacity(num_jobs);
for (product, count) in challenge.jobs_per_product.iter().enumerate() {
for _ in 0..*count {
job_products.push(product);
}
}
if job_products.len() != num_jobs {
return Err(anyhow!(
"Job count mismatch. Expected {}, got {}",
num_jobs,
job_products.len()
));
}
let mut product_avg_times = Vec::with_capacity(challenge.product_processing_times.len());
for product_ops in challenge.product_processing_times.iter() {
let mut avg_ops = Vec::with_capacity(product_ops.len());
for op in product_ops.iter() {
avg_ops.push(average_processing_time(op));
}
product_avg_times.push(avg_ops);
}
let mut job_ops_len = Vec::with_capacity(num_jobs);
let mut job_remaining_work: Vec<f64> = Vec::with_capacity(num_jobs);
for &product in job_products.iter() {
let avg_ops = &product_avg_times[product];
job_ops_len.push(avg_ops.len());
job_remaining_work.push(avg_ops.iter().sum());
}
let mut job_next_op_idx = vec![0usize; num_jobs];
let mut job_ready_time = vec![0u32; num_jobs];
let mut machine_available_time = vec![0u32; num_machines];
let mut job_schedule = job_ops_len
.iter()
.map(|&ops_len| Vec::with_capacity(ops_len))
.collect::<Vec<_>>();
let mut remaining_ops = job_ops_len.iter().sum::<usize>();
let mut time = 0u32;
let eps = 1e-9_f64;
while remaining_ops > 0 {
let mut available_machines = (0..num_machines)
.filter(|&m| machine_available_time[m] <= time)
.collect::<Vec<usize>>();
available_machines.sort_unstable();
let mut scheduled_any = false;
for &machine in available_machines.iter() {
let mut best_job: Option<usize> = None;
let mut best_priority = -1.0_f64;
for job in 0..num_jobs {
if job_next_op_idx[job] >= job_ops_len[job] {
continue;
}
if job_ready_time[job] > time {
continue;
}
let product = job_products[job];
let op_idx = job_next_op_idx[job];
let op_times = &challenge.product_processing_times[product][op_idx];
let proc_time = match op_times.get(&machine) {
Some(&value) => value,
None => continue,
};
let earliest_end = earliest_end_time(time, &machine_available_time, op_times);
let machine_end = time.max(machine_available_time[machine]) + proc_time;
if machine_end != earliest_end {
continue;
}
let priority = job_remaining_work[job];
if priority > best_priority + eps
|| ((priority - best_priority).abs() <= eps
&& best_job.map_or(true, |best| job < best))
{
best_job = Some(job);
best_priority = priority;
}
}
if let Some(job) = best_job {
let product = job_products[job];
let op_idx = job_next_op_idx[job];
let op_times = &challenge.product_processing_times[product][op_idx];
let proc_time = op_times[&machine];
let start_time = time.max(machine_available_time[machine]);
let end_time = start_time + proc_time;
job_schedule[job].push((machine, start_time));
job_next_op_idx[job] += 1;
job_ready_time[job] = end_time;
machine_available_time[machine] = end_time;
job_remaining_work[job] -= product_avg_times[product][op_idx];
if job_remaining_work[job] < 0.0 {
job_remaining_work[job] = 0.0;
}
remaining_ops -= 1;
scheduled_any = true;
}
}
if remaining_ops == 0 {
break;
}
// Compute next event time (either machine becoming available or job becoming ready)
let mut next_time: Option<u32> = None;
for &t in machine_available_time.iter() {
if t > time {
next_time = Some(next_time.map_or(t, |best| best.min(t)));
}
}
for job in 0..num_jobs {
if job_next_op_idx[job] < job_ops_len[job] && job_ready_time[job] > time {
let t = job_ready_time[job];
next_time = Some(next_time.map_or(t, |best| best.min(t)));
}
}
// Advance time to next event
time = next_time.ok_or_else(|| {
if scheduled_any {
anyhow!("No next event time found while operations remain unscheduled")
} else {
anyhow!("No schedulable operations remain; dispatching rules stalled")
}
})?;
}
save_solution(&Solution { job_schedule })?;
Ok(())
}

View File

@ -0,0 +1 @@
pub mod dispatching_rules;

View File

@ -0,0 +1,383 @@
use crate::QUALITY_PRECISION;
mod baselines;
use anyhow::{anyhow, Result};
use rand::{
distributions::Distribution,
rngs::{SmallRng, StdRng},
Rng, SeedableRng,
};
use rand_distr::Normal;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
pub struct FlowConfig {
pub avg_op_flexibility: f32,
pub reentrance_level: f32,
pub flow_structure: f32,
pub product_mix_ratio: f32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Flow {
STRICT,
PARALLEL,
RANDOM,
COMPLEX,
CHAOTIC,
}
impl From<Flow> for FlowConfig {
fn from(flow: Flow) -> Self {
match flow {
Flow::STRICT => FlowConfig {
avg_op_flexibility: 1.0,
reentrance_level: 0.2,
flow_structure: 0.0,
product_mix_ratio: 0.5,
},
Flow::PARALLEL => FlowConfig {
avg_op_flexibility: 3.0,
reentrance_level: 0.2,
flow_structure: 0.0,
product_mix_ratio: 0.5,
},
Flow::RANDOM => FlowConfig {
avg_op_flexibility: 1.0,
reentrance_level: 0.0,
flow_structure: 0.4,
product_mix_ratio: 1.0,
},
Flow::COMPLEX => FlowConfig {
avg_op_flexibility: 3.0,
reentrance_level: 0.2,
flow_structure: 0.4,
product_mix_ratio: 1.0,
},
Flow::CHAOTIC => FlowConfig {
avg_op_flexibility: 10.0,
reentrance_level: 0.0,
flow_structure: 1.0,
product_mix_ratio: 1.0,
},
}
}
}
impl std::fmt::Display for Flow {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Flow::STRICT => write!(f, "strict"),
Flow::PARALLEL => write!(f, "parallel"),
Flow::RANDOM => write!(f, "random"),
Flow::COMPLEX => write!(f, "complex"),
Flow::CHAOTIC => write!(f, "chaotic"),
}
}
}
impl std::str::FromStr for Flow {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"strict" => Ok(Flow::STRICT),
"parallel" => Ok(Flow::PARALLEL),
"random" => Ok(Flow::RANDOM),
"complex" => Ok(Flow::COMPLEX),
"chaotic" => Ok(Flow::CHAOTIC),
_ => Err(anyhow::anyhow!("Invalid flow type: {}", s)),
}
}
}
impl_kv_string_serde! {
Track {
n: usize,
m: usize,
o: usize,
r#flow: Flow
}
}
impl_base64_serde! {
Solution {
job_schedule: Vec<Vec<(usize, u32)>>,
}
}
impl Solution {
pub fn new() -> Self {
Self {
job_schedule: Vec::new(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Challenge {
pub seed: [u8; 32],
pub num_jobs: usize,
pub num_machines: usize,
pub num_operations: usize,
pub jobs_per_product: Vec<usize>,
// each product has a sequence of operations, and each operation has a map of eligible machines to processing times
pub product_processing_times: Vec<Vec<HashMap<usize, u32>>>,
}
impl Challenge {
pub fn generate_instance(seed: &[u8; 32], track: &Track) -> Result<Self> {
let mut rng = SmallRng::from_seed(StdRng::from_seed(seed.clone()).r#gen());
let FlowConfig {
avg_op_flexibility,
reentrance_level,
flow_structure,
product_mix_ratio,
} = track.flow.clone().into();
let n_jobs = track.n;
let n_machines = track.m;
let n_op_types = track.o;
let n_products = 1.max((product_mix_ratio * n_jobs as f32) as usize);
let n_routes = 1.max((flow_structure * n_jobs as f32) as usize);
let min_eligible_machines = 1;
let flexibility_std_dev = 0.5;
let base_proc_time_min = 1;
let base_proc_time_max = 200;
let min_speed_factor = 0.8;
let max_speed_factor = 1.2;
// random product for each job, only keep products that have at least one job
let mut map = HashMap::new();
let jobs_per_product = (0..n_jobs).fold(Vec::new(), |mut acc, _| {
let map_len = map.len();
let product = *map
.entry(rng.gen_range(0..n_products))
.or_insert_with(|| map_len);
if product >= acc.len() {
acc.push(0);
}
acc[product] += 1;
acc
});
// actual number of products (some products may have zero jobs)
let n_products = jobs_per_product.len();
// random route for each product, only keep routes that are used
let mut map = HashMap::new();
let product_route = (0..n_products)
.map(|_| {
let map_len = map.len();
*map.entry(rng.gen_range(0..n_routes))
.or_insert_with(|| map_len)
})
.collect::<Vec<usize>>();
// actual number of routes
let n_routes = map.len();
// generate operation sequence for each route
let routes = (0..n_routes)
.map(|_| {
let seq_len = n_op_types;
let mut base_sequence: Vec<usize> = (0..n_op_types).collect();
let mut steps = Vec::new();
// randomly build op sequence
for _ in 0..seq_len {
let next_op_idx = if rng.r#gen::<f32>() < flow_structure {
// Job Shop Logic: Random permutation
rng.gen_range(0..base_sequence.len())
} else {
// Flow Shop Logic: Pick next sequential op
0
};
let op_id = base_sequence.remove(next_op_idx);
steps.push(op_id);
}
for step_idx in (2..steps.len()).rev() {
// Reentrance Logic
if rng.r#gen::<f32>() < reentrance_level {
// assuming reentrance_level of 0.1
let op_id = steps[rng.gen_range(0..step_idx - 1)];
steps.insert(step_idx, op_id);
}
}
steps
})
.collect::<Vec<Vec<usize>>>();
// generate machine eligibility and base processing time for each operation
let normal = Normal::new(avg_op_flexibility, flexibility_std_dev).unwrap();
let all_machines = (0..n_machines).collect::<HashSet<usize>>();
let op_eligible_machines = (0..n_op_types)
.map(|i| {
if avg_op_flexibility as usize >= n_machines {
(0..n_machines).collect::<HashSet<usize>>()
} else {
let mut eligible = HashSet::<usize>::from([if i < n_machines {
i
} else {
rng.gen_range(0..n_machines)
}]);
if avg_op_flexibility > 1.0 {
let target_flex = min_eligible_machines
.max(normal.sample(&mut rng) as usize)
.min(n_machines);
let mut remaining = all_machines
.difference(&eligible)
.cloned()
.collect::<Vec<usize>>();
let num_to_add = (target_flex - 1).min(remaining.len());
for j in 0..num_to_add {
let idx = rng.gen_range(j..remaining.len());
remaining.swap(j, idx);
}
eligible.extend(remaining[..num_to_add].iter().cloned());
}
eligible
}
})
.collect::<Vec<_>>();
let base_proc_times = (0..n_op_types)
.map(|_| rng.gen_range(base_proc_time_min..=base_proc_time_max))
.collect::<Vec<u32>>();
// generate processing times for each product according to its route
let product_processing_times = product_route
.iter()
.map(|&r_idx| {
let route = &routes[r_idx];
route
.iter()
.map(|&op_id| {
let machines = &op_eligible_machines[op_id];
let base_time = base_proc_times[op_id];
machines
.iter()
.map(|&m_id| {
(
m_id,
1.max(
(base_time as f32
* (min_speed_factor
+ (max_speed_factor - min_speed_factor)
* rng.r#gen::<f32>()))
as u32,
),
)
})
.collect::<HashMap<usize, u32>>()
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
Ok(Challenge {
seed: seed.clone(),
num_jobs: n_jobs,
num_machines: n_machines,
num_operations: n_op_types,
jobs_per_product,
product_processing_times,
})
}
pub fn evaluate_makespan(&self, solution: &Solution) -> Result<u32> {
if solution.job_schedule.len() != self.num_jobs {
return Err(anyhow!(
"Expecting solution to have {} jobs. Got {}",
self.num_jobs,
solution.job_schedule.len(),
));
}
let mut job = 0;
let mut machine_usage = HashMap::<usize, Vec<(u32, u32)>>::new();
let mut makespan = 0u32;
for (product, num_jobs) in self.jobs_per_product.iter().enumerate() {
for _ in 0..*num_jobs {
let schedule = &solution.job_schedule[job];
let processing_times = &self.product_processing_times[product];
if schedule.len() != processing_times.len() {
return Err(anyhow!(
"Job {} of product {} expecting {} operations. Got {}",
job,
product,
processing_times.len(),
schedule.len(),
));
}
let mut min_start_time = 0;
for (op_idx, &(machine, start_time)) in schedule.iter().enumerate() {
let eligible_machines = &processing_times[op_idx];
if !eligible_machines.contains_key(&machine) {
return Err(anyhow!("Job {} schedule contains ineligible machine", job,));
}
if start_time < min_start_time {
return Err(anyhow!(
"Job {} schedule contains operation starting before previous is complete",
job,
));
}
let finish_time = start_time + eligible_machines[&machine];
machine_usage
.entry(machine)
.or_default()
.push((start_time, finish_time));
min_start_time = finish_time;
}
// min_start_time is the finish time of the job
if min_start_time > makespan {
makespan = min_start_time;
}
job += 1;
}
}
for (machine, usage) in machine_usage.iter_mut() {
usage.sort_by_key(|&(start, _)| start);
for i in 1..usage.len() {
if usage[i].0 < usage[i - 1].1 {
return Err(anyhow!(
"Machine {} is scheduled with overlapping jobs",
machine,
));
}
}
}
Ok(makespan)
}
conditional_pub!(
fn compute_greedy_baseline(&self) -> Result<Solution> {
let solution = RefCell::new(Solution::new());
let save_solution_fn = |s: &Solution| -> Result<()> {
*solution.borrow_mut() = s.clone();
Ok(())
};
baselines::dispatching_rules::solve_challenge(self, &save_solution_fn, &None)?;
Ok(solution.into_inner())
}
);
conditional_pub!(
fn compute_sota_baseline(&self) -> Result<Solution> {
Err(anyhow!("Not implemented yet"))
}
);
conditional_pub!(
fn evaluate_solution(&self, solution: &Solution) -> Result<i32> {
let makespan = self.evaluate_makespan(solution)?;
let greedy_solution = self.compute_greedy_baseline()?;
let greedy_makespan = self.evaluate_makespan(&greedy_solution)?;
// TODO: implement SOTA baseline
let quality = (greedy_makespan as f64 - makespan as f64) / greedy_makespan as f64;
let quality = quality.clamp(-10.0, 10.0) * QUALITY_PRECISION as f64;
let quality = quality.round() as i32;
Ok(quality)
}
);
}

View File

@ -202,3 +202,7 @@ pub use hypergraph as c005;
pub mod neuralnet_optimizer;
#[cfg(feature = "c006")]
pub use neuralnet_optimizer as c006;
#[cfg(feature = "c007")]
pub mod job_scheduling;
#[cfg(feature = "c007")]
pub use job_scheduling as c007;

View File

@ -34,3 +34,5 @@ c005 = ["cuda", "tig-challenges/c005"]
hypergraph = ["c005"]
c006 = ["cuda", "tig-challenges/c006"]
neuralnet_optimizer = ["c006"]
c007 = ["tig-challenges/c007"]
job_scheduling = ["c007"]

View File

@ -335,6 +335,12 @@ pub fn compute_solution(
#[cfg(feature = "c006")]
dispatch_challenge!(c006, gpu)
}
"c007" => {
#[cfg(not(feature = "c007"))]
panic!("tig-runtime was not compiled with '--features c007'");
#[cfg(feature = "c007")]
dispatch_challenge!(c007, cpu)
}
_ => panic!("Unsupported challenge"),
}
}

View File

@ -32,3 +32,5 @@ c005 = ["cuda", "tig-challenges/c005"]
hypergraph = ["c005"]
c006 = ["cuda", "tig-challenges/c006"]
neuralnet_optimizer = ["c006"]
c007 = ["tig-challenges/c007"]
job_scheduling = ["c007"]

View File

@ -209,6 +209,12 @@ pub fn verify_solution(
#[cfg(feature = "c006")]
dispatch_challenge!(c006, gpu)
}
"c007" => {
#[cfg(not(feature = "c007"))]
panic!("tig-verifier was not compiled with '--features c007'");
#[cfg(feature = "c007")]
dispatch_challenge!(c007, cpu)
}
_ => panic!("Unsupported challenge"),
}