Simulacion educativa de un GPU con su jerarquia de componentes: Threads, Warps, Cores, SMs y memoria. Incluye un ISA (Instruction Set Architecture) propio y un assembler para escribir kernels.
GPU
└── SMs (Streaming Multiprocessors)
├── Warp Scheduler
├── Cores (ejecutan instrucciones)
├── Shared Memory (por bloque)
└── Warps
└── 32 Threads (cada uno con sus propios registros)
Memoria
├── Global Memory (compartida por todo el GPU, lenta)
└── Shared Memory (por bloque, rapida)
SIMT — Single Instruction, Multiple Threads: un warp ejecuta la misma instruccion en sus 32 threads simultaneamente, pero cada thread opera sobre sus propios datos.
modelo-gpu/
├── main.py
├── gpu/
│ ├── thread.py
│ ├── warp.py
│ ├── core.py
│ ├── scheduler.py
│ ├── memory.py
│ ├── sm.py
│ └── gpu.py
├── isa/
│ ├── opcodes.py
│ ├── instruction.py
│ └── assembler.py
├── kernels/
│ └── vector_add.asm
└── sim/
└── runner.py
Tarea 1 — isa/opcodes.py
Definir el conjunto de instrucciones del GPU.
- Crear un enum
Opcodecon las instrucciones soportadas:- Aritmetica:
ADD,SUB,MUL,DIV,ADDI(con inmediato) - Memoria:
LOAD,STORE - Control:
JMP,BEQ,BNE - Especiales:
MOV,NOP,RET,GETID
- Aritmetica:
GETIDes la instruccion clave: le dice a cada thread cual es su indice
Como probar:
from isa.opcodes import Opcode
print(Opcode.ADD) # Opcode.ADD
print(Opcode.ADD.value) # el valor asignado
print(list(Opcode)) # lista todos los opcodes
# Verificar que existen los 13 opcodes esperados
assert hasattr(Opcode, 'GETID')
assert hasattr(Opcode, 'BEQ')Tarea 2 — isa/instruction.py
Representar una instruccion como objeto.
- Clase
Instructioncon campos:opcode,dest,src1,src2,label - Metodo
__repr__para debug:"ADD R1 R2 R3" - Soportar operandos inmediatos (numeros literales) ademas de registros
Como probar:
from isa.opcodes import Opcode
from isa.instruction import Instruction
i = Instruction(Opcode.ADD, dest='R1', src1='R2', src2='R3')
print(i) # ADD R1 R2 R3
# Con inmediato
i2 = Instruction(Opcode.ADDI, dest='R1', src1='R2', src2=5)
print(i2) # ADDI R1 R2 5
# Con label (para saltos)
i3 = Instruction(Opcode.JMP, label='loop')
print(i3) # JMP loopTarea 3 — gpu/thread.py
La unidad minima de ejecucion.
- Clase
Threadcon:thread_id,registers(dict),pc,state,active - Estado posible:
READY | RUNNING | FINISHED | STALLED - Metodos
read_register(name)ywrite_register(name, value)
Como probar:
from gpu.thread import Thread
t = Thread(thread_id=7)
print(t.thread_id) # 7
print(t.pc) # 0
print(t.state) # READY
print(t.active) # True
t.write_register('R1', 42)
print(t.read_register('R1')) # 42
# Leer registro que no existe debe retornar 0 (o lanzar error claro)
print(t.read_register('R99')) # 0Tarea 4 — gpu/warp.py
Grupo de 32 threads que ejecutan en lockstep.
- Clase
Warpcon:warp_id,threads(lista de 32),active_mask,pc,state - Metodo
get_active_threads()— retorna solo los threads activos - Metodo
is_done()— True si todos los threads terminaron - Manejar warp divergence: cuando un branch divide los threads, ejecutar ambos caminos con mascaras distintas
Como probar:
from gpu.warp import Warp
w = Warp(warp_id=0, start_thread_id=0)
print(len(w.threads)) # 32
print(w.is_done()) # False
print(len(w.get_active_threads())) # 32
# Simular que la mitad de los threads terminan
for t in w.threads[:16]:
t.active = False
print(len(w.get_active_threads())) # 16
print(w.is_done()) # False
# Terminar todos
for t in w.threads:
t.active = False
print(w.is_done()) # TrueTarea 5 — gpu/memory.py
Subsistema de memoria.
- Clase
GlobalMemory: dict o lista como storage, metodosread(addr)ywrite(addr, value), simular latencia con ciclos de espera - Clase
SharedMemory: una instancia por bloque, sin latencia, tamanio maximo configurable
Como probar:
from gpu.memory import GlobalMemory, SharedMemory
gm = GlobalMemory()
gm.write(0, 99)
print(gm.read(0)) # 99
print(gm.latency) # ciclos de espera (ej: 100)
# Shared memory — rapida, tamanio limitado
sm = SharedMemory(size=1024)
sm.write(0, 42)
print(sm.read(0)) # 42
# Verificar que SharedMemory no tiene latencia
# y que GlobalMemory si la tieneTarea 6 — gpu/core.py
El motor de ejecucion: aplica una instruccion a un thread.
- Clase
Coreconcore_idybusy - Metodo
execute(instruction, thread, memory)que implementa cada opcode:ADD/SUB/MUL/DIV: operaciones aritmeticas entre registrosADDI: aritmetica con inmediatoLOAD/STORE: leer y escribir en memoriaGETID: escribirthread.thread_iden el registro destinoMOV/NOP/RET: utilidadesJMP/BEQ/BNE: modificarthread.pc
Como probar:
from gpu.core import Core
from gpu.thread import Thread
from gpu.memory import GlobalMemory
from isa.opcodes import Opcode
from isa.instruction import Instruction
core = Core(core_id=0)
t = Thread(thread_id=5)
mem = GlobalMemory()
t.write_register('R1', 10)
t.write_register('R2', 3)
# ADD
core.execute(Instruction(Opcode.ADD, 'R3', 'R1', 'R2'), t, mem)
print(t.read_register('R3')) # 13
# GETID
core.execute(Instruction(Opcode.GETID, 'R0'), t, mem)
print(t.read_register('R0')) # 5
# LOAD / STORE
mem.write(0, 777)
core.execute(Instruction(Opcode.LOAD, 'R4', src1=0), t, mem)
print(t.read_register('R4')) # 777Tarea 7 — gpu/scheduler.py
Decide que warp ejecuta en cada ciclo.
- Clase
WarpSchedulercon lista de warps y politica (round_robinogreedy) - Metodo
next_warp()— retorna el proximo warpREADY, saltandoWAITINGyDONE round_robin: rotar en orden circulargreedy: siempre el primero disponible- Cuando un warp hace
LOAD, marcarloWAITINGpor N ciclos (simula latencia de memoria y latency hiding)
Como probar:
from gpu.warp import Warp
from gpu.scheduler import WarpScheduler
warps = [Warp(warp_id=i, start_thread_id=i*32) for i in range(4)]
sched = WarpScheduler(warps, policy='round_robin')
# Round robin: debe rotar entre warps
print(sched.next_warp().warp_id) # 0
print(sched.next_warp().warp_id) # 1
print(sched.next_warp().warp_id) # 2
# Marcar warp 3 como WAITING, debe saltarlo
warps[3].state = 'WAITING'
sched2 = WarpScheduler(warps, policy='round_robin')
ids = [sched2.next_warp().warp_id for _ in range(6)]
print(ids) # nunca debe aparecer 3Tarea 8 — gpu/sm.py
El SM integra cores, warps, scheduler y memoria compartida.
- Clase
SMcon:sm_id,cores,warps,scheduler,shared_memory, referencia aglobal_memory - Metodo
assign_block(block)— crea los warps a partir de un bloque de threads - Metodo
step()— ejecuta un ciclo de simulacion:- Pedir al scheduler el siguiente warp
- Fetch de la instruccion en
warp.pc - Ejecutar en todos los threads activos del warp (un core por thread)
- Avanzar
warp.pc - Retornar
Truesi quedan warps sin terminar
Como probar:
from gpu.sm import SM
from gpu.memory import GlobalMemory
from isa.opcodes import Opcode
from isa.instruction import Instruction
program = [
Instruction(Opcode.GETID, 'R0'),
Instruction(Opcode.RET),
]
gm = GlobalMemory()
sm = SM(sm_id=0, num_cores=32, global_memory=gm)
block = {'block_id': 0, 'threads': list(range(32))}
sm.assign_block(block, program)
# Ejecutar ciclos hasta terminar
while sm.step():
pass
# Verificar que cada thread tiene su ID en R0
for warp in sm.warps:
for t in warp.threads:
print(t.thread_id, t.read_register('R0')) # deben coincidirTarea 9 — gpu/gpu.py
El GPU completo: coordina los SMs y lanza kernels.
- Clase
GPUcon:num_sms,sms,global_memory,config - Metodo
launch_kernel(program, grid_dim, block_dim):- Calcular total de bloques
- Distribuir bloques entre SMs (round-robin)
- Cada SM recibe sus bloques via
assign_block()
- Metodo
run()— loop hasta que todos los SMs terminen, contar ciclos
Como probar:
from gpu.gpu import GPU
from isa.opcodes import Opcode
from isa.instruction import Instruction
program = [
Instruction(Opcode.GETID, 'R0'),
Instruction(Opcode.RET),
]
gpu = GPU(num_sms=2, cores_per_sm=32)
gpu.launch_kernel(program, grid_dim=4, block_dim=32)
stats = gpu.run()
print(stats['cycles']) # numero de ciclos totales
print(stats['instructions']) # instrucciones ejecutadas
# Con 4 bloques en 2 SMs, cada SM debe haber procesado 2 bloquesTarea 10 — isa/assembler.py
Convierte texto plano a lista de instrucciones.
- Funcion
assemble(source: str) -> list[Instruction] - Parsear linea por linea: ignorar comentarios (
#) y lineas vacias - Detectar labels (terminan en
:) y resolver saltos a indices numericos - Retornar lista de objetos
Instructionlista para ejecutar
Como probar:
from isa.assembler import assemble
source = """
# Suma simple
GETID R0
ADDI R1 R0 10
loop:
NOP
BNE R0 R1 loop
RET
"""
program = assemble(source)
print(len(program)) # 5 instrucciones (sin comentarios ni vacias)
print(program[0]) # GETID R0
print(program[3]) # BNE R0 R1 2 <- label resuelta a indiceTarea 11 — kernels/vector_add.asm
Primer kernel real escrito en el ISA propio.
- Suma de dos vectores A y B, resultado en C
- Cada thread calcula
C[i] = A[i] + B[i]usandoGETIDpara obtener su indice - Asumir layout en memoria:
Aen[0..N),Ben[N..2N),Cen[2N..3N)
Como probar:
from isa.assembler import assemble
with open("kernels/vector_add.asm") as f:
program = assemble(f.read())
# Verificar que ensambla sin errores
print(len(program), "instrucciones")
# Inspeccion manual: la primera instruccion debe ser GETID
print(program[0]) # GETID Rx
# La ultima debe ser RET
print(program[-1]) # RETLa prueba real es correrlo con el GPU completo en la Tarea 13.
Tarea 12 — sim/runner.py
Orquesta la ejecucion completa.
- Clase
SimRunnercon metodos:load_data(array_a, array_b, n)— escribe inputs en global memoryrun_kernel(program, grid_dim, block_dim)— ensambla, lanza, corre y lee resultadosprint_stats(stats)— imprime ciclos, instrucciones, throughput
Como probar:
from gpu.gpu import GPU
from sim.runner import SimRunner
gpu = GPU(num_sms=2, cores_per_sm=32)
runner = SimRunner(gpu)
N = 32
A = list(range(N))
B = list(range(N, 2*N))
runner.load_data(A, B, N)
# Verificar que la memoria global tiene los datos correctos
print(gpu.global_memory.read(0)) # 0 (A[0])
print(gpu.global_memory.read(N)) # 32 (B[0])Tarea 13 — main.py
Demo y validacion end-to-end.
- Configurar el GPU
- Crear arrays de prueba, cargarlos, ejecutar
vector_add - Verificar que
C[i] == A[i] + B[i]para todoi - Imprimir estadisticas
Como probar:
python main.pySalida esperada:
Ciclos totales: <N>
Instrucciones ejecutadas: <N>
Resultado correcto: True
Si Resultado correcto es False, revisar la cadena hacia atras: primero core.py (ejecuta bien ADD/LOAD/STORE?), luego assembler.py (resuelve bien las direcciones?), luego vector_add.asm (el layout de memoria es correcto?).
| Concepto | Descripcion |
|---|---|
| SIMT | 32 threads ejecutan la misma instruccion, datos distintos |
| Warp divergence | Un branch divide el warp — se ejecutan ambos caminos con mascara |
| Latency hiding | Mientras un warp espera memoria, el scheduler corre otro |
| Occupancy | Cuantos warps activos caben en un SM (limitado por registros y shared memory) |
| Active mask | Bitmask que indica que threads del warp estan activos en este ciclo |
gpu = GPU(num_sms=4, cores_per_sm=32)
runner = SimRunner(gpu)
N = 128
runner.load_data(A=list(range(N)), B=list(range(N, 2*N)), n=N)
with open("kernels/vector_add.asm") as f:
stats = runner.run_kernel(f.read(), grid_dim=4, block_dim=32)
runner.print_stats(stats)
# Ciclos totales: 42
# Instrucciones ejecutadas: 1024
# Resultado correcto: True