@@ -8,7 +8,7 @@ use std::net::SocketAddr;
88use std:: sync:: Arc ;
99
1010use axum:: {
11- extract:: { Path , Request , State } ,
11+ extract:: { DefaultBodyLimit , Path , Request , State } ,
1212 http:: StatusCode ,
1313 middleware:: { self , Next } ,
1414 response:: { IntoResponse , Response } ,
@@ -101,7 +101,20 @@ pub struct ErrorResponse {
101101}
102102
103103/// Start the prover server
104- pub async fn run_server ( port : u16 ) -> anyhow:: Result < ( ) > {
104+ /// Maximum accepted request body.
105+ ///
106+ /// Proving requests scale with the batch: the zk input alone is 64 bytes per
107+ /// handle, so a 50k-handle commitment is ~3 MB before the exclusion proof, and
108+ /// axum's 2 MB default rejects it with a 413 that reads like a prover fault.
109+ /// Bodies are buffered in memory, so this is a memory bound as much as a
110+ /// policy one — generous rather than tight, since the failure mode of setting
111+ /// it too low is a rejected commitment, and PROVER_AUTH_TOKEN gates the port.
112+ ///
113+ /// Kept in step with MAX_BODY_BYTES in the runpod proxy: whichever hop has the
114+ /// lower ceiling is the one that 413s.
115+ const MAX_BODY_BYTES : usize = 512 * 1024 * 1024 ;
116+
117+ pub async fn run_server ( port : u16 , no_calibrate : bool ) -> anyhow:: Result < ( ) > {
105118 // Initialize tracing
106119 tracing_subscriber:: fmt ( )
107120 . with_env_filter (
@@ -116,28 +129,34 @@ pub async fn run_server(port: u16) -> anyhow::Result<()> {
116129 // Create shared state
117130 let state = Arc :: new ( ServerState :: new ( tx) ) ;
118131
119- // Calibrate proving throughput on startup
120- tracing:: info!( "Calibrating proving throughput..." ) ;
121- let calibrate_state = state. clone ( ) ;
122- let calibrate_handle = tokio:: task:: spawn_blocking ( move || {
123- let prover = Prover :: new ( ) ;
124- prover. calibrate ( )
125- } ) ;
126- match calibrate_handle. await {
127- Ok ( Ok ( info) ) => {
128- tracing:: info!(
129- "Calibration complete: {:.2}s per segment at po2={}, {:.0} cycles/sec" ,
130- info. seconds_per_segment,
131- info. calibration_po2,
132- info. cycles_per_sec,
133- ) ;
134- * calibrate_state. calibration . write ( ) . await = Some ( info) ;
135- }
136- Ok ( Err ( e) ) => {
137- tracing:: warn!( "Calibration failed (estimates will be unavailable): {}" , e) ;
138- }
139- Err ( e) => {
140- tracing:: warn!( "Calibration task panicked: {}" , e) ;
132+ // Calibrate proving throughput on startup. This blocks the listener, so
133+ // /health stays unanswered until it completes — deliberate, since an
134+ // estimate is useless before it, but billable on a short-lived pod.
135+ if no_calibrate {
136+ tracing:: info!( "Calibration skipped (--no-calibrate); /estimate will be unavailable" ) ;
137+ } else {
138+ tracing:: info!( "Calibrating proving throughput..." ) ;
139+ let calibrate_state = state. clone ( ) ;
140+ let calibrate_handle = tokio:: task:: spawn_blocking ( move || {
141+ let prover = Prover :: new ( ) ;
142+ prover. calibrate ( )
143+ } ) ;
144+ match calibrate_handle. await {
145+ Ok ( Ok ( info) ) => {
146+ tracing:: info!(
147+ "Calibration complete: {:.2}s per segment at po2={}, {:.0} cycles/sec" ,
148+ info. seconds_per_segment,
149+ info. calibration_po2,
150+ info. cycles_per_sec,
151+ ) ;
152+ * calibrate_state. calibration . write ( ) . await = Some ( info) ;
153+ }
154+ Ok ( Err ( e) ) => {
155+ tracing:: warn!( "Calibration failed (estimates will be unavailable): {}" , e) ;
156+ }
157+ Err ( e) => {
158+ tracing:: warn!( "Calibration task panicked: {}" , e) ;
159+ }
141160 }
142161 }
143162
@@ -150,7 +169,9 @@ pub async fn run_server(port: u16) -> anyhow::Result<()> {
150169 // Optional bearer-token auth. If PROVER_AUTH_TOKEN is set, every route
151170 // (including /health) requires `Authorization: Bearer <token>` — that
152171 // way a successful /health probe also confirms auth is wired correctly.
153- let auth_token = std:: env:: var ( "PROVER_AUTH_TOKEN" ) . ok ( ) . filter ( |s| !s. is_empty ( ) ) ;
172+ let auth_token = std:: env:: var ( "PROVER_AUTH_TOKEN" )
173+ . ok ( )
174+ . filter ( |s| !s. is_empty ( ) ) ;
154175 if auth_token. is_some ( ) {
155176 tracing:: info!( "PROVER_AUTH_TOKEN set, requiring bearer auth on all routes" ) ;
156177 }
@@ -161,7 +182,8 @@ pub async fn run_server(port: u16) -> anyhow::Result<()> {
161182 . route ( "/estimate" , post ( submit_estimate) )
162183 . route ( "/compress" , post ( submit_compress) )
163184 . route ( "/jobs/:job_id" , get ( get_job_status) )
164- . route ( "/jobs/:job_id/receipt" , get ( get_job_receipt) ) ;
185+ . route ( "/jobs/:job_id/receipt" , get ( get_job_receipt) )
186+ . route ( "/calibration" , get ( get_calibration) ) ;
165187 if let Some ( token) = auth_token {
166188 app = app. layer ( middleware:: from_fn ( move |req : Request , next : Next | {
167189 let token = token. clone ( ) ;
@@ -170,6 +192,7 @@ pub async fn run_server(port: u16) -> anyhow::Result<()> {
170192 }
171193
172194 let app = app
195+ . layer ( DefaultBodyLimit :: max ( MAX_BODY_BYTES ) )
173196 . layer ( TraceLayer :: new_for_http ( ) )
174197 . layer (
175198 CorsLayer :: new ( )
@@ -192,6 +215,29 @@ pub async fn run_server(port: u16) -> anyhow::Result<()> {
192215}
193216
194217/// Health check endpoint
218+ /// GET /calibration - Measured proving throughput of this machine.
219+ ///
220+ /// The number that characterises a GPU for cost purposes: cost per proof is
221+ /// total_proving_cycles / cycles_per_sec. Previously only reachable by reading
222+ /// the startup log line or running `subs-prover bench` on the box.
223+ ///
224+ /// 503 when calibration was skipped or failed.
225+ async fn get_calibration ( State ( state) : State < Arc < ServerState > > ) -> impl IntoResponse {
226+ match state. calibration . read ( ) . await . clone ( ) {
227+ Some ( info) => Json ( serde_json:: json!( {
228+ "seconds_per_segment" : info. seconds_per_segment,
229+ "calibration_po2" : info. calibration_po2,
230+ "cycles_per_sec" : info. cycles_per_sec,
231+ } ) )
232+ . into_response ( ) ,
233+ None => (
234+ StatusCode :: SERVICE_UNAVAILABLE ,
235+ "calibration unavailable (skipped or failed)" ,
236+ )
237+ . into_response ( ) ,
238+ }
239+ }
240+
195241async fn health ( ) -> & ' static str {
196242 "ok"
197243}
@@ -435,10 +481,7 @@ async fn get_job_receipt(
435481 tracing:: info!( "Job {} receipt pulled, removing job" , job_id) ;
436482 (
437483 StatusCode :: OK ,
438- [ (
439- axum:: http:: header:: CONTENT_TYPE ,
440- "application/octet-stream" ,
441- ) ] ,
484+ [ ( axum:: http:: header:: CONTENT_TYPE , "application/octet-stream" ) ] ,
442485 receipt,
443486 )
444487 . into_response ( )
0 commit comments