TL;DR
- Ho affrontato il processo di selezione di NVIDIA per un ruolo software vicino ai sistemi senza aver mai scritto un kernel CUDA, e ho ricevuto l'offerta dopo circa sette settimane.
- Quello che distingue questo processo da un loop FAANG generico è il co-design hardware-software: una soluzione O(n) corretta è il punto di partenza, non l'arrivo, perché la domanda successiva riguarda sempre come si comporta il tuo codice su memoria reale e hardware parallelo reale.
- Stavo per fallire il round GPU perché descrivevo un kernel invece di descrivere gli accessi in memoria, e mi sono salvato sapendo spiegare a voce coalescenza, occupancy e intensità aritmetica.
- Se vieni dal backend o dall'applicativo, dedica la maggior parte della preparazione alla semantica della memoria in C/C++ e ai fondamenti di architettura GPU, non ad altro LeetCode.
Introduzione
Non avevo nessuna intenzione di candidarmi in NVIDIA. Da sei anni scrivevo servizi backend a Milano: soprattutto Go, un po' di Java, tanto Kafka e qualche file C++ che toccavo con cautela e abbandonavo in fretta. Le GPU erano quella cosa di cui si lamentava il team di inference su Slack. Poi a marzo mi è passata davanti un'offerta per un ruolo software vicino ai sistemi, di quelle che dicono «C++ performance-critical» e «familiarità con il calcolo parallelo costituisce titolo preferenziale», e l'ho mandata un martedì sera con un curriculum in cui la parola CUDA non compariva da nessuna parte.
Sette settimane dopo avevo un'offerta. Questa è la versione onesta di quello che è successo nel mezzo, incluso il round in cui sono rimasto in silenzio per undici secondi perché sinceramente non sapevo rispondere a una domanda che avrei dovuto vedere arrivare.
Perché ci ho provato
Due motivi, uno rispettabile e uno meno.
Quello rispettabile: da due anni guardavo i miei stessi servizi impantanarsi per ragioni che non sapevo spiegare. Un model server che girava al 30 % di utilizzo e nessuno sapeva dire perché. Sapevo profilare un servizio Go e leggere un flame graph, ma appena il problema scendeva sotto il runtime tiravo a indovinare. Volevo smettere di tirare a indovinare.
Quello meno rispettabile: NVIDIA ha ormai qualcosa come 36.000 persone in ingegneria e chiunque conosca voleva entrarci. Non è una buona ragione, però mi ha retto per sei settimane di studio su argomenti che all'inizio non mi divertivano, quindi non farò finta che non abbia contato.
Una premessa sul mercato italiano
Prima di andare avanti, una cosa che avrei voluto sapere. In Italia le posizioni con questo profilo sono pochissime rispetto al backend o al data engineering, e buona parte di quello che vedi sui portali per i grandi produttori di hardware sono ruoli commerciali o di solution architecture, non ingegneria di prodotto. Leggi per intero la job description prima di investire un mese di preparazione.
Nel mio caso il team non era in Italia, e l'intero processo si è svolto da remoto e in inglese, round tecnici compresi. Se lavori da anni in italiano non è un dettaglio: spiegare la coalescenza degli accessi in memoria nella tua lingua è una cosa, farlo in inglese sotto pressione e con l'editor condiviso è un'altra.
Come ho ottenuto il colloquio
Nessuna referenza interna. Mi sono candidato a freddo dal portale carriere e per undici giorni non ho saputo nulla. Poi ho fatto l'unica cosa che probabilmente ha contato: ho riscritto il primo terzo del curriculum in modo che il lavoro sulle prestazioni fosse il titolo, e non un punto elenco sepolto sotto «microservizi».
Prima: «Sviluppo e manutenzione di pipeline di eventi ad alto throughput.»
Dopo: «Riduzione della latenza p99 di una pipeline di ingestion da 40.000 eventi/s da 180 ms a 34 ms ristrutturando i buffer di batch ed eliminando l'allocazione per messaggio.»
Stesso lavoro. Stessa esperienza. Ma la seconda versione racconta una storia consapevole dell'hardware. Una settimana dopo l'aggiornamento è arrivata la call con recruiting.
Round 1 — la call con recruiting (30 minuti)
Niente di esotico. La recruiter ha ripercorso il mio profilo, mi ha chiesto su cosa volessi lavorare, dove vivessi e con quali tempi, e — questa non me l'aspettavo — mi ha chiesto di dare un voto da uno a dieci alla mia padronanza del C++.
Ho detto sei. Era onesto e credo abbia aiutato. Mi ha risposto che il team cercava persone in grado di arrivare a otto, non persone già a dieci, e che il processo avrebbe indagato il ragionamento sulla memoria a basso livello. Quella frase è stata l'informazione più utile di tutto il percorso, perché mi ha detto esattamente cosa studiare nel mese successivo.
Mi ha anche descritto la forma del loop: un colloquio tecnico telefonico e poi un onsite virtuale. Non si è sbilanciata sul numero di round finché il team non avesse confermato. Il mio è finito con cinque round in circa cinque ore, e da allora ho sentito di loop da quattro e da sei.
Round 2 — il tecnico telefonico e la domanda dietro la domanda
Sessanta minuti, editor condiviso, un intervistatore che per i primi quattro minuti stava chiaramente scrivendo codice in un'altra finestra.
Il problema era su array e stream; l'archetipo è «dato un flusso ampio di valori, mantieni un certo aggregato mobile in modo efficiente». Ho fatto quello che sei anni di condizionamento da colloquio mi avevano insegnato: chiarire i vincoli, illustrare la versione ingenua O(n·k), trovare la versione a passata singola in O(n) con una struttura ausiliaria, scriverla pulita, ripassare i casi limite. Ho finito con quattordici minuti di anticipo e mi sentivo bene.
Poi ha detto: «Bene. Ora immagina che l'array abbia cento milioni di elementi e che tu abbia mille core. Come lo parallelizzi?»
Mi sono bloccato un attimo, perché mentalmente avevo archiviato il problema come risolto. Mi sono ripreso con qualcosa di sensato — spezzare in blocchi, calcolare i parziali per blocco, combinare — e lui ha spinto oltre. Quanto costa il passo di combinazione? L'operazione è associativa? Cosa succede ai confini tra blocchi con una finestra mobile? Quale frazione del tempo totale è davvero la combinazione?
Lì ho capito il loop. Nella maggior parte delle aziende «rendilo più veloce» significa trovare un algoritmo migliore. Qui la risposta O(n) era il biglietto d'ingresso: il colloquio cominciava dopo, e tutto quello che è venuto riguardava come il lavoro si mappa su hardware che fa molte cose contemporaneamente.
Sono passato, ma di misura secondo la mia stessa stima. Ho chiesto un feedback e mi è tornata una sola frase tramite la recruiter: fondamenta algoritmiche solide, vogliono vedere più dimestichezza con la decomposizione parallela. Un modo educato per dire: ti manca una dimensione.
Le quattro settimane di mezzo
Avevo ventisei giorni prima dell'onsite. Ecco cosa ho fatto davvero, in ordine di quanto ha reso.
Prima la gerarchia di memoria, la GPU dopo. Ho passato la prima settimana interamente sul comportamento della cache di CPU: linee di cache, località spaziale e temporale, perché scorrere una matrice 2D per righe è drasticamente più veloce che per colonne, false sharing tra thread. È stata la settimana più redditizia dell'intera preparazione, perché ogni concetto GPU imparato dopo si è rivelato una variazione di «dove vive il dato e quanta strada deve fare».
Semantica del C++, deliberatamente. Puntatori nudi contro riferimenti. Cos'è davvero un puntatore pendente a livello di memoria. Lifetime degli oggetti e RAII. Move semantics e quando avviene una copia silenziosa. std::unique_ptr contro shared_ptr e il costo del contatore atomico del secondo. L'ho fatto scrivendo programmi piccoli e rompendoli di proposito, cosa che richiede più tempo della lettura e si fissa circa quattro volte meglio.
Fondamenti di architettura GPU. Warp e perché un branch che diverge dentro un warp si paga. Occupancy e perché più thread non è automaticamente meglio. Memoria globale contro condivisa. Coalescenza degli accessi, il concetto più importante di tutto il blocco. Il trasferimento host-device come costo di prima classe. E l'abitudine di chiedersi se un kernel è compute-bound o bandwidth-bound prima di ottimizzare qualunque cosa.
Le simulazioni, e la cosa precisa che hanno sistemato. Ho fatto quattro colloqui simulati su PhantomCodeAI nell'arco di due settimane, e il pattern che è tornato ogni singola volta era lo stesso: risolvevo bene la metà algoritmica e poi ammutolivo quando la domanda virava sull'hardware. Non sbagliato: muto. Ragionavo in silenzio e parlavo solo una volta arrivato a una conclusione, cosa che in un colloquio viene letta come «non lo sa». Vederlo in tre trascrizioni consecutive è stato più convincente di qualsiasi consiglio di un amico.
Quello che non ho fatto: altro LeetCode. Ho risolto una decina di problemi in quattro settimane, giusto per non arrugginirmi. Se arrivi da un normale ciclo di preparazione per le grandi aziende tech, la tua parte algoritmica è probabilmente già sufficiente e il collo di bottiglia è altrove.
L'onsite — cinque round, circa cinque ore
Il mio è stato cinque round virtuali consecutivi con due pause brevi. Ordine e composizione variano da team a team, quindi leggilo come il mio loop e non come il loop.
Round 1 — algoritmi, tendenzialmente difficile. Un problema su grafi e visite con un vincolo scomodo di memoria. Nella forma, standard. La svolta era di nuovo nella seconda metà: dato che il grafo non entra in cache, come lo disporresti in memoria per ridurre i miss? Quindici minuti a confrontare liste di adiacenza e layout piatti in stile CSR. Un mese prima non avrei avuto niente da dire.
Round 2 — C++ e memoria. Il round più diretto del loop e, stranamente, quello che ho gradito di più. Archetipi: ecco una classe piccola, dimmi tutti i punti in cui può perdere memoria o fare doppia free. Cosa valuta questa aritmetica dei puntatori e perché. Che differenza c'è tra passaggio per valore, per riferimento e per puntatore. Dove vive questo oggetto e quando muore. Non c'era nessuna furbizia in tutto il round: era una verifica di competenza secca, ed è completamente studiabile.
Round 3 — quello che stava per farmi fuori. GPU e parallelismo. La traccia era un carico di trasformazione dati e la domanda era come l'avrei portato su GPU e come l'avrei reso veloce.
Ho iniziato descrivendo un kernel. Indicizzazione dei thread, dimensioni dei blocchi, la meccanica. L'intervistatore mi ha lasciato andare avanti forse novanta secondi, poi ha detto, con gentilezza: «Mi stai spiegando come si scrive. Io voglio sapere come si comporta la memoria.»
È lì che sono rimasto in silenzio per quelli che la mia registrazione ha poi confermato essere undici secondi.
Quello che mi ha salvato è stato uno dei concetti che avevo macinato: la coalescenza. Sono ripartito dal dato. Ho detto, a voce alta e lentamente, che la domanda importante era se thread adiacenti leggono indirizzi adiacenti, perché se lo fanno l'hardware serve le letture di un warp in poche transazioni, e se non lo fanno lo stesso lavoro logico diventa molte volte il traffico di memoria. Poi ho detto che il pattern di accesso della versione ingenua è con stride, quindi avrei ristrutturato il layout dei dati — da array of structs a struct of arrays — prima ancora di toccare il kernel. E ho chiesto se il carico fosse bandwidth-bound, perché in quel caso nessun tuning serve finché non si sistemano i trasferimenti.
Si è visibilmente rilassato. Il resto del round l'abbiamo passato sul tiling in memoria condivisa e su se la copia host-device si sarebbe mangiata tutto il guadagno per una singola passata sui dati. La risposta era sì, ed è esattamente il motivo per cui o tieni i dati residenti o non vale la pena.
Continuo a pensare di aver perso punti lì. Penso anche che ripartire da un altro livello di astrazione, a voce alta, l'abbia salvato. Gli intervistatori guardano come ti riprendi, e «fammi affrontare la cosa dal lato della memoria» è molto meglio che continuare a parlare di indici di thread.
Round 4 — design. Non un classico round di system design web-scale. Somigliava di più all'architettura di una pipeline: come si muovono i dati in un sistema eterogeneo, dove metti buffer, dove fai batch, come tieni alimentato un acceleratore invece di lasciarlo fermo, e quale componente fissa il tetto di throughput. Se la tua preparazione sul design è tutta load balancer e sharding, allargala; i fondamenti della raccolta di domande di system design restano validi, ma la pressione sarà sul movimento dei dati e non sulla topologia dei servizi.
Round 5 — hiring manager. Comportamentale, più morbido di quanto mi aspettassi, ma con una domanda affilata: raccontami un problema di prestazioni che non hai risolto. Ho detto la verità sul model server al 30 %, compresa la parte in cui avevo dato per scontato che fosse la rete e mi sbagliavo. È sembrato più interessato a quella risposta che a qualsiasi mia storia di successo.
Il debrief e l'offerta
Nove giorni di silenzio, passati convinto che il round 3 mi avesse affondato. Poi una call: il loop era complessivamente positivo, con un round segnalato come misto. La recruiter non ha detto quale. Io so quale.
La conversazione sull'offerta è stata lineare e poco memorabile, nel senso migliore. Ho negoziato: ho chiesto con garbo se ci fosse margine, e ho chiesto più cose sul livello e sul perimetro del team che sulla cifra di copertina, perché non avevo offerte concorrenti né modo onesto di far finta di averne. Qualcosa si è mosso, altro no. Nessuno si è irrigidito perché avevo chiesto.
Non pubblico numeri, ma due cose sì, perché in Italia confondono quasi tutti. Una multinazionale di questo tipo ragiona di solito su una RAL distribuita su dodici mensilità, quindi se arrivi da un contratto con tredicesima e quattordicesima devi normalizzare prima di confrontare qualsiasi cosa. E se compare una componente azionaria, dedica tempo vero a capire vesting e politica di refresh: fuori dalle grandi aziende tech in Italia è una voce rara ed è facile sovrastimarla o sottostimarla.
Cosa studierei di nuovo, in ordine
Se dovessi rifare quelle quattro settimane le spenderei quasi allo stesso modo, con un cambiamento: comincerei le simulazioni nella prima settimana e non nella terza, perché è da lì che ho capito cosa non funzionava nel mio modo di rispondere, quando avevo ancora tempo per correggerlo.
- Gerarchia di memoria e comportamento della cache. Linee di cache, località, false sharing, perché l'ordine dei cicli cambia i tempi di un ordine di grandezza. Tutto il resto si appoggia qui.
- Coalescenza degli accessi in memoria. Se impari un solo concetto specifico delle GPU, impara questo. «Thread contigui toccano indirizzi contigui?» risponde a una quota sorprendente delle domande di follow-up di questo processo.
- Puntatori e lifetime in C/C++. Puntatori pendenti, ownership, RAII, move contro copia, il costo reale di uno
shared_ptr. Aspettati domande dirette, non incidentali. - Modello di esecuzione della GPU. Warp, divergenza, occupancy, memoria condivisa, costo del trasferimento host-device. Quanto basta per ragionare, non per andare in produzione.
- Compute-bound contro bandwidth-bound. Allenati a dire quale delle due è un dato carico, e perché, prima di proporre qualsiasi ottimizzazione.
- Rispondere bene a «rendilo più veloce». Quasi ovunque significa una struttura dati migliore; qui di solito significa la macchina. Se non distingui quale ti stanno chiedendo, chiedi: «vuoi che attacchi l'algoritmo o il pattern di accesso?» è una domanda di chiarimento legittima e viene accolta bene.
Ci aggiungerei un settimo punto non tecnico: se il processo sarà in inglese, esercitati in inglese dal primo giorno. Le ultime tre sessioni di pratica le ho fatte su PhantomCodeAI direttamente in inglese e registrate, e ho scoperto che il mio lessico tecnico inglese era corretto ma la narrazione diventava telegrafica sotto pressione, che è esattamente ciò che un intervistatore interpreta come insicurezza.
La parte a cui continuo a pensare
Non ero il candidato più forte sul fronte algoritmico di quel loop. Quello che avevo erano quattro settimane passate a imparare deliberatamente un solo asse — come si comporta il codice su hardware reale — e la disponibilità a dire «non lo so, fammi ragionare dal lato della memoria» invece di bluffare.
Se sei un ingegnere competente di backend che guarda un annuncio del genere e si dice che non può candidarsi perché non ha mai scritto un kernel: la distanza è più corta di quanto sembri, ed è di lettura, non di esperienza. Quattro settimane di gerarchia di memoria, semantica del C++ e un concetto capito molto bene chiamato coalescenza sono stati, nel mio caso, sufficienti.
Domande frequenti
Serve esperienza con CUDA per superare il colloquio software di NVIDIA?Per il ruolo che ho fatto io, vicino ai sistemi, non è servita, ma bisogna saper ragionare a voce alta su hardware parallelo. Nessuno mi ha chiesto di scrivere a memoria un kernel di produzione. Mi hanno chiesto cosa succede al traffico di memoria quando thread contigui leggono indirizzi non contigui, e se il mio problema fosse limitato dal calcolo o dalla banda. Queste cose si imparano in poche settimane; dieci anni di codice GPU in produzione no.
In cosa è diverso il loop NVIDIA da un classico loop FAANG?I round di algoritmi e strutture dati assomigliano molto a quelli di qualsiasi grande azienda tech. La differenza sta nella seconda metà di ogni domanda. Nella maggior parte delle aziende rendilo più veloce significa trovare una struttura dati migliore. Qui di solito significa la macchina: linee di cache, gerarchia di memoria, false sharing, divergenza dei thread, throughput contro latenza. Stessa domanda, asse di ottimizzazione diverso.
Si può affrontare questo processo dall'Italia?Io l'ho fatto interamente da remoto dall'Italia e tutti i round erano in inglese. La cosa da verificare subito è dove risiede davvero il team, perché nel mio caso non era in Italia e questo cambia fuso orario, trasferte e aspettative di presenza. Chiedilo alla prima call con recruiting, non all'ultima.
Cosa studio se vengo dal backend o dall'applicativo?Tre cose, in quest'ordine. Puntatori, lifetime e ownership in C/C++, perché te lo chiedono in modo diretto. Comportamento della gerarchia di memoria: linee di cache, località, coalescenza e perché lo stesso ciclo diventa dieci volte più lento se scambi gli indici. E il modello di esecuzione di una GPU: warp, occupancy, memoria condivisa e costo del trasferimento host-device. Sono settimane di lettura mirata, non un cambio di carriera.
Come funziona la parte economica per un'offerta del genere in Italia?Non pubblico cifre, perché il pacchetto di una singola persona è un pessimo riferimento per chi ha un altro livello, un'altra sede e un'altra vacancy. Dico però le due cose che in Italia confondono tutti. La prima è che una multinazionale di questo tipo di solito ragiona su una RAL distribuita su dodici mensilità, quindi confrontarla con un'offerta locale che include tredicesima e quattordicesima richiede di normalizzare prima. La seconda è che se compare una componente azionaria conviene capire bene vesting e politica di refresh, perché fuori dalle grandi aziende tech è rara sul mercato italiano ed è facile valutarla male.