tracel-ai/burn · critical

Autobatcher should be able to send current policy state.

Error message

Autobatcher should be able to send current policy state.

What it means

This panic fires inside the dedicated autobatcher thread when it handles a PolicyRequest: it calls autobatcher.state() and tries to send the resulting PolicyState back to the caller over the one-shot mpsc channel. The expect triggers when that receiver has already been dropped — i.e. the AsyncPolicy::state() caller gave up (its thread ended or the channel was dropped) before the autobatcher got around to answering. Because the expect is inside the server loop, this panic kills the whole inference thread, after which every other AsyncPolicy method will also panic.

Source

Thrown at crates/burn-rl/src/policy/async_policy.rs:220

    ///
    /// # Arguments
    ///
    /// * `autobatch_size` - Number of observations to accumulate before running a pass of inference.
    /// * `inner_policy` - The policy used to take actions.
    pub fn new(autobatch_size: usize, inner_policy: P) -> Self {
        let (sender, receiver) = std::sync::mpsc::channel();
        let mut autobatcher = PolicyInferenceServer::new(autobatch_size, inner_policy.clone());
        spawn(move || {
            loop {
                match receiver.recv() {
                    Ok(msg) => match msg {
                        InferenceMessage::ActionMessage(item) => autobatcher.push_action(item),
                        InferenceMessage::ForwardMessage(item) => autobatcher.push_logits(item),
                        InferenceMessage::PolicyUpdate(update) => autobatcher.update_policy(update),
                        InferenceMessage::ToDevice(device) => autobatcher.policy_to_device(&device),
                        InferenceMessage::PolicyRequest(sender) => sender
                            .send(autobatcher.state())
                            .expect("Autobatcher should be able to send current policy state."),
                        InferenceMessage::IncrementAgents(num) => autobatcher.increment_agents(num),
                        InferenceMessage::DecrementAgents(num) => autobatcher.decrement_agents(num),
                    },
                    Err(err) => {
                        log::error!("Error in AsyncPolicy : {}", err);
                        break;
                    }
                }
            }
        });

        Self {
            inference_state_sender: sender,
        }
    }

    /// Increment the number of agents using the inference server.
    pub fn increment_agents(&self, num: usize) {

View on GitHub (pinned to d16f7ba2ed)

Solutions

  1. Ensure the thread that calls state() blocks on receiver.recv() until it gets a reply — do not drop the AsyncPolicy handle or unwind before recv() returns.
  2. Wrap the autobatcher-side send so a dead reply channel does not kill the inference thread: replace .expect(...) with if let Err(err) = sender.send(...) { log::warn!(...); } since a dropped receiver is non-fatal for the server.
  3. Check for earlier panics/logs from the autobatcher thread (e.g. inner_policy.state() failing) that delay or abort the reply.
  4. If using timeouts around state(), poll recv_timeout on the caller side but keep the receiver alive, or re-issue the PolicyRequest after a timeout instead of abandoning it.

Example fix

// before
InferenceMessage::PolicyRequest(sender) => sender
    .send(autobatcher.state())
    .expect("Autobatcher should be able to send current policy state."),
// after
InferenceMessage::PolicyRequest(sender) => {
    if let Err(err) = sender.send(autobatcher.state()) {
        log::warn!("Policy requester dropped before receiving state: {}", err);
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Caller side: keep the receiver alive until the reply arrives
let (tx, rx) = std::sync::mpsc::channel();
policy_handle_send(PolicyRequest(tx)); // must not drop tx/rx before recv
assert!(!std::thread::current().is_panicking());

Try / catch

let state = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| policy.state()))
    .map_err(|_| anyhow::anyhow!("inference server thread died while snapshotting state"))?;

Prevention

When it happens

Trigger: Calling AsyncPolicy::state() from a scope where the returned mpsc::Receiver can be dropped before the autobatcher processes the PolicyRequest — e.g. calling state() on a cloned AsyncPolicy inside a short-lived worker thread that is cancelled/timed out, or wrapping state() in a timeout that abandons the receiver while the inner policy's state() call (possibly a device copy) is still pending.

Common situations: RL training loops that snapshot policy state from rayon/tokio worker tasks with timeouts; dropping an AsyncPolicy clone while a PolicyRequest is still queued behind a large action batch, so the reply channel dies before the server reaches the request; panics or unwinding in the calling thread between send and recv.

Related errors


AI-assisted analysis of tracel-ai/burn@d16f7ba2ed (2026-09-05). Data as JSON: /api/errors/776d7954f76f8da6. Report an issue: GitHub.