In questa sezione

Come i valori di avanzamento vengono suddivisi in modo deterministico e mappati attraverso livelli di esecuzione annidati.

map_progress

La funzione di mappatura centrale traduce l’intervallo di avanzamento [0.0, 1.0] di un figlio in un sotto-intervallo del genitore:

pub fn map_progress(child_progress: f32, base: f32, weight: f32) -> f32 {
    base + child_progress.clamp(0.0, 1.0) * weight
}

Un figlio che riporta 0.5 all’interno di un intervallo [base=0.25, weight=0.5] viene mappato su 0.25 + 0.5 * 0.5 = 0.50.

Questa singola funzione viene usata ovunque l’avanzamento venga suddiviso: le unità di un’esecuzione, le esecuzioni di una mappa, la delega delle chiamate peer e la rendicontazione dell’avanzamento nella UI. Non esistono formule alternative, e la coerenza tra tutti i livelli dipende da questo unico calcolo.

Sorgente: capdag-rs/src/orchestrator/types.rs (map_progress).

Suddivisione a livello di esecuzione

Un’esecuzione è fatta di unità: catene di passi e mappe (Esecuzione). Ogni unità di un ambito ha una quota proporzionale ai passi che contiene, e l’avanzamento dell’ambito è una somma, un termine per unità:

progress = somma sulle unità di  map_progress(unit_progress, 0, share)

Per un’esecuzione di quattro unità di un passo ciascuna, di cui la prima è conclusa, la seconda e la terza sono in esecuzione affiancate a 0.5 e 0.2, e la quarta non è iniziata:

Unità Stato Avanzamento dell’unità Contribuisce
0 conclusa 1.0 0.250
1 in esecuzione 0.5 0.125
2 in esecuzione 0.2 0.050
3 in attesa 0.0 0.000
    esecuzione 0.425

Un’unità che termina senza essere stata eseguita, perché saltata, e una che è fallita ed è stata tollerata, contribuiscono con l’intera quota: l’esecuzione non ha più nulla da fare per loro. Le unità vengono eseguite in parallelo (Esecuzione), quindi un’unità non possiede un intervallo contiguo dell’esecuzione: si muove solo il suo termine. Una mappa è un’unità; il suo avanzamento è la stessa somma sulle sue esecuzioni, e viene riportato con il token di passo della mappa. L’avanzamento riportato da ciascun passo è il suo [0, 1], indicizzato con il suo token di passo, ed è ciò che un client disegna su quel passo.

Il totale è limitato a 1.0 e non viene mai riportato più basso di quanto fosse, così né gli arrotondamenti né l’ordine in cui le unità riportano possono far tornare indietro un’esecuzione.

Sorgente: capdag-rs/src/orchestrator/execute_plan.rs.

ProgressMapper

ProgressMapper avvolge una callback CapProgressFn con una suddivisione dell’intervallo:

pub struct ProgressMapper {
    base: f32,
    weight: f32,
    parent: CapProgressFn,
}

report(child_progress, cap_urn, msg): mappa il valore del figlio nell’intervallo del genitore e chiama la callback del genitore:

pub fn report(&self, child_progress: f32, cap_urn: &str, msg: &str) {
    let overall = map_progress(child_progress, self.base, self.weight);
    (self.parent)(overall, cap_urn, msg);
}

sub_mapper(sub_base, sub_weight): crea un ProgressMapper annidato per un’ulteriore suddivisione. L’intervallo effettivo del sub-mapper è l’intersezione tra l’intervallo del genitore e il sotto-intervallo. È così che funziona l’annidamento su più livelli: ogni livello restringe ulteriormente l’intervallo.

as_cap_progress_fn(): converte il mapper in un Arc<dyn Fn(f32, &str, &str) + Send + Sync> da passare alle API che si aspettano una CapProgressFn.

Sorgente: types.rs (ProgressMapper).

Esempio di mappatura annidata

graph TD
    subgraph "Esecuzione (2 unità di un passo)"
        G0["Unità 0<br/>quota 0.5"]
        G1["Unità 1<br/>quota 0.5"]
    end

    subgraph "Handler dell'unità 0"
        DL["Download<br/>[0.0, 0.25] dell'handler"]
        INF["Inferenza<br/>[0.25, 1.0] dell'handler"]
    end

    subgraph "Intervalli effettivi"
        DLE["Download effettivo<br/>[0.0, 0.125] del task"]
        INFE["Inferenza effettiva<br/>[0.125, 0.5] del task"]
    end

    G0 --> DL
    G0 --> INF
    DL --> DLE
    INF --> INFE

Un esempio concreto che mostra come un valore di avanzamento fluisce attraverso tre livelli:

Configurazione: un’esecuzione di 2 unità di un passo. Una cartuccia ML nell’unità 0 scarica un modello tramite una chiamata peer, poi esegue l’inferenza.

Livello 1: l’esecuzione: l’unità 0 ha una quota di 0.5: il suo avanzamento muove l’esecuzione tra 0.0 e 0.5.

Livello 2: chiamata peer dell’handler: l’handler riserva [0.0, 0.25] del proprio intervallo alla chiamata peer di download e [0.25, 1.0] all’inferenza.

Livello 3: download di modelcartridge: riporta l’avanzamento man mano che i file vengono completati.

Il valore fluisce così:

modelcartridge riporta: progress(0.6, "Downloading file 3 of 5")
    │
    │  Mappatura della chiamata peer dell'handler: map_progress(0.6, 0.0, 0.25) = 0.15
    │
    ▼
L'handler inoltra a CapProgressFn: progress(0.15, "cap:...", "Downloading file 3 of 5")
    │
    │  Mappatura del gruppo 0 del DAG: map_progress(0.15, 0.0, 0.5) = 0.075
    │
    ▼
cap_interpreter mappa sull'intervallo del passo del task: progress(0.075, ...)
    │
    ▼
SQLite: UPDATE task SET progress = 0.075

La stessa catena quando l’inferenza è al 50%:

L'handler riporta: progress(0.5, "Generating token 50 of 100")
    │
    │  Intervallo proprio dell'handler: map_progress(0.5, 0.25, 0.75) = 0.625
    │
    ▼
CapProgressFn: progress(0.625, "cap:...", "Generating token 50 of 100")
    │
    │  Gruppo 0 del DAG: map_progress(0.625, 0.0, 0.5) = 0.3125
    │
    ▼
SQLite: progress = 0.3125

Cricchetto dell’avanzamento

Nel database non c’è alcun cricchetto dell’avanzamento. update_progress_impl esegue un semplice UPDATE SQL, quindi i valori di avanzamento possono tornare indietro. Un handler può emettere 0.8 seguito da 0.3 ed entrambi i valori vengono scritti così come sono.

Un cricchetto (in cui l’avanzamento può solo aumentare) non viene usato, perché interferirebbe con le operazioni che tornano effettivamente su stati precedenti, come la codifica multi-passata o i cicli di ritentativo.

Sorgente: l’engine di MachineFabric (non fa parte di CapDAG).

Proprietà

Lo schema di mappatura garantisce:

  • Monotonicità all’interno di un livello: se i valori del figlio crescono in modo monotono, i valori mappati crescono in modo monotono all’interno della quota di quell’unità. Tra le unità l’avanzamento dell’esecuzione è una somma di tali termini, quindi è monotono comunque le unità si alternino.
  • Limitatezza: i valori mappati restano entro [base, base + weight] perché clamp(0.0, 1.0) limita il contributo del figlio.
  • Determinismo: gli stessi input producono sempre lo stesso output, senza componenti casuali o dipendenti dal tempo.
  • Non negatività: tutti i valori mappati sono ≥ 0 (assumendo base e peso non negativi).
  • Componibilità: le istanze annidate di ProgressMapper si compongono correttamente: l’intervallo effettivo si restringe in modo moltiplicativo a ogni livello.