From API to GPU, Week 5: Tensors, the Data Structure Behind Every Model A developer's ongoing 32-week project to understand AI inference reached Phase 2, focusing on tensors, the core data structure of machine learning models. The developer demonstrated creating tensors in PyTorch, inspecting their properties, and moving them to a GPU, while also starting a parallel CUDA track. The work runs on an NVIDIA DGX Spark system. Phase 2 of 8: Enough ML to understand inference. Week 5 of 32. Phase 1 was about running models. Phase 2 is about understanding what happens inside them, starting with the one data structure they are all built from: the tensor . This week I stop talking about model files and start touching the actual numbers, in PyTorch, on the GPU. If you write software, a tensor is a typed, multi-dimensional array that lives on a specific device. By the end of this post I can create tensors, read their shape and byte size, move them to the GPU, and measure how precision changes both speed and memory. This is also where the parallel CUDA track starts. I am not writing GPU kernels yet, the small programs that run on the GPU. Level 1 is just being a competent CUDA user: picking a device, moving data to it, and timing GPU work correctly.1 Everything runs on the DGX Spark over ssh spark . I reuse the Week 1 PyTorch environment, ~/venvs/w1 , which already has a CUDA build of PyTorch. Here are the names used for different array dimensions: Rank is the number of dimensions. Shape is the size along each dimension. Dtype is the number format, the same FP32, FP16, and BF16 I measured in Week 4. Device is where the tensor lives, the CPU or the GPU. Instead of describing this, I print it. The first script builds a scalar, vector, matrix, and 3-D tensor and reports each one's rank, shape, dtype, byte size, and device. It then shows three operations I explain right after: an element-wise add position by position , a broadcast a smaller tensor applied across a larger one , and a host-to-device transfer moving a tensor to the GPU : bash /usr/bin/env python3 """Week 5 - tensor basics: rank, shape, dtype, bytes, and device. Shows a scalar, vector, matrix, and 3D tensor, then one element-wise operation, one broadcast, and one CPU-to-GPU transfer. Every printed size and byte count comes from PyTorch, not from a hand estimate. """ from future import annotations import torch def describe name: str, tensor: torch.Tensor - None: print f"{name:8} rank={tensor.ndim} " f"shape={tuple tensor.shape } " f"dtype={str tensor.dtype .replace 'torch.', '' } " f"elem={tensor.element size }B " f"total={tensor.nbytes}B " f"dev={tensor.device}" def main - None: scalar = torch.tensor 3.0 vector = torch.tensor 1.0, 2.0, 3.0 matrix = torch.zeros 2, 3 tensor3d = torch.zeros 2, 3, 4 print "=== rank, shape, dtype, bytes, device ===" describe "scalar", scalar describe "vector", vector describe "matrix", matrix describe "tensor3d", tensor3d print "\n=== same shape, three precisions ===" for dtype in torch.float32, torch.float16, torch.bfloat16 : describe str dtype .replace "torch.", "" , torch.zeros 1024, 1024, dtype=dtype print "\n=== element-wise operation ===" a = torch.tensor 1.0, 2.0, 3.0 b = torch.tensor 10.0, 20.0, 30.0 print "a + b =", a + b .tolist print "\n=== broadcasting ===" matrix = torch.ones 2, 3 row = torch.tensor 1.0, 2.0, 3.0 print "matrix shape", tuple matrix.shape , "+ row shape", tuple row.shape print "result:\n", matrix + row .tolist print "\n=== device transfer ===" if torch.cuda.is available : cpu tensor = torch.ones 3 gpu tensor = cpu tensor.to "cuda" print "cpu tensor.device", cpu tensor.device print "gpu tensor.device", gpu tensor.device else: print "CUDA not available; skipping GPU transfer" if name == " main ": main I ran it on the Spark. These commands use public/... paths because I run them from the parent repository. If you cloned the public companion repo, drop the public/ prefix and run from that repo root. ssh spark '~/venvs/w1/bin/python -' \ < public/week-05-pytorch-tensors/tensor basics.py === rank, shape, dtype, bytes, device === scalar rank=0 shape= dtype=float32 elem=4B total=4B dev=cpu vector rank=1 shape= 3, dtype=float32 elem=4B total=12B dev=cpu matrix rank=2 shape= 2, 3 dtype=float32 elem=4B total=24B dev=cpu tensor3d rank=3 shape= 2, 3, 4 dtype=float32 elem=4B total=96B dev=cpu === same shape, three precisions === float32 rank=2 shape= 1024, 1024 dtype=float32 elem=4B total=4194304B dev=cpu float16 rank=2 shape= 1024, 1024 dtype=float16 elem=2B total=2097152B dev=cpu bfloat16 rank=2 shape= 1024, 1024 dtype=bfloat16 elem=2B total=2097152B dev=cpu === element-wise operation === a + b = 11.0, 22.0, 33.0 === broadcasting === matrix shape 2, 3 + row shape 3, result: 2.0, 3.0, 4.0 , 2.0, 3.0, 4.0 === device transfer === cpu tensor.device cpu gpu tensor.device cuda:0 There is a lot in that output, so here is what matters. The shape notation is worth decoding once. means zero dimensions, a single number. 3, means one dimension holding three values, the trailing comma marking it as a shape rather than a plain number. 2, 3 means two rows and three columns. 2, 3, 4 adds a third dimension. I never set a dtype in those constructors, and every tensor still came out float32 . That is PyTorch's default floating-point type. The precision block below sets the dtype on purpose. Look at the vector: shape 3, , 4 bytes per element, 12 total bytes. That is just 3 times 4. The matrix is 2 times 3 times 4, which is 24 bytes. The rule is the same one from Week 4: total bytes equals element count times bytes per element. PyTorch reports it directly with nbytes , so I no longer have to estimate. The three-precision block makes the point sharper. The same 1024, 1024 shape is 4,194,304 bytes at FP32 but 2,097,152 bytes at FP16 and BF16, exactly half. A dtype decides two things at once: the byte size of every number, and which values the number can represent. This experiment measures the byte size. The next one looks at the values. FP16 and BF16 both report 2 bytes, so they use the same memory. The difference is how they split those 16 bits between range how large a number can get and step size how finely close numbers can be told apart . I read both directly with torch.finfo : python ssh spark '~/venvs/w1/bin/python - <