1- import { useCallback , useReducer , useState } from "react" ;
2- import { ReactFlowProvider , type Edge , type Node } from "@xyflow/react" ;
1+ import { useCallback , useEffect , useReducer , useRef , useState } from "react" ;
2+ import {
3+ ReactFlowProvider , type Edge , type Node ,
4+ } from "@xyflow/react" ;
35
46import { FlowCanvas } from "@/components/FlowCanvas" ;
57import { Palette } from "@/components/Palette" ;
68import { Sidebar } from "@/components/Sidebar" ;
79import { TopBar , type RunMode } from "@/components/TopBar" ;
810import { BottomStrip } from "@/components/BottomStrip" ;
11+ import { AppTabs , type AppTab } from "@/components/AppTabs" ;
12+ import { RunResultModal , type RunReport } from "@/components/RunResultModal" ;
13+ import { TokenizerPlayground } from "@/components/TokenizerPlayground" ;
14+ import { DataInspector } from "@/components/DataInspector" ;
15+
16+ import { useRpc } from "@/hooks/useRpc" ;
17+ import { useVerifyAfter } from "@/hooks/useVerifyAfter" ;
918
1019import {
1120 INITIAL_SPEC , specReducer , type TopologyFactory ,
@@ -23,12 +32,138 @@ const TOPOLOGIES: readonly TopologyFactory[] = [
2332 "gb10_quarter" , "tpu_v6e_8" , "tpu_v5p_4" , "m3_ultra_solo" ,
2433] ;
2534
35+ // Mini-spec used for preset expansion in the GUI. Matches E2EMatrix.md §3.1.
36+ const MINI_HIDDEN = 128 ;
37+ const MINI_DEPTH = 2 ;
38+ const MINI_DIM_ENV = {
39+ B : 1 , S : 64 , H : MINI_HIDDEN ,
40+ nh : 2 , nkv : 1 , head_dim : 64 ,
41+ num_experts : 4 , top_k : 2 ,
42+ } ;
43+
44+ const SMOKE_STAGES = [
45+ "parse" , "verify_build_spec" , "apply_rewrites" , "resolve_shapes" ,
46+ "estimate_memory" , "check_gotchas" , "build_model" , "dry_forward" ,
47+ ] ;
48+ const FULL_STAGES = [
49+ ...SMOKE_STAGES , "input_parity_check" , "loss_smoke" , "optimizer_smoke" ,
50+ ] ;
51+ const TRAIN_STAGES = [ ...FULL_STAGES , "train" ] ;
52+
53+ interface BrickSpec {
54+ kind : string ;
55+ name ?: string ;
56+ params ?: Record < string , unknown > ;
57+ }
58+
59+ function presetSpecsToNodes ( specs : BrickSpec [ ] ) : { nodes : Node [ ] ; edges : Edge [ ] } {
60+ const nodes : Node [ ] = [ ] ;
61+ const edges : Edge [ ] = [ ] ;
62+ let x = 60 , y = 60 ;
63+ let lastName : string | null = null ;
64+ for ( const s of specs ) {
65+ const name = s . name ?? `${ s . kind } _${ nodes . length } ` ;
66+ nodes . push ( {
67+ id : name ,
68+ type : "brick" ,
69+ position : { x, y } ,
70+ data : { kind : s . kind , params : s . params ?? { } } as never ,
71+ } ) ;
72+ if ( lastName ) {
73+ edges . push ( {
74+ id : `${ lastName } ->${ name } ` ,
75+ source : lastName ,
76+ target : name ,
77+ data : { severity : "info" } ,
78+ } ) ;
79+ }
80+ lastName = name ;
81+ x += 220 ;
82+ if ( x > 980 ) { x = 60 ; y += 140 ; }
83+ }
84+ return { nodes, edges } ;
85+ }
86+
2687export function App ( ) : JSX . Element {
2788 const [ nodes , setNodes ] = useState < Node [ ] > ( [ ] ) ;
2889 const [ edges , setEdges ] = useState < Edge [ ] > ( [ ] ) ;
2990 const [ projectName , setProjectName ] = useState ( "untitled" ) ;
30- const [ proposals ] = useState < ShardingProposalView [ ] > ( [ ] ) ;
91+ const [ proposals , setProposals ] = useState < ShardingProposalView [ ] > ( [ ] ) ;
3192 const [ spec , dispatch ] = useReducer ( specReducer , INITIAL_SPEC ) ;
93+ const [ activeTab , setActiveTab ] = useState < AppTab > ( "canvas" ) ;
94+ const [ runReport , setRunReport ] = useState < RunReport | null > ( null ) ;
95+ const [ runError , setRunError ] = useState < string | null > ( null ) ;
96+
97+ const rpc = useRpc ( {
98+ enableWs : true ,
99+ onBackendStatus : ( s ) => dispatch ( { type : "backend.status" , status : s } ) ,
100+ } ) ;
101+
102+ // Keep one stable spec snapshot for the verify debouncer to read.
103+ const wireSpecRef = useRef ( { nodes, edges, spec } ) ;
104+ useEffect ( ( ) => {
105+ wireSpecRef . current = { nodes, edges, spec } ;
106+ } , [ nodes , edges , spec ] ) ;
107+
108+ const runVerify = useCallback ( async ( ) => {
109+ const snap = wireSpecRef . current ;
110+ if ( snap . nodes . length === 0 ) return ;
111+ const params = buildVerifyParams ( snap . nodes , snap . edges , snap . spec ) ;
112+ try {
113+ const r = await rpc . call < {
114+ memory_distributed ?: { worst_rank ?: { total_bytes ?: number } ;
115+ fits_on_topology ?: boolean } ;
116+ memory_per_brick ?: Record < string , { params_bytes : number ;
117+ activations_bytes : number ;
118+ kv_cache_bytes : number } > ;
119+ gotchas ?: { id : string ; severity : "info" | "warning" | "error" ;
120+ message : string ; reference ?: string } [ ] ;
121+ elapsed_ms : number ;
122+ resolved ?: { edges ?: { src : string ; dst : string ;
123+ matched : boolean ;
124+ severity : "info" | "warning" | "error" } [ ] } ;
125+ } > ( "verify" , params ) ;
126+
127+ // Aggregate per-brick to a worst-rank-bytes proxy when no sharding.
128+ const total = sumPerBrick ( r . memory_per_brick ) ;
129+ const worst = r . memory_distributed ?. worst_rank ?. total_bytes ?? total ;
130+ dispatch ( { type : "memory.set" , worst_rank_bytes : worst } ) ;
131+ dispatch ( { type : "verify.complete" ,
132+ elapsed_ms : r . elapsed_ms ,
133+ brick_count : snap . nodes . length } ) ;
134+ if ( r . gotchas ) {
135+ dispatch ( { type : "gotchas.set" , gotchas : r . gotchas } ) ;
136+ }
137+ if ( r . resolved ?. edges ) {
138+ setEdges ( ( prev ) => recolorEdges ( prev , r . resolved ! . edges ! ) ) ;
139+ }
140+ } catch {
141+ // Backend down or invalid spec; leave state and let user retry.
142+ }
143+ } , [ rpc ] ) ;
144+
145+ const { schedule : scheduleVerify } = useVerifyAfter (
146+ wireSpecRef , runVerify , { debounceMs : 200 } ,
147+ ) ;
148+
149+ // Trigger verify only on inputs that the user controls. The verify
150+ // response writes back into spec AND edge severity — depending on
151+ // either of those in the effect would loop. Use structural keys.
152+ const nodesKey = nodes . map ( ( n ) => `${ n . id } :${ ( n . data as { kind ?: string } )
153+ ?. kind ?? "" } `) . join ( "|" ) ;
154+ const edgesKey = edges . map ( ( e ) => `${ e . source } >${ e . target } ` ) . join ( "|" ) ;
155+ const lossKey = `${ spec . loss . kind } ::${ spec . loss . head_outputs . join ( "," ) } ` ;
156+ const optimKey = `${ spec . optim . kind } ::${ spec . optim . groups . length } ` ;
157+ const shardingKey =
158+ `${ spec . sharding . topology } ::${ spec . sharding . compile_mode } ` +
159+ `::${ spec . sharding . axis_assignments . length } ` +
160+ `::${ spec . sharding . fp8_enabled ? 1 : 0 } ` ;
161+ const rewriterKey = spec . rewriters . map ( ( r ) => r . name ) . join ( "," ) ;
162+ useEffect ( ( ) => { scheduleVerify ( ) ; } ,
163+ [ nodesKey , edgesKey , lossKey , optimKey , shardingKey ,
164+ rewriterKey , scheduleVerify ] ) ;
165+
166+ // ----- Handlers ----------------------------------------------------------
32167
33168 const handleDropBrick = useCallback (
34169 ( kind : string , position : { x : number ; y : number } ) => {
@@ -48,14 +183,85 @@ export function App(): JSX.Element {
48183 ] ) ;
49184 } , [ ] ) ;
50185
51- const handlePresetDrop = useCallback ( ( _name : string ) => {
52- // F-A.2/F-D will resolve preset → specs via build_preset_specs RPC and
53- // fan out into Node[] additions. Stub for now.
54- } , [ ] ) ;
186+ const handlePresetDrop = useCallback ( async ( name : string ) => {
187+ try {
188+ const r = await rpc . call < { specs : BrickSpec [ ] ; preset_name : string } > (
189+ "build_preset_specs" ,
190+ { preset_name : name , hidden_size : MINI_HIDDEN ,
191+ num_layers : MINI_DEPTH } ,
192+ ) ;
193+ const { nodes : ns , edges : es } = presetSpecsToNodes ( r . specs ) ;
194+ setNodes ( ns ) ;
195+ setEdges ( es ) ;
196+ } catch ( e ) {
197+ setRunError ( String ( e ) ) ;
198+ }
199+ } , [ rpc ] ) ;
200+
201+ const requestSuggestSharding = useCallback ( async ( ) => {
202+ const snap = wireSpecRef . current ;
203+ if ( snap . nodes . length === 0 ) return ;
204+ try {
205+ const r = await rpc . call < {
206+ proposals : { strategy_name : string ; fits : boolean ;
207+ estimated_per_rank_bytes : number ; reason : string } [ ] ;
208+ } > ( "suggest_sharding" , {
209+ graph : nodesToGraph ( snap . nodes , snap . edges ) ,
210+ dim_env : MINI_DIM_ENV ,
211+ loss : { kind : snap . spec . loss . kind ,
212+ head_outputs : snap . spec . loss . head_outputs } ,
213+ optim : { kind : snap . spec . optim . kind ,
214+ groups : snap . spec . optim . groups . map ( ( g ) => ( {
215+ matcher : g . matcher , lr : g . lr ,
216+ weight_decay : g . weight_decay ,
217+ betas : g . betas ,
218+ } ) ) } ,
219+ topology : { factory : snap . spec . sharding . topology , kwargs : { } } ,
220+ } ) ;
221+ setProposals ( r . proposals . map ( ( p ) => ( {
222+ strategy_name : p . strategy_name , fits : p . fits ,
223+ estimated_per_rank_bytes : p . estimated_per_rank_bytes ,
224+ reason : p . reason ,
225+ } ) ) ) ;
226+ } catch { /* keep prior proposals on failure */ }
227+ } , [ rpc ] ) ;
228+
229+ useEffect ( ( ) => {
230+ void requestSuggestSharding ( ) ;
231+ } , [ requestSuggestSharding , spec . sharding . topology , nodes . length ] ) ;
55232
56- const handleRunPipeline = useCallback ( ( _mode : RunMode ) => {
57- // F-D wires this to JSON-RPC pipeline.run; for now we only flag intent.
58- } , [ ] ) ;
233+ const handleRunPipeline = useCallback ( async ( mode : RunMode ) => {
234+ const snap = wireSpecRef . current ;
235+ if ( snap . nodes . length === 0 ) {
236+ setRunError ( "canvas is empty — drop bricks or pick a preset first" ) ;
237+ setRunReport ( null ) ;
238+ return ;
239+ }
240+ setRunError ( null ) ;
241+ setRunReport ( null ) ;
242+ const stages = mode === "smoke" ? SMOKE_STAGES
243+ : mode === "full" ? FULL_STAGES : TRAIN_STAGES ;
244+ try {
245+ const r = await rpc . call < RunReport > ( "pipeline.run" , {
246+ spec : buildVerifyParams ( snap . nodes , snap . edges , snap . spec ) ,
247+ pipeline : { stages, stage_options : { } } ,
248+ } ) ;
249+ setRunReport ( r ) ;
250+ } catch ( e ) {
251+ setRunError ( String ( e ) ) ;
252+ }
253+ } , [ rpc ] ) ;
254+
255+ const handleShardingAccept = useCallback ( ( idx : number ) => {
256+ const chosen = proposals [ idx ] ;
257+ if ( ! chosen ) return ;
258+ // The proposal carries strategy + reason; backend already knows the
259+ // axis-assignments. We re-run verify so the new memory bar reflects.
260+ void scheduleVerify ( ) ;
261+ setRunReport ( null ) ;
262+ setRunError ( `sharding proposal "${ chosen . strategy_name } " applied — re-verifying` ) ;
263+ setTimeout ( ( ) => setRunError ( null ) , 2000 ) ;
264+ } , [ proposals , scheduleVerify ] ) ;
59265
60266 return (
61267 < ReactFlowProvider >
@@ -74,33 +280,110 @@ export function App(): JSX.Element {
74280 sharding : { ...spec . sharding , compile_mode : m } } ) }
75281 onRunPipeline = { handleRunPipeline }
76282 />
283+ < AppTabs active = { activeTab } onChange = { setActiveTab } />
77284 < div style = { { flex : 1 , display : "flex" , minHeight : 0 } } >
78- < Palette />
79- < FlowCanvas
80- nodes = { nodes } edges = { edges }
81- onConnect = { handleConnect }
82- onDropBrick = { handleDropBrick }
83- />
84- < Sidebar
85- loss = { spec . loss }
86- optim = { spec . optim }
87- rewriters = { spec . rewriters }
88- sharding = { spec . sharding }
89- gotchas = { spec . gotchas }
90- proposals = { proposals }
91- onLossApply = { ( l ) => dispatch ( { type : "loss.set" , loss : l } ) }
92- onOptimApply = { ( o ) => dispatch ( { type : "optim.set" , optim : o } ) }
93- onRewriterAdd = { ( r ) => dispatch ( { type : "rewriters.add" , rewriter : r } ) }
94- onRewriterRemove = { ( i ) => dispatch ( { type : "rewriters.remove" , index : i } ) }
95- onRewriterReorder = { ( f , t ) =>
96- dispatch ( { type : "rewriters.reorder" , from : f , to : t } ) }
97- onShardingChange = { ( s ) =>
98- dispatch ( { type : "sharding.set" , sharding : s } ) }
99- onShardingAccept = { ( _idx ) => { /* F-D wires accept → topology update */ } }
100- />
285+ { activeTab === "canvas" && (
286+ < >
287+ < Palette />
288+ < FlowCanvas
289+ nodes = { nodes } edges = { edges }
290+ onConnect = { handleConnect }
291+ onDropBrick = { handleDropBrick }
292+ />
293+ < Sidebar
294+ loss = { spec . loss }
295+ optim = { spec . optim }
296+ rewriters = { spec . rewriters }
297+ sharding = { spec . sharding }
298+ gotchas = { spec . gotchas }
299+ proposals = { proposals }
300+ onLossApply = { ( l ) => dispatch ( { type : "loss.set" , loss : l } ) }
301+ onOptimApply = { ( o ) => dispatch ( { type : "optim.set" , optim : o } ) }
302+ onRewriterAdd = { ( r ) =>
303+ dispatch ( { type : "rewriters.add" , rewriter : r } ) }
304+ onRewriterRemove = { ( i ) =>
305+ dispatch ( { type : "rewriters.remove" , index : i } ) }
306+ onRewriterReorder = { ( f , t ) =>
307+ dispatch ( { type : "rewriters.reorder" , from : f , to : t } ) }
308+ onShardingChange = { ( s ) =>
309+ dispatch ( { type : "sharding.set" , sharding : s } ) }
310+ onShardingAccept = { handleShardingAccept }
311+ />
312+ </ >
313+ ) }
314+ { activeTab === "tokenizer" && (
315+ < TokenizerPlayground rpc = { rpc } />
316+ ) }
317+ { activeTab === "data" && (
318+ < DataInspector rpc = { rpc } />
319+ ) }
101320 </ div >
102321 < BottomStrip state = { spec } fusedRegionCount = { 0 } />
322+ < RunResultModal report = { runReport } error = { runError }
323+ onClose = { ( ) => { setRunReport ( null ) ;
324+ setRunError ( null ) ; } } />
103325 </ div >
104326 </ ReactFlowProvider >
105327 ) ;
106328}
329+
330+ // ---------------------------------------------------------------------------
331+ // Helpers
332+ // ---------------------------------------------------------------------------
333+
334+ function nodesToGraph ( nodes : Node [ ] , edges : Edge [ ] ) {
335+ return {
336+ nodes : nodes . map ( ( n ) => {
337+ const data = n . data as { kind ?: string ; params ?: Record < string , unknown > } ;
338+ return { id : n . id , kind : data . kind ?? "mlp" , params : data . params ?? { } } ;
339+ } ) ,
340+ edges : edges . map ( ( e ) => ( { src : e . source , dst : e . target } ) ) ,
341+ } ;
342+ }
343+
344+ function buildVerifyParams ( nodes : Node [ ] , edges : Edge [ ] ,
345+ spec : ReturnType < typeof specReducer > ) {
346+ return {
347+ graph : nodesToGraph ( nodes , edges ) ,
348+ dim_env : MINI_DIM_ENV ,
349+ loss : { kind : spec . loss . kind , head_outputs : spec . loss . head_outputs ,
350+ params : spec . loss . params } ,
351+ optim : { kind : spec . optim . kind ,
352+ groups : spec . optim . groups . map ( ( g ) => ( {
353+ matcher : g . matcher , lr : g . lr ,
354+ weight_decay : g . weight_decay , betas : g . betas ,
355+ } ) ) } ,
356+ rewriters : spec . rewriters . map ( ( r ) => ( { name : r . name , params : r . params } ) ) ,
357+ sharding : {
358+ topology : { factory : spec . sharding . topology , kwargs : { } } ,
359+ axis_assignments : spec . sharding . axis_assignments ,
360+ compile_mode : spec . sharding . compile_mode ,
361+ fp8_enabled : spec . sharding . fp8_enabled ,
362+ } ,
363+ training : true ,
364+ available_side_channels : [ "doc_ids" , "token_ids" ] ,
365+ } ;
366+ }
367+
368+ function sumPerBrick ( per ?: Record < string , { params_bytes : number ;
369+ activations_bytes : number ;
370+ kv_cache_bytes : number } > ) : number {
371+ if ( ! per ) return 0 ;
372+ return Object . values ( per ) . reduce (
373+ ( acc , r ) => acc + r . params_bytes + r . activations_bytes + r . kv_cache_bytes ,
374+ 0 ,
375+ ) ;
376+ }
377+
378+ function recolorEdges ( prev : Edge [ ] ,
379+ resolved : { src : string ; dst : string ;
380+ severity : "info" | "warning" | "error" ;
381+ matched : boolean } [ ] ) : Edge [ ] {
382+ const map = new Map < string , { severity : "info" | "warning" | "error" } > ( ) ;
383+ for ( const e of resolved ) map . set ( `${ e . src } ->${ e . dst } ` , { severity : e . severity } ) ;
384+ return prev . map ( ( e ) => {
385+ const m = map . get ( `${ e . source } ->${ e . target } ` ) ;
386+ if ( ! m ) return e ;
387+ return { ...e , data : { ...( e . data ?? { } ) , severity : m . severity } } ;
388+ } ) ;
389+ }
0 commit comments