top of page

Il modello di disegno RP2040 esegue programmi esatti, ma l'AI resta sull'host

1 ora fa
Tempo di lettura: 14 min

Il modello di disegno RP2040 ha generato programmi compatti per 12.670 test hardware, ma il suo transformer da 825.344 parametri non è mai stato eseguito sul microcontrollore. Un computer host ha invece prodotto bytecode di disegno e lo ha inviato a un Raspberry Pi Pico per un'esecuzione deterministica.

Questa distinzione definisce sia il valore sia i limiti del progetto. Non è l'ennesima affermazione secondo cui un piccolo dispositivo possa eseguire localmente un modello generativo utile. È un esperimento che separa la generazione neurale incerta dall'esecuzione esatta e vincolata.

Il sistema mette in discussione il consueto approccio di generazione dei pixel. Si chiede se un piccolo transformer possa scrivere una descrizione eseguibile e poi affidarla a una macchina minimale dal comportamento prevedibile. I risultati hardware appaiono insolitamente puliti, mentre la capacità del modello di comporre strutture non familiari resta molto meno certa.

Il modello di disegno RP2040 separa generazione ed esecuzione

Il risultato centrale è una divisione del lavoro, non l'inferenza neurale sul dispositivo.

Secondo il repository pubblico del progetto, un transformer autoregressivo con 825.344 parametri viene eseguito su un computer host. Genera circa 100 byte di bytecode di disegno per ciascun esempio.

Il bytecode è un formato di istruzioni compatto interpretato da un altro programma. In questo caso, descrive operazioni quali lo spostamento di una penna virtuale, il disegno di linee, la valutazione di curve, l'applicazione di trasformazioni intere e la ripetizione di sequenze limitate.

L'host trasferisce quel programma a un Raspberry Pi Pico. Una piccola macchina virtuale, o VM, esegue le istruzioni sul microcontrollore RP2040 del Pico. Quindi trasmette le coordinate geometriche tramite UART, un'interfaccia standard di comunicazione seriale.

L'immagine finale deriva da quelle coordinate restituite. Il Pico non memorizza i pesi del transformer, non esegue inferenza neurale intensiva in termini di matrici e non richiede un runtime per tensor.

Questo confine è importante, perché l'espressione "modello su un RP2040" suggerirebbe un risultato tecnico diverso. Un modello con 825.344 parametri potrebbe richiedere diversi megabyte nei formati numerici convenzionali, prima ancora di considerare la memoria di runtime.

L'autore del progetto evita esplicitamente questa affermazione. La discussione originale afferma che il transformer resta sull'host, mentre il Pico memorizza ed esegue il suo output.

Il modello dimostrativo rilasciato copre cinque categorie: gatti, autobus, fiori, barche a vela e biciclette. Non viene presentato come un sistema text-to-image aperto.

Questo ambito ristretto rende i risultati più semplici da interpretare. L'esperimento studia la rappresentazione, la generazione di programmi e l'esecuzione vincolata senza rivendicare una conoscenza visiva ampia.

Il modello produce istruzioni anziché una griglia di pixel colorati. Questo crea un'interfaccia utile tra l'AI probabilistica e il software embedded deterministico.

Un generatore di pixel si impegna direttamente sul risultato visibile. Un generatore di programmi, invece, propone una sequenza che un altro sistema può convalidare, limitare, eseguire o rifiutare.

Questa differenza crea la tensione principale dell'articolo. L'esecuzione hardware appare esatta ed economica, ma un'esecuzione esatta non garantisce che il programma generato rappresenti il disegno desiderato.

Il Pico può eseguire perfettamente un cattivo programma per una bicicletta. Può anche rifiutare in modo affidabile una sequenza malformata o troppo lunga se la VM applica limiti adeguati.

In altre parole, la correttezza dell'esecuzione e la correttezza generativa sono proprietà separate. Il progetto misura entrambe, e i risultati indicano direzioni diverse.

L'esecuzione esatta è il risultato più solido

Il Pico ha corrisposto con coerenza all'interprete di riferimento, sebbene le misurazioni provengano dagli stessi artefatti di test del progetto.

L'autore riferisce che durante una serie di test hardware sono stati eseguiti 12.670 programmi generati. Ogni traccia restituita corrispondeva esattamente alla VM di riferimento in Python.

Una traccia è la geometria ordinata prodotta da un programma. Una corrispondenza esatta significa che il dispositivo e il riferimento hanno restituito coordinate identiche, anziché limitarsi a produrre immagini dall'aspetto simile.

Il registro degli esperimenti del progetto riporta un'uguaglianza a tolleranza zero per tutte le 12.670 tracce. Elenca inoltre il superamento di 120 programmi di conformità su 120 rispetto a una baseline QEMU.

Si tratta di risultati di prima parte, non di una replica indipendente. Tuttavia, il repository include l'interprete, la struttura dei test, le tracce acquisite e la documentazione necessari per un'ispezione tecnica.

Secondo quanto riferito, l'interprete C occupa 1.862 byte di flash. Questa cifra copre la sola VM, escludendo l'archiviazione del bytecode e l'infrastruttura di trasporto circostante.

L'implementazione non utilizza RAM allocata staticamente per lo stato della VM. L'uso massimo dello stack ha raggiunto 492 byte nella configurazione misurata.

Con un clock RP2040 deliberatamente ridotto a 12 MHz, la media riportata è stata di 7.334 cicli per disegno. Ciò corrisponde a circa 0,611 millisecondi per i programmi QuickDraw misurati.

L'autore riporta inoltre 1,959 cicli per istruzione eseguita. Queste misurazioni riguardano il lavoro dell'interprete, non il tempo necessario per generare un programma sull'host.

Escludono inoltre il tempo di trasferimento dall'host al dispositivo e il lavoro di visualizzazione. I lettori non dovrebbero considerare 0,611 millisecondi come latenza generativa end-to-end.

L'RP2040 è un microcontrollore Arm Cortex-M0+ dual-core con 264 kB di SRAM integrata. Raspberry Pi indica una frequenza di clock massima di 133 MHz nella sua documentazione RP2040.

Il chip non dispone di un'unità hardware per la virgola mobile. Questa limitazione spesso complica il codice grafico, poiché curve e trasformazioni usano comunemente coordinate frazionarie.

Questa VM evita l'aritmetica in virgola mobile tramite una rappresentazione a virgola fissa. La virgola fissa memorizza i valori frazionari come interi scalati, producendo risultati prevedibili tra implementazioni diverse.

Il valutatore di curve sfrutta conteggi di passi che sono potenze di due. Con questa restrizione, i coefficienti cubici Bézier pertinenti possono essere rappresentati come frazioni binarie con un denominatore noto.

L'implementazione seleziona un numero sufficiente di bit frazionari per preservare tali valori durante il calcolo intero. Questo design elimina dal percorso geometrico misurato le differenze di arrotondamento tra piattaforme.

L'aritmetica deterministica rende inoltre il confronto insolitamente rigoroso. Il test non richiede un punteggio di somiglianza dell'immagine né una tolleranza attorno a ciascun vertice.

Il riferimento e il dispositivo emettono la stessa traccia, oppure non la emettono. Questo risultato binario è più facile da verificare rispetto a una valutazione soggettiva della somiglianza visiva.

I test fisici hanno rivelato almeno una classe di problemi che la simulazione sull'host non aveva individuato. La documentazione del progetto descrive condizioni di gara nell'inizializzazione dei clock periferici durante l'avvio bare-metal.

Questa osservazione sostiene la decisione di testare sul silicio. Un interprete può essere matematicamente corretto mentre la sua sequenza di trasporto o avvio resta inaffidabile su una scheda reale.

Le prove attuali presentano comunque limiti chiari. L'autore non ha misurato l'energia per disegno, perché il banco di prova non disponeva di apparecchiature adeguate per rilevare la corrente.

Il repository non include inoltre il checkpoint addestrato. Gli utenti possono eseguire la VM e riprodurre le acquisizioni registrate, ma la generazione live del modello richiede un checkpoint ottenuto separatamente.

Queste limitazioni non cancellano il risultato dell'esecuzione. Definiscono ciò che i revisori esterni possono riprodurre immediatamente e ciò che dipende ancora dai materiali dell'autore.

I programmi offrono un controllo che i pixel non possono garantire

Un output eseguibile trasforma il comportamento del modello in qualcosa che un runtime vincolato può ispezionare e governare.

Un programma di disegno espone operazioni, flusso di controllo e struttura geometrica. Un'immagine raster espone soltanto la disposizione finale dei pixel.

Questa differenza conta sui dispositivi piccoli. Un runtime può imporre un limite di fuel, ovvero un numero massimo di istruzioni consentite prima della terminazione.

Può inoltre limitare l'annidamento dei cicli, la profondità delle chiamate, la profondità delle trasformazioni, gli intervalli delle coordinate e il volume di output. Questi vincoli rendono finito il comportamento generato anche quando il modello produce una sequenza difettosa.

La VM del progetto trasmette i vertici in streaming anziché memorizzare un disegno completo. Ciò riduce la pressione sulla memoria di lavoro e si adatta al comportamento di un dispositivo progettato per il controllo in tempo reale.

L'approccio ricorda altri sistemi che separano pianificazione ed esecuzione. Una macchina più grande esegue l'inferenza costosa, mentre un controller più piccolo segue una rappresentazione intermedia compatta.

Questo modello è già presente nella robotica, nel controllo numerico computerizzato, nei plotter e nelle interfacce embedded. L'elemento insolito qui è l'uso di un transformer con meno di un milione di parametri per produrre il programma intermedio.

Il bytecode di disegno è particolarmente adatto all'esperimento. Linee, curve e motivi ripetuti hanno risultati visibili, ma la loro esecuzione resta più semplice di quella di un linguaggio di programmazione generico.

Una previsione d'immagine malformata produce un'immagine poco attraente. Un programma malformato solleva ulteriori questioni su terminazione, validità e sicurezza del runtime.

La VM risponde ad alcune di queste domande con un set di istruzioni deliberatamente ristretto. Non offre accesso arbitrario alla memoria né servizi generali di sistema operativo.

Ciò rende il sistema più vicino a un linguaggio specifico di dominio che al normale codice generato. Un linguaggio specifico di dominio supporta un compito ristretto con meno operazioni pericolose o ambigue.

Il risultato è un contratto vincolato. Il modello propone un disegno, mentre l'interprete decide cosa quei byte significhino secondo regole fisse.

Questo contratto crea opportunità che vanno oltre gli schizzi. Un'architettura simile potrebbe rappresentare percorsi utensile, comandi per plotter a penna, schemi LED, animazioni semplici o layout di interfacce limitati.

Tuttavia, queste applicazioni richiederebbero una propria convalida. Una geometria esatta su un Pico non dimostra un movimento sicuro dei motori né un controllo affidabile di macchinari fisici.

Il dominio di destinazione determinerebbe inoltre quali errori contano. Un fiore leggermente malformato è innocuo, mentre il percorso malformato di un attuatore può danneggiare l'apparecchiatura.

L'idea trasferibile più solida del progetto è quindi architetturale. La generazione probabilistica può restare al di fuori del confine di esecuzione fidato.

Il componente embedded può rimanere piccolo, testabile e deterministico. Non deve ereditare la complessità del modello che ha proposto il programma.

Questa separazione cambia anche il modo in cui gli sviluppatori possono eseguire il debug dei guasti. Possono ispezionare i byte generati, riprodurli in Python, confrontare le tracce e isolare il comportamento specifico del dispositivo.

Una pipeline di pixel spesso nasconde la struttura nelle attivazioni neurali. Una pipeline di programmi lascia un artefatto dal significato operativo esplicito.

Questo artefatto può essere registrato e versionato. Può inoltre essere verificato rispetto a regole note prima che un dispositivo lo riceva.

Per i team di ingegneria, questo assomiglia più a una pipeline di compilazione che a un generatore di immagini. Il modello agisce come un front end incerto, mentre la VM agisce come un back end di esecuzione rigoroso.

L'analogia non va spinta troppo oltre. I compilatori tradizionali traducono testo sorgente ben definito, mentre questo transformer campiona programmi da una distribuzione appresa.

Tuttavia, il confine è prezioso. Conferisce a un componente software convenzionale l'autorità su ciò che l'output generato può fare.

La generazione di programmi con piccoli modelli fallisce ancora nella composizione

L'interprete esegue esattamente, ma il transformer non genera in modo affidabile relazioni sconosciute con precisione.

Gli esperimenti del progetto mostrano che una bassa perdita di previsione non produce automaticamente programmi campionati affidabili. Questo divario è il motivo principale per considerare il lavoro una ricerca anziché un sistema completato.

Il modello autoregressivo piatto di base riporta una test loss convergente di 489,2 bit per disegno. La test loss misura l'incertezza predittiva, non se un disegno campionato soddisfi una relazione geometrica desiderata.

L'autore ha testato diverse rappresentazioni con lo stesso budget generale di parametri. Includevano byte, singoli bit, token tipizzati e delta relativi delle coordinate.

Su un corpus sintetico di programmi, la rappresentazione a bit ha ottenuto risultati pressoché equivalenti ai byte. La differenza riportata era di meno 0,67 bit per disegno, con un'incertezza di più o meno 0,77 bit.

Il risultato è cambiato sui bozzetti umani provenienti dai dati Quick, Draw data di Google. In quel caso, la modellazione a livello di bit ha comportato una penalità riportata di 11,58 bit per disegno, con un'incertezza di più o meno 0,60 bit.

I bit hanno inoltre ampliato le sequenze di valutazione di un fattore otto. L'esperimento ha elaborato 254 milioni di token a bit, rispetto a 32 milioni di token byte.

Il tempo di valutazione riportato è salito da quattro minuti per i byte a 48 minuti per i bit. Si tratta di un rallentamento superiore a otto volte nella configurazione documentata.

Il confronto indebolisce qualsiasi semplice affermazione secondo cui un vocabolario più piccolo aiuti sempre un modello minuscolo. Un alfabeto di due simboli riduce i costi degli embedding, ma costringe la rete a ricostruire i confini dei byte e la struttura dei campi.

I pattern sintetici hanno apparentemente reso questa ricostruzione gestibile. I bozzetti umani più vari non hanno prodotto lo stesso esito.

I token tipizzati hanno introdotto un altro compromesso. Legano opcode e ruoli degli operandi in modo più esplicito, ma il loro vocabolario più ampio consuma una quota sostanziale di un piccolo budget di parametri.

In un modello ampio, la tabella degli embedding occupava il 22 percento di tutti i parametri. Quella configurazione ha ottenuto un risultato peggiore di 4,87 bit per disegno rispetto al ramo di confronto.

Un modello profondo e stretto ha assorbito il costo del vocabolario più efficacemente. Ciò suggerisce che rappresentazione e architettura interagiscano fortemente sotto la scala del milione di parametri.

I fallimenti più rivelatori hanno riguardato la geometria ripetuta. Il modello ha appreso ripetizioni prevedibili entro l'intervallo presente durante l'addestramento.

La sua sorpresa è diminuita del 74 percento quando ha incontrato la seconda copia di un motivo noto. Una metrica di recupero ha raggiunto 0,807 in una configurazione riportata.

Eppure le prestazioni sono crollate alla quinta copia, esattamente una ripetizione oltre il numero massimo nell'addestramento. Il modello sembrava aver appreso una distribuzione dei conteggi anziché una regola di ciclo astratta.

Il progetto ha inoltre testato la compatibilità geometrica con teacher forcing. Il teacher forcing valuta il successivo elemento corretto dopo aver fornito la vera sequenza precedente.

In quella configurazione, le continuazioni compatibili hanno ricevuto un forte vantaggio di 4,28 bit per byte target. Il livello di significatività corretto riportato era 0,001.

Il campionamento libero ha prodotto un esito molto diverso. Il completamento esatto è riuscito solo circa l'uno percento delle volte sulle forme composte testate.

Il successo nei casi più semplici a passaggi piatti variava dal sette al 13 percento. Il modello poteva riconoscere una continuazione compatibile quando gli veniva mostrato il contesto, ma raramente costruiva da solo l'intera continuazione.

Questa disconnessione è centrale nella modellazione generativa contemporanea. La preferenza a livello di token può sembrare convincente, mentre piccoli errori locali si accumulano durante il campionamento autonomo.

Ogni output campionato diventa parte del contesto della previsione successiva. Una coordinata, un opcode o una decisione sulla lunghezza errati possono allontanare la sequenza dalle condizioni incontrate durante l'addestramento.

L'RP2040 non può riparare quel fallimento semantico. Può eseguire esattamente il programma risultante, ma l'esecuzione esatta preserva l'errore.

La pianificazione gerarchica migliora la lunghezza più della likelihood

L'aggiunta di una struttura esplicita ha migliorato la terminazione, ma ha reso i disegni meno probabili secondo la principale metrica di loss del progetto.

L'autore ha confrontato il transformer piatto con progetti gerarchici con lo stesso budget di 825.344 parametri. Questi sistemi prevedevano prima riepiloghi dei tratti, quindi generavano bytecode dettagliato per ciascun tratto.

Un pianificatore usava l'autoregressione. Un altro usava la diffusione, che converte gradualmente il rumore in una previsione strutturata tramite ripetuti passaggi di denoising.

Entrambe le varianti gerarchiche hanno perso di circa 40-55 bit per disegno rispetto al modello piatto. La penalità esatta variava in base al tipo di pianificatore e al budget computazionale.

L'esperimento ha quindi respinto l'ipotesi che la pianificazione gerarchica avrebbe migliorato la likelihood a parità di scala. Una struttura più esplicita comportava un costo di modellazione misurabile.

I pianificatori hanno tuttavia controllato più accuratamente la lunghezza dell'output. Il loro errore di distribuzione delle lunghezze variava da 1,8 a 3,5 byte, a seconda della configurazione.

Il divario corrispondente del modello piatto variava da 7,0 a 13,6 byte. Era più propenso a interrompersi prematuramente o a continuare verso la lunghezza massima consentita.

Non si tratta di un dettaglio implementativo minore. I programmi generati devono terminare a confini sensati prima di poter diventare comandi utili.

Un modello con una likelihood media migliore può comunque produrre campioni scomodi se assegna troppa probabilità alla terminazione anticipata. Può anche generare code lunghe e ripetitive.

La gerarchia ha separato il conteggio dei tratti dalla costruzione locale dei tratti. Questa decisione esplicita ha migliorato la distribuzione delle lunghezze generate, anche se la likelihood complessiva è diminuita.

La diffusione non ha fornito un vantaggio chiaro rispetto a un pianificatore autoregressivo sulla rappresentazione riepilogativa condivisa. Il guadagno nella terminazione derivava dalla gerarchia, non dal denoising.

Esperimenti successivi sul set di istruzioni hanno prodotto un andamento simile. Operazioni esplicite di ripetizione e trasformazione hanno fornito una compressione diretta limitata, poiché il modello già prevedeva la geometria ripetuta con bassa sorpresa.

Tuttavia, sequenze più corte hanno migliorato l'uso del contesto e la terminazione. Gli errori riportati sulla lunghezza generata sono scesi in un intervallo dal nove all'11 percento.

Modelli piatti comparabili mostravano errori dal 41 al 112 percento. Si tratta di misurazioni sperimentali proprietarie, ma illustrano una tensione progettuale significativa.

Una rappresentazione può aiutare la generazione senza vincere sulla test loss convenzionale. Viceversa, una loss più bassa non garantisce programmi ben formati durante il campionamento.

Questa tensione dovrebbe guidare la valutazione futura. I ricercatori necessitano di metriche per validità, completamento relazionale esatto, terminazione, novità e comportamento in esecuzione.

La qualità visiva resta rilevante, ma non può essere considerata da sola. Due disegni potrebbero sembrare simili mentre i loro programmi differiscono sostanzialmente per lunghezza, struttura o riuso.

Anche la memorizzazione è una questione irrisolta. Un modello piccolo può riprodurre motivi familiari senza apprendere le trasformazioni che li generano.

Il repository documenta controlli per posizione, prossimità, frequenza e insiemi di coordinate. Questi controlli rafforzano l'esperimento relazionale, ma non risolvono la questione della novità nell'intero corpus di addestramento.

Una release più solida includerebbe checkpoint, manifesti di addestramento, campioni generati, analisi dei vicini più prossimi e script end-to-end ripetibili.

Molteplici seed di addestramento indipendenti chiarirebbero inoltre quali comportamenti sopravvivono ai cambiamenti di inizializzazione. Alcuni risultati sulla ripetizione fuori distribuzione divergevano già in modo evidente tra i seed.

Il progetto attuale riporta risultati negativi anziché nasconderli. È utile perché i fallimenti identificano il punto in cui i modelli compatti smettono di comportarsi come ragionatori simbolici.

Cosa verificare successivamente

Il prossimo traguardo non è una galleria più ampia; è dimostrare che le relazioni esplicite migliorano la generazione di programmi non visti.

La direzione attuale del progetto aggiunge un'azione copy-or-emit. Il modello può produrre byte ordinari oppure fare riferimento a un intervallo sorgente precedente tramite una trasformazione affine.

Una trasformazione affine può traslare, ruotare, riflettere o ridimensionare la geometria preservando le linee rette. In questo sistema, le operazioni supportate rimarrebbero basate su numeri interi ed eseguibili dallo stile di VM esistente.

Questa proposta mira direttamente al divario del teacher forcing. Il modello sembra già sensibile a un contesto relazionale compatibile, ma il campionamento libero raramente completa quella relazione con esattezza.

Un'azione esplicita potrebbe ridurre il numero di decisioni separate necessarie per riprodurre un motivo trasformato. Una relazione corretta potrebbe sostituire molte fragili previsioni di coordinate.

Il primo segnale da osservare è la prestazione su combinazioni non viste. Il modello dovrebbe generare relazioni esatte escluse dall'addestramento, non limitarsi a comprimere forme ripetute familiari.

La valutazione dovrebbe confrontare l'emissione piatta con il comportamento copy-or-emit a parità di budget di parametri e addestramento. Il successo esatto nella generazione libera conta più della sola preferenza con teacher forcing.

Se il completamento relazionale non visto aumenterà in misura sostanziale oltre il livello riportato dell'uno percento, il meccanismo acquisirà credibilità. Se migliorerà solo la likelihood, il problema centrale della generazione rimarrà.

Il secondo segnale è la riproduzione indipendente della valutazione hardware. Il progetto fornisce codice sorgente e artefatti acquisiti, ma al momento non include il checkpoint del modello.

Uno sviluppatore esterno dovrebbe poter ricostruire l'interprete, eseguire la suite di conformità, inviare programmi generati a un Pico e riprodurre tracce identiche bit per bit.

Questo processo dovrebbe riportare impostazioni del compilatore, configurazione del clock, overhead di trasporto e contabilità completa della memoria. Dovrebbe distinguere la flash dell'interprete dalla dimensione totale del firmware.

Una riproduzione riuscita rafforzerebbe l'affermazione sull'esecuzione. Una discrepanza aiuterebbe a identificare se il risultato dipende dalla toolchain, dalla revisione della scheda o da dettagli di configurazione non documentati.

Il terzo segnale è una misurazione end-to-end delle risorse. L'attuale valore di 0,611 millisecondi copre l'esecuzione della VM a 12 MHz, non l'inferenza host né il trasferimento seriale.

Una dimostrazione pratica dovrebbe separare tempo di generazione, tempo di validazione, tempo di trasferimento, tempo di esecuzione e tempo di rendering. Le misurazioni energetiche chiarirebbero inoltre il costo della fase embedded.

Queste misurazioni non trasformerebbero il progetto in IA on-device. Mostrerebbero se l'architettura divisa offre un utile compromesso di sistema.

La lezione più ampia appare già credibile, anche prima di questi test. Piccoli modelli generativi possono produrre rappresentazioni intermedie eseguibili, mentre runtime deterministici minuscoli impongono regole operative ristrette.

Resta poco chiaro se il modello possa generare la struttura corretta al di fuori di combinazioni familiari. L'esattezza hardware risolve solo l'ultima fase di quel problema.

Gli sviluppatori che valutano il modello di disegno RP2040 dovrebbero quindi porsi due domande distinte. Il Pico esegue esattamente ogni istruzione valida, e il transformer scrive in modo affidabile il programma previsto?

Le evidenze disponibili forniscono una forte risposta proprietaria alla prima domanda. Offrono una risposta molto più prudente alla seconda.

Osservate l'esperimento copy-or-emit, il rilascio di checkpoint riproducibili e un'esecuzione indipendente su Pico. Questi tre test determineranno se il progetto diventerà un modello progettuale riutilizzabile o rimarrà un istruttivo prototipo di ricerca.

 
 

Inizia gratis

Un assistente IA local-first con gestione della conoscenza personale

Per una migliore esperienza con l’IA,

al momento remio supporta solo Windows 10+ (x64) e M-Chip Macs.

Il tuo partner AI al lavoro
Fai di più con remio

Pianifica. Crea. Consegna.
Tutto in un unico posto.

bottom of page