1
Fork 0

fix(ppo): added message_passer to ppo

This commit is contained in:
Robin Meersman 2026-05-05 18:22:52 +02:00
parent 079480324d
commit af68a0064c
4 changed files with 27 additions and 38 deletions

View file

@ -40,16 +40,17 @@ class Actor(nn.Module):
class MessagePasser(nn.Module): class MessagePasser(nn.Module):
hidden_dim: int hidden_dim: int
num_propagation_steps: int num_propagation_steps: int
adj_matrix: jnp.ndarray
@nn.compact @nn.compact
def __call__(self, x: jnp.ndarray, adj_matrix: jnp.ndarray): def __call__(self, x: jnp.ndarray):
for _ in range(self.num_propagation_steps): for _ in range(self.num_propagation_steps):
# (n_nodes, feat) # (n_nodes, feat)
messages = nn.Dense(self.hidden_dim)(x) messages = nn.Dense(self.hidden_dim)(x)
messages = nn.tanh(messages) messages = nn.tanh(messages)
# note: if mean is wanted: adj_matrix / (adj.sum(axis=-1, keepdims=True) + 1e-8) # note: if mean is wanted: adj_matrix / (adj.sum(axis=-1, keepdims=True) + 1e-8)
agg = adj_matrix agg = self.adj_matrix
aggregated = agg @ messages aggregated = agg @ messages
x_concat = jnp.concatenate([x, aggregated], axis=-1) x_concat = jnp.concatenate([x, aggregated], axis=-1)

View file

@ -72,6 +72,10 @@ def create_obs_processor(
padded[key] = arr padded[key] = arr
return padded return padded
# TODO
def _split_to_agents() -> dict:
return {}
def _flatten_features(obs: dict) -> jnp.ndarray: def _flatten_features(obs: dict) -> jnp.ndarray:
ordered_keys = [ ordered_keys = [
"disk_z_tilt", "disk_z_tilt",
@ -93,7 +97,7 @@ def create_obs_processor(
if v.size == 0: if v.size == 0:
continue continue
# reshape scalars # reshape scalars to 1D array
if v.ndim == 0: if v.ndim == 0:
v = v.reshape(1) v = v.reshape(1)
@ -102,19 +106,8 @@ def create_obs_processor(
values.append(v.reshape(1, -1)) # (1, feat) values.append(v.reshape(1, -1)) # (1, feat)
continue continue
# -------- SPLIT TO SEGMENTS -------- # -------- SCALE WITH SEGMENTS --------
if key in _JOINT_SCALED_KEYS: if key in _JOINT_SCALED_KEYS:
if morph_mode == MorphMode.SEGMENT:
joint_count = 3
axis_per_joint = 2
center_size = num_arms * joint_count * axis_per_joint
v_center = v[:center_size].reshape(
num_arms, joint_count * axis_per_joint
) # (arms, 6)
v_segs = v[center_size:].reshape(-1, 2) # (segs, 2)
values.append(jnp.concatenate([v_center, v_segs], axis=0)) # (arms+segs, ?)
continue
v = v.reshape(num_arms, -1) # (n_arms, 2) v = v.reshape(num_arms, -1) # (n_arms, 2)
elif key in _SEGMENT_SCALED_KEYS: elif key in _SEGMENT_SCALED_KEYS:
@ -122,25 +115,20 @@ def create_obs_processor(
else: else:
# global key, broadcast to all nodes # global key, broadcast to all nodes
n_nodes = (num_segments + num_arms) if morph_mode == MorphMode.SEGMENT else num_arms v = jnp.repeat(v[None, :], num_arms, axis=0) # (num_arms, feat)
v = jnp.repeat(v[None, :], n_nodes, axis=0) # (n_nodes, feat)
# -------- SEGMENT MODE -------- # RING + FULLY CONNECTED
if morph_mode == MorphMode.SEGMENT:
values.append(v) # (n_nodes, feat)
continue
# -------- ARM MODE --------
v = v.reshape(num_arms, -1) v = v.reshape(num_arms, -1)
values.append(v) # (n_arms, feat) values.append(v) # (n_arms, feat)
return jnp.concatenate(values, axis=-1) return jnp.concatenate(values, axis=-1) # (agent, feat)
def _process_single(obs_dict: dict) -> jnp.ndarray: def _process_single(obs_dict: dict) -> jnp.ndarray:
processed = _add_derived_features(obs_dict) processed = _add_derived_features(obs_dict) # key |--> (feat-count, feat-lengths, )
processed = _normalize_features(processed) processed = _normalize_features(processed) # key |--> (feat-count, feat-lengths, )
# TODO: split to agents # key |--> (agents, feat-count, feat-lengths)
if padding_masks is not None: if padding_masks is not None:
processed = _pad_features(processed) processed = _pad_features(processed) # key |--> (agents, feat-count, feat-lengths')
return _flatten_features(processed) return _flatten_features(processed) # (agents, feat)
return jax.jit(jax.vmap(_process_single)) return jax.jit(jax.vmap(_process_single))

View file

@ -119,7 +119,7 @@ def get_action_and_value(
): ):
hidden_sensor = sensor_apply(params["sensor_params"], x) hidden_sensor = sensor_apply(params["sensor_params"], x)
hidden_critic = feature_extractor_apply(params["feature_extractor_params"], x) hidden_critic = feature_extractor_apply(params["feature_extractor_params"], x)
hidden_sensor = message_passer(hidden_sensor) hidden_sensor = message_passer(params["message_passer_params"], hidden_sensor)
debug.callback(logger.debug, f"[SHAPE] hidden_sensor: {hidden_sensor.shape}") debug.callback(logger.debug, f"[SHAPE] hidden_sensor: {hidden_sensor.shape}")
debug.callback(logger.debug, f"[SHAPE] hidden_critic: {hidden_critic.shape}") debug.callback(logger.debug, f"[SHAPE] hidden_critic: {hidden_critic.shape}")

View file

@ -62,7 +62,6 @@ def _get_action_and_value_noise(
key, key,
action_low, action_low,
action_high, action_high,
adj_matrix: jnp.ndarray,
): ):
# (B, n_nodes, feat) # (B, n_nodes, feat)
hidden = apply_per_node(sensor, agent_state.params["sensor_params"], next_obs) hidden = apply_per_node(sensor, agent_state.params["sensor_params"], next_obs)
@ -70,7 +69,7 @@ def _get_action_and_value_noise(
if message_passer is not None: if message_passer is not None:
params = agent_state.params["message_passer_params"] params = agent_state.params["message_passer_params"]
# (n_nodes, feat) --> let each node talk with its neighbours ==> vmap over B dimension # (n_nodes, feat) --> let each node talk with its neighbours ==> vmap over B dimension
hidden = jax.vmap(lambda x: message_passer.apply(params, x, adj_matrix))(hidden) hidden = jax.vmap(lambda x: message_passer.apply(params, x))(hidden)
hidden_critic = apply_shared( hidden_critic = apply_shared(
feature_extractor, agent_state.params["feature_extractor_params"], next_obs feature_extractor, agent_state.params["feature_extractor_params"], next_obs
@ -100,7 +99,6 @@ def _step_once(
carry, carry,
_, _,
env_step_fn, env_step_fn,
adj_matrix,
sensor: nn.Module, sensor: nn.Module,
feature_extractor: nn.Module, feature_extractor: nn.Module,
actor: nn.Module, actor: nn.Module,
@ -121,7 +119,6 @@ def _step_once(
key, key,
action_low, action_low,
action_high, action_high,
adj_matrix,
) )
logger11.debug(f"[_step_once] raw_action: {raw_action.shape}") logger11.debug(f"[_step_once] raw_action: {raw_action.shape}")
logger11.debug(f"[_step_once] clipped_action: {flat_clipped_action.shape}") logger11.debug(f"[_step_once] clipped_action: {flat_clipped_action.shape}")
@ -242,7 +239,6 @@ def _rollout_jit(
message_passer: Optional[nn.Module], message_passer: Optional[nn.Module],
action_low, action_low,
action_high, action_high,
adj_matrix,
): ):
(agent_state, episode_stats, next_obs, next_done, key, env_state), storage = jax.lax.scan( (agent_state, episode_stats, next_obs, next_done, key, env_state), storage = jax.lax.scan(
partial( partial(
@ -255,7 +251,6 @@ def _rollout_jit(
env_step_fn=step_env_fn, env_step_fn=step_env_fn,
action_low=action_low, action_low=action_low,
action_high=action_high, action_high=action_high,
adj_matrix=adj_matrix,
), ),
(agent_state, episode_stats, next_obs, next_done, key, env_state), (agent_state, episode_stats, next_obs, next_done, key, env_state),
(), (),
@ -393,7 +388,6 @@ class PPOTrainer:
message_passer=self.message_passer, message_passer=self.message_passer,
action_low=action_low, action_low=action_low,
action_high=action_high, action_high=action_high,
adj_matrix=self.adj,
) )
) )
self._compute_gae_jit = logged_jit( self._compute_gae_jit = logged_jit(
@ -419,7 +413,13 @@ class PPOTrainer:
def apply_feature(p, x): def apply_feature(p, x):
return apply_shared(self.feature_extractor, p, x) return apply_shared(self.feature_extractor, p, x)
self._ppo = PPO(self.ppo, apply_sensor, apply_actor, apply_critic, apply_feature) def apply_message_passer(p, x):
assert self.message_passer is not None
return jax.vmap(lambda x_in: self.message_passer.apply(p, x_in))(x)
self._ppo = PPO(
self.ppo, apply_sensor, apply_actor, apply_critic, apply_feature, apply_message_passer
)
self.agent_state = self._init_agent_state() self.agent_state = self._init_agent_state()
@ -453,6 +453,7 @@ class PPOTrainer:
MessagePasser( MessagePasser(
hidden_dim=300, hidden_dim=300,
num_propagation_steps=self.cfg.architecture.message_passing_steps or 4, num_propagation_steps=self.cfg.architecture.message_passing_steps or 4,
adj_matrix=self.adj,
) )
if self.morph_mode != MorphMode.CENTRALIZED if self.morph_mode != MorphMode.CENTRALIZED
else None else None
@ -517,7 +518,6 @@ class PPOTrainer:
message_passer_params = self.message_passer.init( message_passer_params = self.message_passer.init(
message_passer_key, message_passer_key,
self.sensor.apply(single_sensor_param, sample_obs), self.sensor.apply(single_sensor_param, sample_obs),
self.adj,
) )
self.logger.debug( self.logger.debug(
f"[_init_agent_state] message_passer_params: { f"[_init_agent_state] message_passer_params: {