diff --git a/compiler_opt/es/propeller/__init__.py b/compiler_opt/es/propeller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/compiler_opt/es/propeller/gin_configs/blackbox_learner.gin b/compiler_opt/es/propeller/gin_configs/blackbox_learner.gin new file mode 100644 index 00000000..c5be316d --- /dev/null +++ b/compiler_opt/es/propeller/gin_configs/blackbox_learner.gin @@ -0,0 +1,25 @@ +import compiler_opt.es.blackbox_learner +import compiler_opt.rl.gin_external_configurables +import compiler_opt.es.blackbox_optimizers +import compiler_opt.es.blackbox_evaluator +import compiler_opt.es.es_trainer_lib + +# Inlining model settings + +# Blackbox learner config +BlackboxLearnerConfig.total_steps = 10000 +BlackboxLearnerConfig.total_num_perturbations = 25 +BlackboxLearnerConfig.blackbox_optimizer = %blackbox_optimizers.Algorithm.MONTE_CARLO +BlackboxLearnerConfig.estimator_type = %blackbox_optimizers.EstimatorType.ANTITHETIC +BlackboxLearnerConfig.fvalues_normalization = True +BlackboxLearnerConfig.hyperparameters_update_method = %blackbox_optimizers.UpdateMethod.NO_METHOD + +BlackboxLearnerConfig.num_top_directions = 0 + +BlackboxLearnerConfig.precision_parameter = 0.5 + +BlackboxLearnerConfig.step_size = 0.5 + +blackbox_evaluator.PropellerBlackboxEvaluator.total_num_perturbations = 25 +BlackboxLearnerConfig.evaluator = @blackbox_evaluator.PropellerBlackboxEvaluator +BlackboxLearnerConfig.save_best_policy = True \ No newline at end of file diff --git a/compiler_opt/es/propeller/gin_configs/propeller.gin b/compiler_opt/es/propeller/gin_configs/propeller.gin new file mode 100644 index 00000000..23cb9522 --- /dev/null +++ b/compiler_opt/es/propeller/gin_configs/propeller.gin @@ -0,0 +1,27 @@ +import compiler_opt.rl.gin_external_configurables +import compiler_opt.rl.propeller.config +import compiler_opt.es.es_trainer_lib +import compiler_opt.rl.propeller +import compiler_opt.es.propeller.propeller_worker +import compiler_opt.rl.agent_config + +config_registry.get_configuration.implementation = @configs.PropellerConfig + +PropellerWorker.workspace_path = '' +PropellerWorker.initial_clang_path = '' +PropellerWorker.perf_profile_path = '' +PropellerWorker.fdo_profile_path = '' +PropellerWorker.tflite_model_dump_path = '' +PropellerWorker.propeller_profiles_path = '' + +create_agent.policy_network = @agents.RegressionCloningNetwork +agents.RegressionCloningNetwork.fc_layer_params = (256, 256, 256) + +propeller.config.get_observation_processing_layer_creator.quantile_file_dir = '' +propeller.config.get_observation_processing_layer_creator.with_sqrt = True +propeller.config.get_observation_processing_layer_creator.with_z_score_normalization = True + +policy_utils.create_actor_policy.actor_network_ctor = @agents.RegressionCloningNetwork + +compiler_opt.es.es_trainer_lib.train.is_corpus_required = False +compiler_opt.es.es_trainer_lib.train.worker_class = @PropellerWorker diff --git a/compiler_opt/es/propeller/propeller_worker.py b/compiler_opt/es/propeller/propeller_worker.py new file mode 100644 index 00000000..ea9bb330 --- /dev/null +++ b/compiler_opt/es/propeller/propeller_worker.py @@ -0,0 +1,242 @@ +"""Worker for propeller for code layout evaluation (Open Source).""" + +import logging +import os +import re +import shutil +import subprocess +import tempfile +from typing import Optional + +import gin +import numpy as np + +from ...distributed import worker +from ...es import policy_utils + + +def _run_command(cmd: list[str], cwd: Optional[str] = None) -> tuple[int, str, str]: + """Runs a local command.""" + logging.info('Running local command: %s', ' '.join(cmd)) + try: + process = subprocess.run( + cmd, + capture_output=True, + text=True, + cwd=cwd, + check=False, + errors='ignore', + ) + return process.returncode, process.stdout, process.stderr + except Exception as e: # pylint: disable=broad-except + logging.exception('Local command failed: %s', e) + return -1, '', str(e) + + +def _run_remote_command(host: str, cmd_list: list[str]) -> tuple[int, str, str]: + """Runs a remote command via standard SSH.""" + if host == 'local': + return _run_command(cmd_list) + + full_cmd = ['ssh', host] + cmd_list + logging.info('Running remote command on [%s]: %s', host, ' '.join(cmd_list)) + try: + process = subprocess.run( + full_cmd, + capture_output=True, + text=True, + check=False, + errors='ignore', + ) + return process.returncode, process.stdout, process.stderr + except Exception as e: # pylint: disable=broad-except + logging.exception('Remote command failed: %s', e) + return -1, '', str(e) + + +def _copy_to_host(host: str, src: str, dest: str) -> bool: + """Copies a file to a remote host via standard SCP.""" + if not os.path.exists(src): + logging.error('Local file missing: %s', src) + return False + + if host == 'local': + try: + os.makedirs(dest, exist_ok=True) + shutil.copy(src, dest) + return True + except Exception as e: # pylint: disable=broad-except + logging.exception('Local copy failed: %s', e) + return False + + scp_cmd = ['scp', '-r', src, f'{host}:{dest}/'] + logging.info('Copying file to [%s]: %s -> %s', host, src, dest) + try: + subprocess.run(scp_cmd, check=True, capture_output=True, text=True) + return True + except Exception as e: # pylint: disable=broad-except + logging.exception('SCP failed: %s', e) + return False + + +@gin.configurable +class PropellerWorker(worker.Worker): + """A generic worker that evaluates Propeller policies via SSH/SCP.""" + + def __init__( + self, + *, + initial_clang_path: str, + perf_profile_path: str, + propeller_prof_gen_path: str, + remote_workdir: str, + link_cmd: list[str], + benchmark_cmd: list[str], + base_policy_path: Optional[str] = None, + ): + """Initializes the PropellerWorker. + + Args: + initial_clang_path: Path to the initial Clang binary (local). + perf_profile_path: Path to the input perf profile (local). + propeller_prof_gen_path: Path to generate_propeller_profiles binary + (local). + remote_workdir: Directory on the remote host to use for execution. + link_cmd: Command to run on remote host to link Clang. + benchmark_cmd: Command to run on remote host to benchmark Clang. + base_policy_path: Optional path to base policy for TFLite conversion. + """ + self._initial_clang_path = initial_clang_path + self._perf_profile_path = perf_profile_path + self._propeller_prof_gen_path = propeller_prof_gen_path + self._remote_workdir = remote_workdir + self._link_cmd = link_cmd + self._benchmark_cmd = benchmark_cmd + self._base_policy_path = base_policy_path + self._eval_counter = 0 + + def compile( + self, + policy: Optional[bytes], + modules: list, # Ignored, but kept for interface compatibility + gcp_host: Optional[str] = None, + perf_profile_path: Optional[str] = None, + perturbation_index: Optional[int] = 0, + ) -> Optional[float]: + # pylint: disable=unused-argument + """Evaluates a policy by generating profiles, linking, and benchmarking.""" + host = gcp_host + if not host: + raise ValueError("gcp_host must be provided") + + self._eval_counter += 1 + eval_id = f'{host}-{self._eval_counter}' + logging.info('[%s] Starting evaluation %s', host, eval_id) + + # Create a local temp dir for profile generation + with tempfile.TemporaryDirectory(prefix='propeller_worker_') as tmp_dir: + clang_input_path = self._initial_clang_path + perf_data_path = perf_profile_path or self._perf_profile_path + + tflite_policy_dir = None + if policy is not None: + if not self._base_policy_path: + raise ValueError('base_policy_path is required when policy is provided') + local_policy_dir = os.path.join(tmp_dir, 'policy') + tflite_policy_dir = policy_utils.convert_to_tflite( + policy, local_policy_dir, self._base_policy_path + ) + logging.info('Saved TFLite model to %s', tflite_policy_dir) + + # Output paths for generated profiles (local temp dir) + cc_profile_path = os.path.join(tmp_dir, 'cc_profile.txt') + ld_profile_path = os.path.join(tmp_dir, 'ld_profile.txt') + + prof_gen_cmd = [ + self._propeller_prof_gen_path, + f'--binary={clang_input_path}', + f'--profile={perf_data_path}', + f'--cc_profile={cc_profile_path}', + f'--ld_profile={ld_profile_path}', + '--alsologtostderr', + ] + + if tflite_policy_dir and policy is not None: + options_str = ( + 'code_layout_params {' + ' inter_function_reordering: true, split_all_basic_blocks: true,' + f" policy_path: '{tflite_policy_dir}'" + '}' + ) + prof_gen_cmd.append(f'--propeller_options="{options_str}"') + prof_gen_cmd.append('--use_ml') + else: + options_str = ( + 'code_layout_params { inter_function_reordering: false,' + ' split_all_basic_blocks: true }' + ) + prof_gen_cmd.append(f'--propeller_options="{options_str}"') + + # Run profile generation locally + return_code, stdout, stderr = _run_command(prof_gen_cmd) + if return_code != 0: + logging.error('Profile generation failed') + logging.error('STDOUT:\n%s', stdout) + logging.error('STDERR:\n%s', stderr) + return None + + logging.info('Profile generation succeeded.') + + # Copy profiles to remote host + # We copy them to the configured remote_workdir + + # Ensure remote workdir exists + _run_remote_command(host, ['mkdir', '-p', self._remote_workdir]) + + if not _copy_to_host(host, cc_profile_path, self._remote_workdir): + logging.error('Failed to copy cc_profile.txt to host') + return None + if not _copy_to_host(host, ld_profile_path, self._remote_workdir): + logging.error('Failed to copy ld_profile.txt to host') + return None + + # Run link command on remote host + logging.info('Running link command on %s...', host) + return_code, stdout, stderr = _run_remote_command( + host, self._link_cmd + ) + if return_code != 0: + logging.error('Failed to link Clang on remote host') + logging.error('STDOUT:\n%s', stdout) + logging.error('STDERR:\n%s', stderr) + return None + logging.info('Link command succeeded.') + + # Run benchmark on remote host + logging.info('Running benchmark on %s...', host) + return_code, stdout, stderr = _run_remote_command( + host, self._benchmark_cmd + ) + if return_code != 0: + logging.error('Failed to run benchmark on remote host') + logging.error('STDOUT:\n%s', stdout) + logging.error('STDERR:\n%s', stderr) + return None + logging.info('Benchmark command succeeded.') + + # Parse output for cycles (similar to verified worker) + matches = re.findall(r'(\d[\d,]*)\s+cycles[:\w]*', stdout + '\n' + stderr) + if matches: + cycles_list = [float(m.replace(',', '')) for m in matches] + # If we have multiple runs, discard first as warm-up and take median + if len(cycles_list) > 1: + warmed_cycles_list = cycles_list[1:] + measured_cycles = float(np.median(warmed_cycles_list)) + else: + measured_cycles = cycles_list[0] + logging.info('[%s] Measured cycles: %f', host, measured_cycles) + return measured_cycles + else: + logging.error('Failed to parse cycles from benchmark output') + logging.error('Benchmark output was:\n%s', stdout + '\n' + stderr) + return None diff --git a/compiler_opt/rl/propeller/__init__.py b/compiler_opt/rl/propeller/__init__.py new file mode 100644 index 00000000..5d9eeee0 --- /dev/null +++ b/compiler_opt/rl/propeller/__init__.py @@ -0,0 +1,53 @@ +"""Propeller RL configuration.""" + +import gin +import tensorflow as tf + +from .. import problem_configuration +from . import config +from . import propeller_runner +from . import agent_config + + +@gin.register(module='configs') +class PropellerConfig(problem_configuration.ProblemConfiguration): + """Propeller configuration for Regression RL.""" + + def get_env(self): + raise NotImplementedError( + 'get_env not implemented for RegressionPropellerConfig' + ) + + def get_runner_type(self): + return propeller_runner.PropellerRunner + + def get_signature_spec(self): + return config.get_propeller_regression_signature_spec() + + def get_preprocessing_layer_creator(self): + return config.get_observation_processing_layer_creator() + + def get_nonnormalized_features(self): + return config.get_nonnormalized_features() + + +@gin.register(module='configs') +class RegressionPropellerConfig(problem_configuration.ProblemConfiguration): + """Propeller configuration for Regression RL.""" + + def get_env(self): + raise NotImplementedError( + 'get_env not implemented for RegressionPropellerConfig' + ) + + def get_runner_type(self): + return propeller_runner.PropellerRunner + + def get_signature_spec(self): + return config.get_propeller_regression_signature_spec() + + def get_preprocessing_layer_creator(self): + return config.get_observation_processing_layer_creator() + + def get_nonnormalized_features(self): + return config.get_nonnormalized_features() diff --git a/compiler_opt/rl/propeller/agent_config.py b/compiler_opt/rl/propeller/agent_config.py new file mode 100644 index 00000000..651893ee --- /dev/null +++ b/compiler_opt/rl/propeller/agent_config.py @@ -0,0 +1,207 @@ +"""Propeller-specific Behavioral Cloning agent configuration.""" + +import os +from typing import Any + +import gin +import tensorflow as tf +from tf_agents.agents.behavioral_cloning import behavioral_cloning_agent +from tf_agents.networks import network +from tf_agents.specs import tensor_spec + +from tf_agents.agents import tf_agent +from tf_agents.typing import types +from ..agent_config import AgentConfig + + +@gin.configurable(module='agents') +class PropellerRegressionCloningNetwork(network.Network): + """A tf_agents Network that processes features and outputs regression score.""" + + def __init__( + self, + input_tensor_spec, + output_tensor_spec, + preprocessing_layers=None, + preprocessing_combiner=None, + fc_layer_params=(64, 32), + dropout_rate=0.2, + name='PropellerRegressionNetwork', + **kwargs + ): + super().__init__( + input_tensor_spec=input_tensor_spec, state_spec=(), name=name, **kwargs + ) + self._output_dim = ( + output_tensor_spec.shape[0] if len(output_tensor_spec.shape) > 0 else 1 + ) + + if preprocessing_layers is None: + self._flat_preprocessing_layers = None + else: + self._flat_preprocessing_layers = [ + layer for layer in tf.nest.flatten(preprocessing_layers) + ] + self._preprocessing_nest = tf.nest.map_structure( + lambda l: None, preprocessing_layers + ) + self._preprocessing_combiner = preprocessing_combiner + + self._dense_layers = [] + self._dropout_layers = [] + + for num_units in fc_layer_params: + self._dense_layers.append( + tf.keras.layers.Dense(num_units, activation='relu') + ) + if dropout_rate > 0.0: + self._dropout_layers.append(tf.keras.layers.Dropout(dropout_rate)) + + self._score_layer = tf.keras.layers.Dense( + self._output_dim, activation='sigmoid' + ) + + def call( + self, observations, step_type=None, network_state=(), training=False + ): + + if self._flat_preprocessing_layers is not None: + processed = [] + for obs, layer in zip( + nest.flatten_up_to(self._preprocessing_nest, observations), + self._flat_preprocessing_layers, + ): + res = layer(obs, training=training) if layer is not None else obs + if ( + len(obs.shape) > 1 + and obs.shape[-1] == 1 + and len(res.shape) > len(obs.shape) + ): + res = tf.squeeze(res, axis=-2) + processed.append(res) + if self._preprocessing_combiner is not None: + processed = self._preprocessing_combiner(processed) + else: + processed = nest.pack_sequence_as(self._preprocessing_nest, processed) + else: + processed = observations + + # Clean up features not used by scoring + processed.pop('mask', None) + processed.pop('is_chosen', None) + processed.pop('merge_order', None) + processed.pop('score_gain', None) + processed.pop('decision_id', None) + + feature_list = [] + for feat in tf.nest.flatten(processed): + feature_list.append(feat) + + x = tf.concat(feature_list, axis=-1) + + flat_x = x + if len(flat_x.shape) > 2: + orig_shape = tf.shape(flat_x) + last_dim = flat_x.shape[-1] + flat_x = tf.reshape(flat_x, [-1, last_dim]) + else: + orig_shape = None + + for i in range(len(self._dense_layers)): + flat_x = self._dense_layers[i](flat_x) + if self._dropout_layers: + flat_x = self._dropout_layers[i](flat_x, training=training) + + flat_scores = self._score_layer(flat_x) + + if orig_shape is not None: + new_shape = tf.concat([orig_shape[:-1], [self._output_dim]], axis=0) + raw_scores = tf.reshape(flat_scores, new_shape) + else: + raw_scores = flat_scores + + return raw_scores, network_state + + +@gin.configurable(module='agents') +class PropellerBCAgentConfig(AgentConfig): + """Behavioral Cloning agent configuration for Propeller regression.""" + + is_regression = True + + def create_agent( + self, + preprocessing_layers: tf.keras.layers.Layer, + policy_network: types.Network, + ) -> tf_agent.TFAgent: + """Creates a behavioral_cloning_agent.""" + + network = policy_network( + self.time_step_spec.observation, + self.action_spec, + preprocessing_layers=preprocessing_layers, + name='RegressionNetwork', + ) + + def custom_bc_loss(experience, training=False): + batch_size = ( + tf.compat.dimension_value(experience.step_type.shape[0]) + or tf.shape(experience.step_type)[0] + ) + network_state = network.get_initial_state(batch_size) + bc_predictions, _ = network( + experience.observation, + step_type=experience.step_type, + training=training, + network_state=network_state, + ) + + if ( + isinstance(preprocessing_layers, dict) + and 'score_gain' in preprocessing_layers + ): + layer = preprocessing_layers['score_gain'] + true_processed = layer(experience.observation['score_gain']) + elif ( + hasattr(preprocessing_layers, 'get') + and 'score_gain' in preprocessing_layers + ): + layer = preprocessing_layers.get('score_gain') + true_processed = layer(experience.observation['score_gain']) + else: + flat_layers = [l for l in tf.nest.flatten(preprocessing_layers)] + flat_obs = [ + o + for o in tf.nest.flatten_up_to( + preprocessing_layers, experience.observation + ) + ] + processed_dict = tf.nest.pack_sequence_as( + preprocessing_layers, + [ + l(o) if l is not None else o + for o, l in zip(flat_obs, flat_layers) + ], + ) + true_processed = processed_dict['score_gain'] + + if len(true_processed.shape) > len(bc_predictions.shape): + true_processed = tf.squeeze(true_processed, axis=-2) + + # The 1st element (index 0) is the bucketized percentile (quantile). + # We predict the quantile instead of predicting the raw score gain. + target_1d = true_processed[..., 0:1] + + # Unweighted Mean Squared Error (MSE) Loss + squared_error = tf.square(target_1d - bc_predictions) + losses = tf.squeeze(squared_error, axis=-1) + + return losses + + return behavioral_cloning_agent.BehavioralCloningAgent( + self.time_step_spec, + self.action_spec, + cloning_network=network, + num_outer_dims=2, + loss_fn=custom_bc_loss, + ) diff --git a/compiler_opt/rl/propeller/agent_config_test.py b/compiler_opt/rl/propeller/agent_config_test.py new file mode 100644 index 00000000..9f5493de --- /dev/null +++ b/compiler_opt/rl/propeller/agent_config_test.py @@ -0,0 +1,72 @@ +"""Tests for Propeller-specific Behavioral Cloning agent configuration.""" + +import gin +import tensorflow as tf +from tf_agents.agents.behavioral_cloning import behavioral_cloning_agent +from tf_agents.networks import network +from tf_agents.specs import tensor_spec +from tf_agents.trajectories import time_step + +# agent config import + +class PropellerBCAgentConfigTest(tf.test.TestCase): + + def setUp(self): + super().setUp() + gin.clear_config() + + def test_regression_agent_creation(self): + # Define specs + # Observation spec needs score_gain to simulate propeller data + observation_spec = { + 'obs': tensor_spec.TensorSpec(shape=(10,), dtype=tf.float32, name='obs'), + 'score_gain': tensor_spec.TensorSpec(shape=(1,), dtype=tf.float32, name='score_gain'), + } + time_step_spec = time_step.time_step_spec(observation_spec) + + action_spec = tensor_spec.BoundedTensorSpec( + shape=(1,), dtype=tf.float32, minimum=-100.0, maximum=100.0, name='action' + ) + + gin.bind_parameter('BehavioralCloningAgent.optimizer', + tf.compat.v1.train.AdamOptimizer()) + + # Create config + config = agent_config.PropellerBCAgentConfig( + time_step_spec=time_step_spec, action_spec=action_spec + ) + + # Dummy network returning 1D tensor matching action_spec + class DummyNetwork(network.Network): + + def __init__(self, input_tensor_spec, action_spec, name=None, **kwargs): + super().__init__( + input_tensor_spec=input_tensor_spec, state_spec=(), name=name + ) + + def call( + self, _observation, _step_type=None, network_state=(), _training=False + ): + # Output raw prediction matching action_spec + return tf.constant([[5.0]]), network_state + + # Mock preprocessing layers + # The target_1d extraction uses index 0 of score_gain + # In actual code, preprocessing layer returns [bucketized, bucketized^2, ...] + # We mock it returning a [Batch, 2] tensor + preprocessing_layers = { + 'obs': tf.keras.layers.Lambda(lambda x: x), + 'score_gain': tf.keras.layers.Lambda(lambda x: tf.concat([x, x], axis=-1)), # Returns [Batch, 2] + } + + # Create agent + agent = config.create_agent(preprocessing_layers, DummyNetwork) + + self.assertIsInstance( + agent, behavioral_cloning_agent.BehavioralCloningAgent + ) + self.assertTrue(callable(agent._bc_loss_fn)) + + +if __name__ == '__main__': + tf.test.main() diff --git a/compiler_opt/rl/propeller/config.py b/compiler_opt/rl/propeller/config.py new file mode 100644 index 00000000..b551c9a0 --- /dev/null +++ b/compiler_opt/rl/propeller/config.py @@ -0,0 +1,162 @@ +"""Propeller Training config.""" + +import gin +import tensorflow as tf +from tf_agents.agents.behavioral_cloning import behavioral_cloning_agent +from tf_agents.networks import network +from tf_agents.policies import actor_policy +from tf_agents.specs import tensor_spec +from tf_agents.specs import tensor_spec +from tf_agents.trajectories import time_step +from tf_agents.trajectories import time_step as ts +from .. import agent_config +from .. import feature_ops + + +@gin.configurable() +def get_propeller_regression_signature_spec(): + """Returns (time_step_spec, action_spec) for Propeller Regression.""" + observation_spec = { + key: tf.TensorSpec(dtype=tf.float32, shape=(1,), name=key) + for key in ( + 'unsplit_density', + 'split_density', + 'unsplit_size', + 'unsplit_freq', + 'split_size', + 'split_freq', + 'score_gain', + 'edge1_weight', + 'edge2_weight', + 'edge1_distance', + 'edge2_distance', + 'broken_bond_weight', + 'broken_bond_distance', + ) + } + + # observation_spec['edge1_type'] = tf.TensorSpec( + # dtype=tf.float32, shape=(4,), name='edge1_type' + # ) + # observation_spec['edge2_type'] = tf.TensorSpec( + # dtype=tf.float32, shape=(4,), name='edge2_type' + # ) + # observation_spec['broken_bond_type'] = tf.TensorSpec( + # dtype=tf.float32, shape=(4,), name='broken_bond_type' + # ) + + observation_spec['split_s1_is_entry'] = tf.TensorSpec( + dtype=tf.int64, shape=(1,), name='split_s1_is_entry' + ) + observation_spec['split_s2_is_entry'] = tf.TensorSpec( + dtype=tf.int64, shape=(1,), name='split_s2_is_entry' + ) + observation_spec['unsplit_is_entry'] = tf.TensorSpec( + dtype=tf.int64, shape=(1,), name='unsplit_is_entry' + ) + # observation_spec['decision_id'] = tf.TensorSpec( + # dtype=tf.int64, shape=(1,), name='decision_id' + # ) + + reward_spec = tf.TensorSpec(dtype=tf.float32, shape=(), name='reward') + time_step_spec = time_step.time_step_spec(observation_spec, reward_spec) + + # Target action is a single scalar value. + action_spec = tensor_spec.BoundedTensorSpec( + dtype=tf.float32, + shape=(1,), + minimum=-1e9, # Changed to allow symmetric log negative values + maximum=1e9, + name='target_score_gain', + ) + + return time_step_spec, action_spec + + +@gin.configurable +def log1p_preprocessing(x): + """Safely applies log(1+x) to input features.""" + # Cast to float32 and use tf.maximum to prevent any accidental negative + # values from producing NaNs during the log operation. + safe_x = tf.maximum(tf.cast(x, tf.float32), 0.0) + return tf.math.log1p(safe_x) + + +@gin.configurable +def get_observation_processing_layer_creator( + quantile_file_dir=None, + with_sqrt=True, + with_z_score_normalization=True, + eps=1e-8, +): + """Wrapper for observation_processing_layer.""" + quantile_map = feature_ops.build_quantile_map(quantile_file_dir) + + def observation_processing_layer(obs_spec): + """Creates the layer to process observation given obs_spec.""" + if obs_spec.name == 'decision_id': + return tf.keras.layers.Lambda(lambda x: x) + + if obs_spec.name in ['edge1_type', 'edge2_type', 'broken_bond_type']: + tf.print( + 'INFO: Log1p preprocessing for feature:', + obs_spec.name, + ) + return tf.keras.layers.Lambda(log1p_preprocessing) + + if obs_spec.name in get_onehot_features(): + tf.print( + 'INFO: One-hot feature, skipping normalization:', + obs_spec.name, + ) + return tf.keras.layers.Lambda(lambda x: tf.cast(x, tf.float32)) + + elif obs_spec.name in get_nonnormalized_features(): + tf.print( + 'INFO: Non-normalized feature, skipping normalization:', + obs_spec.name, + ) + return tf.keras.layers.Lambda(feature_ops.identity_fn) + + if obs_spec.name not in quantile_map: + tf.print( + 'WARNING: Missing quantile for feature, skipping normalization:', + obs_spec.name, + ) + return tf.keras.layers.Lambda(feature_ops.identity_fn) + + tf.print( + 'INFO: Normalizing feature:', + obs_spec.name, + ) + quantile = quantile_map[obs_spec.name] + return tf.keras.layers.Lambda( + feature_ops.get_normalize_fn( + quantile, + with_sqrt=False, + with_z_score_normalization=False, + eps=eps, + preprocessing_fn=log1p_preprocessing, + ) + ) + + return observation_processing_layer + + +@gin.configurable() +def get_onehot_features(): + return ['merge_order'] + + +@gin.configurable() +def get_nonnormalized_features(): + return [ + 'reward', + # 'is_chosen', + 'decision_id', + 'split_s1_is_entry', + 'split_s2_is_entry', + 'unsplit_is_entry', + 'merge_order', + 'mask', + ] diff --git a/compiler_opt/rl/propeller/gin_configs/behavioral_cloning_nn_agent.gin b/compiler_opt/rl/propeller/gin_configs/behavioral_cloning_nn_agent.gin new file mode 100644 index 00000000..e658a87f --- /dev/null +++ b/compiler_opt/rl/propeller/gin_configs/behavioral_cloning_nn_agent.gin @@ -0,0 +1,48 @@ +import gin.tf.external_configurables +import compiler_opt.rl.constant +import compiler_opt.rl.gin_external_configurables +import compiler_opt.rl.propeller.config +import compiler_opt.rl.trainer +import tf_agents.agents.behavioral_cloning.behavioral_cloning_agent + +# Add the import path for Propeller-specific agent config: +import compiler_opt.rl.propeller.agent_config + +config_registry.get_configuration.implementation=@configs.PropellerConfig + +train_eval.agent_config_type=@agents.PropellerBCAgentConfig +train_eval.num_iterations= 100000 +train_eval.batch_size = 256 +train_eval.train_sequence_length = 1 + +# Force exact 50/50 dataset interleaving between positive and negative/zero targets +data_reader.use_exact_50_50_sampling.enabled = False +data_reader.use_inlining_bc_trajectory.enabled = True + +# quantile file dir +propeller.config.get_observation_processing_layer_creator.with_sqrt = True +propeller.config.get_observation_processing_layer_creator.with_z_score_normalization = True + +# ---------------------------------------------------------------------- +# 1. USE THE REGRESSION MODEL +# ---------------------------------------------------------------------- +create_agent.policy_network = @agents.PropellerRegressionCloningNetwork + +# You can configure the shared dense layers for your scoring network here +agents.PropellerRegressionCloningNetwork.fc_layer_params=(256, 256, 256) + +# ---------------------------------------------------------------------- +# 2. UPDATE OPTIMIZER +# ---------------------------------------------------------------------- +tf.keras.optimizers.Adam.learning_rate = 0.0001 +tf.keras.optimizers.Adam.epsilon = 1e-8 + +BehavioralCloningAgent.optimizer = @tf.keras.optimizers.Adam() + +BehavioralCloningAgent.epsilon_greedy = 0.1 +BehavioralCloningAgent.gradient_clipping = None +BehavioralCloningAgent.debug_summaries = True +BehavioralCloningAgent.summarize_grads_and_vars = True + +# Disable percentage correct since we are performing continuous regression now +Trainer.bc_percentage_correct = False diff --git a/compiler_opt/rl/propeller/logs_to_tfrecord.py b/compiler_opt/rl/propeller/logs_to_tfrecord.py new file mode 100644 index 00000000..654b27d5 --- /dev/null +++ b/compiler_opt/rl/propeller/logs_to_tfrecord.py @@ -0,0 +1,44 @@ +"""Tool to convert Propeller log files to TFRecord.""" + +import glob +import os + +from absl import app +from absl import flags +from absl import logging +import tensorflow as tf + +from .. import log_reader + +flags.DEFINE_string('input_log_dir', None, 'Directory containing .log files.') +flags.DEFINE_string( + 'output_tfrecord', 'propeller.tfrecord', 'Output TFRecord file.' +) + +FLAGS = flags.FLAGS + + +def main(_): + log_files = glob.glob(os.path.join(FLAGS.input_log_dir, '*.log')) + logging.info('Found %d log files.', len(log_files)) + + with tf.io.TFRecordWriter(FLAGS.output_tfrecord) as writer: + total_records = 0 + for log_file in log_files: + logging.info('Processing %s', log_file) + try: + sequence_examples = log_reader.read_log_as_sequence_examples(log_file) + for se in sequence_examples.values(): + writer.write(se.SerializeToString()) + total_records += 1 + except Exception as e: # pylint: disable=broad-except + logging.error('Error processing %s: %s', log_file, e) + + logging.info( + 'Done. Written %d records to %s', total_records, FLAGS.output_tfrecord + ) + + +if __name__ == '__main__': + flags.mark_flag_as_required('input_log_dir') + app.run(main) diff --git a/compiler_opt/rl/propeller/propeller_runner.py b/compiler_opt/rl/propeller/propeller_runner.py new file mode 100644 index 00000000..0eb3b236 --- /dev/null +++ b/compiler_opt/rl/propeller/propeller_runner.py @@ -0,0 +1,125 @@ +"""Propeller Runner for RL.""" + +import os + + +import gin +import tensorflow as tf + +from .. import compilation_runner +from .. import corpus +from .. import log_reader + +_DEFAULT_IDENTIFIER = 'default' + + +@gin.configurable(module='runners') +class PropellerRunner(compilation_runner.CompilationRunner): + """Runner for Propeller Optimization.""" + + def __init__( + self, + propeller_prof_gen_path, + perf_profile_path, + initial_clang_path, + cc_profile_path, + ld_profile_path, + **kwargs, + ): + super().__init__(**kwargs) + self._propeller_prof_gen_path = propeller_prof_gen_path + # perf_profile_path can be a file or a directory. + # If directory, we look for {module_name}.perf + self._perf_profile_path = perf_profile_path + self._initial_clang_path = initial_clang_path + self._cc_profile_path = cc_profile_path + self._ld_profile_path = ld_profile_path + + def compile_fn( + self, + command_line: corpus.FullyQualifiedCmdLine, + tf_policy_path: str, + reward_only: bool, + workdir: str, + module_name: str | None = None, + ) -> dict[str, tuple[tf.train.SequenceExample, float]]: + + results = {} + + if not module_name: + print('Error: module_name must be provided.') + return {} + + current_perf_profile = os.path.join( + self._perf_profile_path, f'{module_name}.perf' + ) + + # current_perf_profile = os.path.join(self._perf_profile_path, f'perf.data') + + if not os.path.exists(current_perf_profile): + print(f'Error: {current_perf_profile} does not exist.') + return {} + + print(f'Processing {module_name}') + + # Unique log path for this module + log_path = os.path.join( + workdir, + f'log_{module_name}.log', + ) + + cmd = [ + self._propeller_prof_gen_path, + f'--profile={current_perf_profile}', + f'--binary={self._initial_clang_path}', + f'--cc_profile={self._cc_profile_path}', + f'--ld_profile={self._ld_profile_path}', + '--alsologtostderr', + ] + + propeller_options = [ + 'inter_function_reordering: true', + 'split_all_basic_blocks: true', + ] + + if tf_policy_path: + propeller_options.append(f"policy_path: '{tf_policy_path}'") + + # Always generate logs to discover keys + cmd.append(f'--training_log={log_path}') + + cmd.append('--use_ml') + + options_str = f"code_layout_params {{ {', '.join(propeller_options)} }}" + cmd.append(f'--propeller_options={options_str}') + + # Run the tool + try: + compilation_runner.start_cancellable_process( + cmd, + timeout=self._compilation_timeout, + cancellation_manager=self._cancellation_manager, + ) + except Exception as e: + print(f'Error running Propeller for {module_name}: {e}') + return {} + + # Calculate Reward (Placeholder) + reward = 0.0 + + # Read the generated trace + if not os.path.exists(log_path): + return {} + + log_result = log_reader.read_log_as_sequence_examples(log_path) + if not log_result: + return {} + + for func_name, sequence_example in log_result.items(): + key = f'{module_name}/{func_name}' + if reward_only: + results[key] = (None, reward) + else: + results[key] = (sequence_example, reward) + + return results