-
Notifications
You must be signed in to change notification settings - Fork 1
Carry-over states between episodes, instead of resetting everything to 0. #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,7 +80,8 @@ def __init__(self, | |
| skip_plot_job_queue, | ||
| steps_per_iteration, | ||
| evaluation_mode=False, | ||
| workload_gen=None): | ||
| workload_gen=None, | ||
| carry_over_state=False): | ||
| super().__init__() | ||
|
|
||
| self.weights = weights | ||
|
|
@@ -119,6 +120,7 @@ def __init__(self, | |
| self.np_random = None | ||
| self._seed = None | ||
| self.workload_gen = workload_gen | ||
| self.carry_over_state = carry_over_state | ||
|
|
||
| if self.external_durations: | ||
| durations_sampler.init(self.external_durations) | ||
|
|
@@ -152,6 +154,9 @@ def __init__(self, | |
| # Initialize reward calculator | ||
| self.reward_calculator = RewardCalculator(self.prices) | ||
|
|
||
| self.reset_timeline_state() | ||
| self.metrics.reset_episode_metrics() | ||
|
|
||
| # actions: - change number of available nodes: | ||
| # action_type: 0: decrease, 1: maintain, 2: increase | ||
| # action_magnitude: 0-MAX_CHANGE (+1ed in the action) | ||
|
|
@@ -192,21 +197,59 @@ def reset(self, seed=None, options=None): | |
| self.episode_idx += 1 | ||
|
|
||
| # Reset metrics | ||
| self.metrics.reset_state_metrics() | ||
|
|
||
| # Choose starting index in the external price series | ||
| if self.prices is not None and getattr(self.prices, "external_prices", None) is not None: | ||
| n_prices = len(self.prices.external_prices) | ||
| episode_span = EPISODE_HOURS | ||
|
|
||
| # Episode k starts at hour k * episode_span (wrapping around the year) | ||
| start_index = (self.episode_idx * episode_span) % n_prices | ||
| if options and "price_start_index" in options: # For testing Purposes. Leave out 'options' to advance episode. | ||
| start_index = int(options["price_start_index"]) % n_prices | ||
| self.prices.reset(start_index=start_index) | ||
| if self.carry_over_state: | ||
| self.metrics.reset_episode_metrics() | ||
| else: | ||
| # Synthetic prices or no external prices | ||
| self.prices.reset(start_index=0) | ||
| self.metrics.reset_state_metrics() | ||
|
|
||
| self.metrics.current_hour = 0 | ||
|
|
||
| if not self.carry_over_state: | ||
| # Choose starting index in the external price series | ||
| if self.prices is not None and getattr(self.prices, "external_prices", None) is not None: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not use any |
||
| n_prices = len(self.prices.external_prices) | ||
| episode_span = EPISODE_HOURS | ||
|
|
||
| # Episode k starts at hour k * episode_span (wrapping around the year) | ||
| start_index = (self.episode_idx * episode_span) % n_prices | ||
| if options and "price_start_index" in options: # For testing Purposes. Leave out 'options' to advance episode. | ||
| start_index = int(options["price_start_index"]) % n_prices | ||
| self.prices.reset(start_index=start_index) | ||
| else: | ||
| # Synthetic prices or no external prices | ||
| self.prices.reset(start_index=0) | ||
|
|
||
| self.state = { | ||
| # Initialize all nodes to be 'online but free' (0) | ||
| 'nodes': np.zeros(MAX_NODES, dtype=np.int32), | ||
| # Initialize job queue to be empty | ||
| 'job_queue': np.zeros((MAX_QUEUE_SIZE * 4), dtype=np.int32), | ||
| # Initialize predicted prices array | ||
| 'predicted_prices': self.prices.predicted_prices.copy(), | ||
| } | ||
|
|
||
| self.baseline_state = { | ||
| 'nodes': np.zeros(MAX_NODES, dtype=np.int32), | ||
| 'job_queue': np.zeros((MAX_QUEUE_SIZE * 4), dtype=np.int32), | ||
| } | ||
|
|
||
| self.cores_available = np.full(MAX_NODES, CORES_PER_NODE, dtype=np.int32) | ||
| self.baseline_cores_available = np.full(MAX_NODES, CORES_PER_NODE, dtype=np.int32) | ||
|
|
||
| # Job tracking: { job_id: {'duration': remaining_hours, 'allocation': [(node_idx1, cores1), ...]}, ... } | ||
| self.running_jobs = {} | ||
| self.baseline_running_jobs = {} | ||
|
|
||
| self.next_job_id = 0 # shared between baseline and normal jobs | ||
|
|
||
| # Track next empty slot in job queue for O(1) insertion | ||
| self.next_empty_slot = 0 | ||
| self.baseline_next_empty_slot = 0 | ||
|
|
||
| return self.state, {} | ||
|
|
||
| def reset_timeline_state(self): | ||
| self.metrics.current_hour = 0 | ||
|
|
||
| self.state = { | ||
| # Initialize all nodes to be 'online but free' (0) | ||
|
|
@@ -224,7 +267,6 @@ def reset(self, seed=None, options=None): | |
|
|
||
| self.cores_available = np.full(MAX_NODES, CORES_PER_NODE, dtype=np.int32) | ||
| self.baseline_cores_available = np.full(MAX_NODES, CORES_PER_NODE, dtype=np.int32) | ||
|
|
||
| # Job tracking: { job_id: {'duration': remaining_hours, 'allocation': [(node_idx1, cores1), ...]}, ... } | ||
| self.running_jobs = {} | ||
| self.baseline_running_jobs = {} | ||
|
|
@@ -235,11 +277,10 @@ def reset(self, seed=None, options=None): | |
| self.next_empty_slot = 0 | ||
| self.baseline_next_empty_slot = 0 | ||
|
|
||
| return self.state, {} | ||
|
|
||
| def step(self, action): | ||
| self.current_step += 1 | ||
| self.metrics.current_hour += 1 | ||
| self.metrics.total_time_hours += 1 | ||
| if self.metrics.current_hour == 1: | ||
| self.current_episode += 1 | ||
| self.env_print(Fore.GREEN + f"\n[[[ Starting episode: {self.current_episode}, step: {self.current_step}, hour: {self.metrics.current_hour}" + Fore.RESET) | ||
|
|
@@ -272,7 +313,9 @@ def step(self, action): | |
| new_jobs_nodes, new_jobs_cores, self.next_empty_slot | ||
| ) | ||
| self.metrics.jobs_submitted += len(new_jobs) | ||
| self.metrics.episode_jobs_submitted += len(new_jobs) | ||
| self.metrics.jobs_rejected_queue_full += (new_jobs_count - len(new_jobs)) | ||
| self.metrics.episode_jobs_rejected_queue_full += (new_jobs_count - len(new_jobs)) | ||
|
|
||
| self.env_print("nodes: ", np.array2string(self.state['nodes'], separator=' ', max_line_width=np.inf)) | ||
| self.env_print(f"cores_available: {np.array2string(self.cores_available, separator=' ', max_line_width=np.inf)} ({np.sum(self.cores_available)})") | ||
|
|
@@ -288,11 +331,38 @@ def step(self, action): | |
| # Assign jobs to available nodes | ||
| self.env_print(f"[4] Assigning jobs to available nodes...") | ||
|
|
||
| # Create metrics dict for job assignment | ||
| job_metrics = { | ||
| 'jobs_completed': self.metrics.jobs_completed, | ||
| 'total_job_wait_time': self.metrics.total_job_wait_time, | ||
| 'jobs_dropped': self.metrics.jobs_dropped, | ||
| 'dropped_this_episode': self.metrics.dropped_this_episode, | ||
| 'baseline_jobs_completed': self.metrics.baseline_jobs_completed, | ||
| 'baseline_total_job_wait_time': self.metrics.baseline_total_job_wait_time, | ||
| 'baseline_jobs_dropped': self.metrics.baseline_jobs_dropped, | ||
| 'baseline_dropped_this_episode': self.metrics.baseline_dropped_this_episode, | ||
| 'episode_jobs_completed': self.metrics.episode_jobs_completed, | ||
| 'episode_total_job_wait_time': self.metrics.episode_total_job_wait_time, | ||
| 'episode_jobs_dropped': self.metrics.episode_jobs_dropped, | ||
| 'episode_baseline_jobs_completed': self.metrics.episode_baseline_jobs_completed, | ||
| 'episode_baseline_total_job_wait_time': self.metrics.episode_baseline_total_job_wait_time, | ||
| 'episode_baseline_jobs_dropped': self.metrics.episode_baseline_jobs_dropped, | ||
| } | ||
|
|
||
| num_launched_jobs, self.next_empty_slot, num_dropped_this_step, self.next_job_id = assign_jobs_to_available_nodes( | ||
| job_queue_2d, self.state['nodes'], self.cores_available, self.running_jobs, | ||
| self.next_empty_slot, self.next_job_id, self.metrics, is_baseline=False | ||
| ) | ||
|
|
||
| # Update metrics from job_metrics dict | ||
| self.metrics.jobs_completed = job_metrics['jobs_completed'] | ||
| self.metrics.total_job_wait_time = job_metrics['total_job_wait_time'] | ||
| self.metrics.jobs_dropped = job_metrics['jobs_dropped'] | ||
| self.metrics.dropped_this_episode = job_metrics['dropped_this_episode'] | ||
| self.metrics.episode_jobs_completed = job_metrics['episode_jobs_completed'] | ||
| self.metrics.episode_total_job_wait_time = job_metrics['episode_total_job_wait_time'] | ||
| self.metrics.episode_jobs_dropped = job_metrics['episode_jobs_dropped'] | ||
|
|
||
|
Comment on lines
+334
to
+365
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass the metrics dicts into job/baseline helpers (current code discards updates). 🐛 Proposed fix- num_launched_jobs, self.next_empty_slot, num_dropped_this_step, self.next_job_id = assign_jobs_to_available_nodes(
- job_queue_2d, self.state['nodes'], self.cores_available, self.running_jobs,
- self.next_empty_slot, self.next_job_id, self.metrics, is_baseline=False
- )
+ num_launched_jobs, self.next_empty_slot, num_dropped_this_step, self.next_job_id = assign_jobs_to_available_nodes(
+ job_queue_2d, self.state['nodes'], self.cores_available, self.running_jobs,
+ self.next_empty_slot, self.next_job_id, job_metrics, is_baseline=False
+ )
@@
- baseline_cost, baseline_cost_off, self.baseline_next_empty_slot, self.next_job_id = baseline_step(
- self.baseline_state, self.baseline_cores_available, self.baseline_running_jobs,
- current_price, new_jobs_count, new_jobs_durations, new_jobs_nodes, new_jobs_cores,
- self.baseline_next_empty_slot, self.next_job_id, self.metrics, self.env_print
- )
+ baseline_cost, baseline_cost_off, self.baseline_next_empty_slot, self.next_job_id = baseline_step(
+ self.baseline_state, self.baseline_cores_available, self.baseline_running_jobs,
+ current_price, new_jobs_count, new_jobs_durations, new_jobs_nodes, new_jobs_cores,
+ self.baseline_next_empty_slot, self.next_job_id, baseline_metrics, self.env_print
+ )Also applies to: 392-427 🤖 Prompt for AI Agents |
||
| self.env_print(f" {num_launched_jobs} jobs launched") | ||
|
|
||
| # Calculate node utilization stats | ||
|
|
@@ -313,18 +383,53 @@ def step(self, action): | |
| # Track max queue size | ||
| if num_unprocessed_jobs > self.metrics.max_queue_size_reached: | ||
| self.metrics.max_queue_size_reached = num_unprocessed_jobs | ||
| if num_unprocessed_jobs > self.metrics.episode_max_queue_size_reached: | ||
| self.metrics.episode_max_queue_size_reached = num_unprocessed_jobs | ||
|
|
||
| self.env_print(f"[5] Calculating reward...") | ||
|
|
||
| # Baseline step | ||
| baseline_metrics = { | ||
| 'baseline_jobs_submitted': self.metrics.baseline_jobs_submitted, | ||
| 'baseline_jobs_rejected_queue_full': self.metrics.baseline_jobs_rejected_queue_full, | ||
| 'baseline_jobs_completed': self.metrics.baseline_jobs_completed, | ||
| 'baseline_total_job_wait_time': self.metrics.baseline_total_job_wait_time, | ||
| 'baseline_jobs_dropped': self.metrics.baseline_jobs_dropped, | ||
| 'baseline_dropped_this_episode': self.metrics.baseline_dropped_this_episode, | ||
| 'baseline_max_queue_size_reached': self.metrics.baseline_max_queue_size_reached, | ||
| 'episode_baseline_jobs_submitted': self.metrics.episode_baseline_jobs_submitted, | ||
| 'episode_baseline_jobs_rejected_queue_full': self.metrics.episode_baseline_jobs_rejected_queue_full, | ||
| 'episode_baseline_jobs_completed': self.metrics.episode_baseline_jobs_completed, | ||
| 'episode_baseline_total_job_wait_time': self.metrics.episode_baseline_total_job_wait_time, | ||
| 'episode_baseline_jobs_dropped': self.metrics.episode_baseline_jobs_dropped, | ||
| 'episode_baseline_max_queue_size_reached': self.metrics.episode_baseline_max_queue_size_reached, | ||
| } | ||
|
|
||
| baseline_cost, baseline_cost_off, self.baseline_next_empty_slot, self.next_job_id = baseline_step( | ||
| self.baseline_state, self.baseline_cores_available, self.baseline_running_jobs, | ||
| current_price, new_jobs_count, new_jobs_durations, new_jobs_nodes, new_jobs_cores, | ||
| self.baseline_next_empty_slot, self.next_job_id, self.metrics, self.env_print | ||
| ) | ||
|
|
||
| # Update metrics from baseline_metrics dict | ||
| self.metrics.baseline_jobs_submitted = baseline_metrics['baseline_jobs_submitted'] | ||
| self.metrics.baseline_jobs_rejected_queue_full = baseline_metrics['baseline_jobs_rejected_queue_full'] | ||
| self.metrics.baseline_jobs_completed = baseline_metrics['baseline_jobs_completed'] | ||
| self.metrics.baseline_total_job_wait_time = baseline_metrics['baseline_total_job_wait_time'] | ||
| self.metrics.baseline_jobs_dropped = baseline_metrics['baseline_jobs_dropped'] | ||
| self.metrics.baseline_dropped_this_episode = baseline_metrics['baseline_dropped_this_episode'] | ||
| self.metrics.baseline_max_queue_size_reached = baseline_metrics['baseline_max_queue_size_reached'] | ||
| self.metrics.episode_baseline_jobs_submitted = baseline_metrics['episode_baseline_jobs_submitted'] | ||
| self.metrics.episode_baseline_jobs_rejected_queue_full = baseline_metrics['episode_baseline_jobs_rejected_queue_full'] | ||
| self.metrics.episode_baseline_jobs_completed = baseline_metrics['episode_baseline_jobs_completed'] | ||
| self.metrics.episode_baseline_total_job_wait_time = baseline_metrics['episode_baseline_total_job_wait_time'] | ||
| self.metrics.episode_baseline_jobs_dropped = baseline_metrics['episode_baseline_jobs_dropped'] | ||
| self.metrics.episode_baseline_max_queue_size_reached = baseline_metrics['episode_baseline_max_queue_size_reached'] | ||
|
|
||
| self.metrics.baseline_cost += baseline_cost | ||
| self.metrics.baseline_cost_off += baseline_cost_off | ||
| self.metrics.episode_baseline_cost += baseline_cost | ||
| self.metrics.episode_baseline_cost_off += baseline_cost_off | ||
|
|
||
| step_reward, step_cost, eff_reward_norm, price_reward_norm, idle_penalty_norm, job_age_penalty_norm = self.reward_calculator.calculate( | ||
| num_used_nodes, num_idle_nodes, current_price, average_future_price, | ||
|
|
@@ -334,6 +439,7 @@ def step(self, action): | |
|
|
||
| self.metrics.episode_reward += step_reward | ||
| self.metrics.total_cost += step_cost | ||
| self.metrics.episode_total_cost += step_cost | ||
|
|
||
| # Store normalized reward components for plotting | ||
| self.metrics.eff_rewards.append(eff_reward_norm * 100) | ||
|
|
@@ -385,4 +491,11 @@ def step(self, action): | |
|
|
||
| self.env_print(Fore.GREEN + f"]]]" + Fore.RESET) | ||
|
|
||
| return self.state, step_reward, terminated, truncated, {} | ||
| info = { | ||
| "step_cost": float(step_cost), | ||
| "num_unprocessed_jobs": int(num_unprocessed_jobs), | ||
| "num_on_nodes": int(num_on_nodes), | ||
| "dropped_this_episode": int(getattr(self.metrics, "dropped_this_episode", 0)), | ||
| } | ||
|
|
||
| return self.state, step_reward, terminated, truncated, info | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 2426
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 1410
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 1050
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 577
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 796
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 3707
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 183
🏁 Script executed:
Repository: FairRootGroup/powersched
Length of output: 6999
reset_timeline_state()attempts to copyself.prices.predicted_priceswhile it is stillNone.In
__init__at line 175,reset_timeline_state()is called beforePrices.reset()is invoked. However,Prices.__init__initializespredicted_prices = None(line 19), and it only gets populated whenPrices.reset()is called. This causesAttributeError: 'NoneType' object has no attribute 'copy'at line 277 when trying to executeself.prices.predicted_prices.copy().Call
self.prices.reset(start_index=0)beforeself.reset_timeline_state()in__init__, matching the pattern used in thereset()method (lines 120–123).🤖 Prompt for AI Agents