1use rlx_compile::legalize_broadcast;
18use rlx_ir::infer::GraphExt;
19use rlx_ir::{DType, Graph, NodeId, Op, Shape};
20
21use crate::graph::actor::WeightStore;
22use crate::graph::mlp::{ParamSlot, concat_features, init_mat, init_vec, mlp_trunk};
23use crate::spec::RlSpec;
24
25#[derive(Debug, Clone)]
26pub struct CriticGraphBundle {
27 pub forward: Graph,
28 pub train: CriticTrainGraph,
29 pub q_grad: CriticQGradGraph,
30}
31
32#[derive(Debug, Clone)]
33pub struct CriticTrainGraph {
34 pub graph: Graph,
35 pub d_output: NodeId,
36 pub loss: NodeId,
37 pub params: Vec<ParamSlot>,
38}
39
40#[derive(Debug, Clone)]
41pub struct CriticQGradGraph {
42 pub graph: Graph,
43 pub d_output: NodeId,
44 pub action: NodeId,
45 pub grad_action: NodeId,
46}
47
48pub fn build_critic_graphs(spec: &RlSpec) -> CriticGraphBundle {
49 CriticGraphBundle {
50 forward: legalize_broadcast::run(build_forward(spec)),
51 train: build_train(spec),
52 q_grad: build_q_grad(spec),
53 }
54}
55
56pub struct CompiledTwinCritic {
57 pub forward: rlx_runtime::CompiledGraph,
58 pub train: rlx_runtime::CompiledGraph,
59 pub q_grad: rlx_runtime::CompiledGraph,
60 pub train_meta: CriticTrainGraph,
61 pub q_grad_meta: CriticQGradGraph,
62}
63
64impl CompiledTwinCritic {
65 pub fn compile(session: &rlx_runtime::Session, bundle: &CriticGraphBundle) -> Self {
66 Self {
67 forward: session.compile(bundle.forward.clone()),
68 train: session.compile(bundle.train.graph.clone()),
69 q_grad: session.compile(bundle.q_grad.graph.clone()),
70 train_meta: bundle.train.clone(),
71 q_grad_meta: bundle.q_grad.clone(),
72 }
73 }
74
75 pub fn set_weights(&mut self, weights: &WeightStore) {
76 for g in [&mut self.forward, &mut self.train, &mut self.q_grad] {
77 weights.apply(g);
78 }
79 }
80
81 pub fn q_values(&mut self, state: &[f32], action: &[f32]) -> (f32, f32) {
82 let outs = self.forward.run(&[("state", state), ("action", action)]);
83 let q1 = outs.first().map(|v| v[0]).unwrap_or(0.0);
84 let q2 = outs.get(1).map(|v| v[0]).unwrap_or(0.0);
85 (q1, q2)
86 }
87
88 pub fn action_grad(&mut self, state: &[f32], action: &[f32]) -> Vec<f32> {
89 let outs = self.q_grad.run(&[
90 ("state", state),
91 ("action", action),
92 ("d_output", &[1.0f32]),
93 ]);
94 outs.into_iter().nth(1).unwrap_or_default()
95 }
96}
97
98pub fn init_critic_weights(spec: &RlSpec, seed: u64) -> WeightStore {
99 let mut w = WeightStore::default();
100 let mut s = seed.wrapping_add(0x000C_171C);
101 let mut in_d = spec.critic_in_dim();
102 for (li, &hd) in spec.hidden.iter().enumerate() {
103 init_mat(&mut w, &format!("critic_w{li}"), in_d, hd, &mut s);
104 init_vec(&mut w, &format!("critic_b{li}"), hd, &mut s);
105 in_d = hd;
106 }
107 init_mat(&mut w, "critic_q1_w", in_d, 1, &mut s);
108 init_vec(&mut w, "critic_q1_b", 1, &mut s);
109 init_mat(&mut w, "critic_q2_w", in_d, 1, &mut s);
110 init_vec(&mut w, "critic_q2_b", 1, &mut s);
111 w
112}
113
114fn build_forward(spec: &RlSpec) -> Graph {
115 let f = DType::F32;
116 let b = spec.batch;
117 let sd = spec.state_dim;
118 let ad = spec.action_dim;
119 let mut g = Graph::new("critic_forward");
120 let mut params = Vec::new();
121
122 let state = g.input("state", Shape::new(&[b, sd], f));
123 let action = g.input("action", Shape::new(&[b, ad], f));
124 let feats = concat_features(&mut g, vec![state, action]);
125 let (h, in_d) = mlp_trunk(
126 &mut g,
127 feats,
128 spec.critic_in_dim(),
129 &spec.hidden,
130 "critic",
131 &mut params,
132 );
133 let q1 = q_head(&mut g, h, in_d, "critic_q1", b, &mut params);
134 let q2 = q_head(&mut g, h, in_d, "critic_q2", b, &mut params);
135 let _ = params;
136 g.set_outputs(vec![q1, q2]);
137 g
138}
139
140fn q_head(
141 g: &mut Graph,
142 h: NodeId,
143 in_d: usize,
144 prefix: &str,
145 batch: usize,
146 params: &mut Vec<ParamSlot>,
147) -> NodeId {
148 let f = DType::F32;
149 let w_name = format!("{prefix}_w");
150 let b_name = format!("{prefix}_b");
151 let w = g.param(&w_name, Shape::new(&[in_d, 1], f));
152 let b = g.param(&b_name, Shape::new(&[1], f));
153 params.push(ParamSlot {
154 name: w_name,
155 shape: vec![in_d, 1],
156 param: w,
157 grad: None,
158 });
159 params.push(ParamSlot {
160 name: b_name,
161 shape: vec![1],
162 param: b,
163 grad: None,
164 });
165 let q = g.mm(h, w);
166 let q = g.add(q, b);
167 g.reshape_(q, vec![batch as i64, 1])
168}
169
170fn build_train(spec: &RlSpec) -> CriticTrainGraph {
171 let f = DType::F32;
172 let b = spec.batch;
173 let sd = spec.state_dim;
174 let ad = spec.action_dim;
175 let mut g = Graph::new("critic_train");
176 let mut params = Vec::new();
177
178 let state = g.input("state", Shape::new(&[b, sd], f));
179 let action = g.input("action", Shape::new(&[b, ad], f));
180 let target = g.input("target", Shape::new(&[b, 1], f));
181
182 let feats = concat_features(&mut g, vec![state, action]);
183 let (h, in_d) = mlp_trunk(
184 &mut g,
185 feats,
186 spec.critic_in_dim(),
187 &spec.hidden,
188 "critic",
189 &mut params,
190 );
191 let q1 = q_head(&mut g, h, in_d, "critic_q1", b, &mut params);
192 let err = g.sub(q1, target);
193 let sq = g.mul(err, err);
194 let loss = g.mean(sq, vec![0, 1], false);
195 g.set_outputs(vec![loss]);
196 finalize_critic_train(g, params, loss)
197}
198
199fn build_q_grad(spec: &RlSpec) -> CriticQGradGraph {
200 let f = DType::F32;
201 let b = spec.batch;
202 let sd = spec.state_dim;
203 let ad = spec.action_dim;
204 let mut g = Graph::new("critic_q_grad");
205
206 let state = g.input("state", Shape::new(&[b, sd], f));
207 let action = g.input("action", Shape::new(&[b, ad], f));
208 let feats = concat_features(&mut g, vec![state, action]);
209 let mut params = Vec::new();
210 let (h, in_d) = mlp_trunk(
211 &mut g,
212 feats,
213 spec.critic_in_dim(),
214 &spec.hidden,
215 "critic",
216 &mut params,
217 );
218 let q1 = q_head(&mut g, h, in_d, "critic_q1", b, &mut params);
219 let loss = g.sum(q1, vec![0, 1], false);
220 g.set_outputs(vec![loss]);
221 let _ = params;
222
223 let (g, remap) = legalize_broadcast::run_with_remap(g);
224 let action = remap[&action];
225 let bwd = rlx_autodiff::grad_with_loss(&g, &[action]);
226 let d_output = bwd
227 .nodes()
228 .iter()
229 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
230 .map(|n| n.id)
231 .expect("d_output");
232 let grad_action = bwd.outputs[1];
233
234 CriticQGradGraph {
235 graph: bwd,
236 d_output,
237 action,
238 grad_action,
239 }
240}
241
242fn finalize_critic_train(g: Graph, params: Vec<ParamSlot>, loss_fwd: NodeId) -> CriticTrainGraph {
243 let (g, remap) = legalize_broadcast::run_with_remap(g);
244 let params: Vec<ParamSlot> = params
245 .into_iter()
246 .map(|mut p| {
247 p.param = remap[&p.param];
248 p
249 })
250 .collect();
251 let loss_fwd = remap[&loss_fwd];
252 let wrt: Vec<NodeId> = params.iter().map(|p| p.param).collect();
253 let bwd = rlx_autodiff::grad_with_loss(&g, &wrt);
254 let d_output = bwd
255 .nodes()
256 .iter()
257 .find(|n| matches!(&n.op, Op::Input { name } if name == "d_output"))
258 .map(|n| n.id)
259 .expect("d_output");
260 let grad_ids: Vec<NodeId> = bwd.outputs[1..=params.len()].to_vec();
261 let params: Vec<ParamSlot> = params
262 .into_iter()
263 .zip(grad_ids)
264 .map(|(mut p, grad)| {
265 p.grad = Some(grad);
266 p
267 })
268 .collect();
269 CriticTrainGraph {
270 graph: bwd,
271 d_output,
272 loss: loss_fwd,
273 params,
274 }
275}