11 Tutorial — CNN × CUDA
One CNN, two backends — train it on CPU SIMD and CUDA GPU, then audit every tensor operator.
The series finale ties the whole stack together: a convolutional neural network defined in a handful of LayerBuilder lines trains on MNIST twice — first on the CPU SIMD kernel, then on the CUDA backend registered by CudaTensor.Register. Because the weight seed and the data are identical, the two runs are directly comparable, and the script goes one step further than a benchmark: it audits which backend every single tensor operator actually used.
02 Pipeline
One network, two execution backends
Define
A LayerBuilder assembles the network: input_layer(28×28, 1) →
conv_layer(5, 32) → relu → pool 2×2 →
conv_layer(5, 64) → relu → pool 2×2 →
full_connected_layer(10) → softmax, trained by
AdaGradTrainer(20, 0.001).
Probe
The environment probe prints the active backend (SIMD), the weight-init seed
(12345) and the training config (5 passes × 1000 images).
Train on CPU
Five passes over 1000 MNIST digits: loss falls 2.390 → 0.359, evaluation reaches 915 / 1000 (91.50%) in 77.4 s.
Switch to CUDA
CudaTensor.Register(engineOptions) translates and registers the double-precision IL2Cuda
kernels (thresholds MinGpuElements=4096, MinGemmElements=65536), then the exact
same training run executes again with loss 2.390 → 0.359 and the same 91.50%.
Compare
The script audits every tensor operator of the network — each line of the operator table names its gating operand and the backend that actually ran it — then reports the final loss/ accuracy/elapsed comparison.
01 The Script
Full demo source
The complete script exactly as executed by the sciBASIC# script engine (vbs.exe) — nothing elided.
#include "Microsoft.VisualBasic.DeepLearning.dll"
#include "Microsoft.VisualBasic.MachineLearning.TensorFlow.dll"
#include "Microsoft.VisualBasic.MachineLearning.DataStorage.dll"
#include "Microsoft.VisualBasic.Computing.ILCuda.dll"
#include "Microsoft.VisualBasic.Computing.ILCuda.GPUTensor.dll"
imports Microsoft.VisualBasic.ApplicationServices
imports Microsoft.VisualBasic.Language
imports Microsoft.VisualBasic.Linq
imports Microsoft.VisualBasic.MachineLearning.CNN
imports Microsoft.VisualBasic.MachineLearning.CNN.data
imports Microsoft.VisualBasic.MachineLearning.CNN.trainers
imports Microsoft.VisualBasic.MachineLearning.DataStorage
imports Microsoft.VisualBasic.MachineLearning.TensorFlow
imports Microsoft.VisualBasic.Computing.ILCuda.Runtime
imports Microsoft.VisualBasic.Computing.ILCuda.GPUTensor
' ============================================================================
' MNIST CNN CUDA acceleration demo (script version)
'
' The same logic as the MnistCnnGpuTest project under DeepLearning\test:
' 1) environment probe (default backend / random seed / training config)
' 2) run one pass with the CPU (SIMD) backend as the baseline
' 3) register the CUDA compute engine; on failure print diagnostics and
' fall back to CPU (so the script always produces a result)
' 4) run the very same configuration again with the CUDA backend
' 5) print, for every tensor operator of this network, whether it is gated
' onto the GPU or falls back to the CPU
' 6) CPU vs GPU numeric consistency, wall-clock time and speedup
'
' Run:
' vbs.exe tutorials\VBS\scripts\mnist_cnn\mnist_cnn.vb --mnist-data=<MNIST dir>
'
' Notes:
' * The network weights are initialized through a shared "unseeded" random
' generator; without fixing the seed every run starts from different
' weights and the results cannot be compared. The seed is therefore reset
' before each pass, so the CPU and GPU paths see exactly the same initial
' weights and sample order -- any difference can only come from the numeric
' implementation of the backend.
' * The training flow lives in the Public Class below (a top-level Function
' in a script is rewritten by the engine into an anonymous function, which
' can neither capture outer variables nor be reflected; type blocks are
' kept verbatim).
' ============================================================================
''' <summary>
''' One complete "build network -> train -> evaluate" pass.
''' </summary>
''' <remarks>
''' Executes on whichever backend <c>Tensor.computeKernel</c> currently points
''' to, so the same code can run on CPU (SIMD) or CUDA and the two can be
''' compared directly.
''' </remarks>
Public Class CnnRunner
''' <summary>
''' Run one deterministic training + evaluation pass.
''' </summary>
''' <returns>``{last pass loss, number of correct classifications, total samples, elapsed seconds}``</returns>
Public Shared Function Execute(imagesPath As String, labelsPath As String,
randomSeed As Integer, passes As Integer, samples As Integer) As (lastLoss as double, correct as double, size as double, cost_ms as double)
' The seed must be set before the network is built (i.e. before the
' first call to Vector.rand)
Call Microsoft.VisualBasic.Math.RandomExtensions.SetSeed(randomSeed)
Dim reader As New MNIST(imagesPath, labelsPath)
' Materialize the samples first, guaranteeing that every pass and every
' run sees the same batch of data in the same order
Dim dataset = reader.ExtractVectors.Take(samples).ToArray
' The network structure is identical to MnistTest in the repository:
' input 28x28x1 -> conv5x32 -> relu -> pool2 -> conv5x64 -> relu -> pool2 -> fc10 -> softmax
'
' Streamed with the LayerBuilder + operator, one-to-one with R# auto_encoder.R:
' let cnn = cnn() + input_layer([28,28],1) + conv_layer(5,32,1,2) + pool_layer(2,2,0) + ...
' The only difference is that VB implicit line continuation requires the
' binary operator to stay at the end of the previous line.
Dim cnn As LayerBuilder = New LayerBuilder() +
input_layer({reader.ImageSize.Width, reader.ImageSize.Height}, 1) +
conv_layer(5, 32, 1, 2) +
relu_layer() +
pool_layer(2, 2, 0) +
conv_layer(5, 64, 1, 2) +
relu_layer() +
pool_layer(2, 2, 0) +
full_connected_layer(10) +
softmax_layer()
Dim net As ConvolutionalNN = New ConvolutionalNN(cnn)
Dim trainer As TrainerAlgorithm = New AdaGradTrainer(20, 0.001F).SetKernel(net)
Dim db As New DataBlock(reader.ImageSize.Width, reader.ImageSize.Height, 1, 0)
Dim watch As Stopwatch = Stopwatch.StartNew()
Dim lastLoss As Double = 0
For p As Integer = 1 To passes
Dim passLoss As Double = 0
Dim check As New PerformanceCounter()
For Each digit In dataset
Call db.addImageData(digit.value, digit.value.Max)
Dim result As TrainResult = trainer.train(db, {Val(digit.description)}, check.Set)
passLoss += result.Loss
Next
lastLoss = passLoss / dataset.Length
Call Console.WriteLine($" pass {p}/{passes} loss={lastLoss:R}")
Next
Dim correct As double = 0
For Each digit In dataset
Call db.addImageData(digit.value, digit.value.Max)
If CInt(Val(digit.description)) = CInt(Which.Max(net.predict(db))) Then
correct += 1
End If
Next
watch.Stop()
Return (lastLoss, correct, cdbl( dataset.Length), cdbl( watch.Elapsed.TotalSeconds))
End Function
End Class
' ---------------------------------------------------------------------------
' 0) Command line arguments
' ---------------------------------------------------------------------------
dim mnist_repo = ?"--mnist-data"
dim images_file = $"{mnist_repo}\train-images-idx3-ubyte"
dim labels_file = $"{mnist_repo}\train-labels-idx1-ubyte"
dim random_seed = 12345
dim train_passes = 5
dim train_samples = 1000
' ---------------------------------------------------------------------------
' 1) Environment probe
' ---------------------------------------------------------------------------
call console.WriteLine("=== 1) Environment probe ===")
call console.WriteLine($" default backend = {Tensor.computeKernel.Name}")
call console.WriteLine($" random seed = {random_seed} (weight init)")
call console.WriteLine($" training config = {train_passes} passes x {train_samples} images")
' ---------------------------------------------------------------------------
' 2) CPU (SIMD) baseline
' ---------------------------------------------------------------------------
call console.WriteLine()
call console.WriteLine("=== 2) CPU(SIMD) training + evaluation ===")
dim (cpu_loss, cpu_correct,cpu_total,cpu_seconds) = CnnRunner.Execute(images_file, labels_file, random_seed, train_passes, train_samples)
call console.WriteLine($" backend={Tensor.computeKernel.Name} loss={cpu_loss:R} " &
$"correct={cpu_correct}/{cpu_total} ({cpu_correct / cpu_total:P2}) elapsed={cpu_seconds:F3}s")
' ---------------------------------------------------------------------------
' 3) Register the CUDA compute engine
' ---------------------------------------------------------------------------
call console.WriteLine()
call console.WriteLine("=== 3) Register the CUDA compute engine ===")
dim gpu_enabled = false
dim gpu_options as new EngineOptions()
if CudaTensor.Register(gpu_options) then
gpu_enabled = true
call console.WriteLine($" [OK] current backend = {Tensor.computeKernel.Name}")
call console.WriteLine($" thresholds : MinGpuElements={CudaTensor.MinGpuElements}, MinGemmElements={CudaTensor.MinGemmElements}")
if CudaTensor.KernelFailures.Count = 0 then
call console.WriteLine(" IL2Cuda double-precision kernels: all translated and registered successfully")
else
for each failure in CudaTensor.KernelFailures
call console.WriteLine($" [kernel translation failed] {failure.Key} -> {failure.Value}")
next
end if
else
call console.WriteLine($" [fallback] registration failed, using the CPU this run: {CudaTensor.LastError}")
for each line in gpu_options.Diagnostics
call console.WriteLine($" {line}")
next
end if
' ---------------------------------------------------------------------------
' 4) CUDA (GPU) training + evaluation
' ---------------------------------------------------------------------------
if gpu_enabled then
call console.WriteLine()
call console.WriteLine("=== 4) CUDA(GPU) training + evaluation ===")
dim (gpu_loss ,gpu_correct,gpu_total,gpu_seconds) = CnnRunner.Execute(images_file, labels_file, random_seed, train_passes, train_samples)
call console.WriteLine($" backend={Tensor.computeKernel.Name} loss={gpu_loss:R} " &
$"correct={gpu_correct}/{gpu_total} ({gpu_correct / gpu_total:P2}) elapsed={gpu_seconds:F3}s")
' -----------------------------------------------------------------------
' 5) The actual execution backend of every tensor operator of this network
'
' Key point: the gating operand is not the same for every operator
' (all verified against the CudaTensor implementation):
' * element-wise operators / MaxPool2D -- gated on the element count of
' the input tensor itself
' * Conv2D forward -- gated on the element count of
' the **input** x (NOT the output!)
' * Conv2DBackward* -- gated on gradOutput (i.e. the
' output of this layer)
' * MatMul -- gated on m*k*n
' Without making the gating operand explicit it is very easy to misread
' "some operators run on the GPU" as "everything runs on the GPU".
' -----------------------------------------------------------------------
call console.WriteLine()
call console.WriteLine("=== 5) Actual execution backend of every tensor operator ===")
dim min_gpu = CudaTensor.MinGpuElements
dim min_gemm = CudaTensor.MinGemmElements
dim image_size = 28
dim relu1_size = 28 * 28 * 32
dim pool1_size = 14 * 14 * 32
dim conv2_size = 14 * 14 * 64
dim pool2_size = 7 * 7 * 64
dim fc_gemm = 10 * pool2_size * 1
call console.WriteLine($" backend = {Tensor.computeKernel.Name}; single sample (N=1); thresholds MinGpuElements={min_gpu}, MinGemmElements={min_gemm}")
call console.WriteLine($" {"operator",-16}{"size",14}{" gating operand",-20}{"verdict"}")
call console.WriteLine($" {"conv1 fwd",-16}{image_size * image_size,14}{" input x",-20}{If(image_size * image_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"conv1 bwd",-16}{relu1_size,14}{" gradOutput",-20}{If(relu1_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"pool1 fwd",-16}{relu1_size,14}{" input x",-20}{If(relu1_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"pool1 bwd",-16}{pool1_size,14}{" gradOutput",-20}{If(pool1_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"relu1 fwd/bwd",-16}{relu1_size,14}{" input x",-20}{If(relu1_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"conv2 fwd",-16}{pool1_size,14}{" input x",-20}{If(pool1_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"conv2 bwd",-16}{conv2_size,14}{" gradOutput",-20}{If(conv2_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"pool2 fwd",-16}{conv2_size,14}{" input x",-20}{If(conv2_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"pool2 bwd",-16}{pool2_size,14}{" gradOutput",-20}{If(pool2_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"relu2 fwd/bwd",-16}{conv2_size,14}{" input x",-20}{If(conv2_size >= min_gpu, "GPU", "CPU fallback")}")
call console.WriteLine($" {"fc MatMul",-16}{fc_gemm,14}{" m*k*n",-20}{If(fc_gemm >= min_gemm, "GPU", "CPU fallback")}")
call console.WriteLine($" {"softmax",-16}{10,14}{" input x",-20}{If(10 >= min_gpu, "GPU", "CPU fallback")}")
' -----------------------------------------------------------------------
' 6) CPU vs GPU comparison
' -----------------------------------------------------------------------
call console.WriteLine()
call console.WriteLine("=== 6) CPU(SIMD) vs CUDA(GPU) ===")
dim loss_error = System.Math.Abs(cpu_loss - gpu_loss)
call console.WriteLine($" {"backend",-14}{"loss",-24}{"accuracy",-18}{"elapsed(s)"}")
call console.WriteLine($" {"SIMD(CPU)",-14}{cpu_loss,-24:R}{cpu_correct / cpu_total,-18:P2}{cpu_seconds:F3}")
call console.WriteLine($" {"CUDA(GPU)",-14}{gpu_loss,-24:R}{gpu_correct / gpu_total,-18:P2}{gpu_seconds:F3}")
call console.WriteLine()
call console.WriteLine($" loss max abs error = {loss_error:E3} " &
If(loss_error < 1.0E-9, "OK", "FAIL"))
call console.WriteLine($" correct count = CPU {cpu_correct}/{cpu_total} GPU {gpu_correct}/{gpu_total} " &
If(cpu_correct = gpu_correct, "OK", "difference see above"))
call console.WriteLine($" elapsed ratio = CPU {cpu_seconds:F3}s GPU {gpu_seconds:F3}s " &
$"ratio={cpu_seconds / gpu_seconds:F2}x")
call console.WriteLine()
call console.WriteLine(" note: a loss difference only at the rounding level means the GPU and CPU")
call console.WriteLine(" numeric implementations agree. With a single sample (N=1) the end-to-end")
call console.WriteLine(" run is not faster than the CPU -- ILCudaTensor uses a copy-based")
call console.WriteLine(" execution model that performs an H2D upload + kernel + D2H read-back")
call console.WriteLine(" on every operator call, and pooling/full-connected/softmax are below")
call console.WriteLine(" the GPU threshold and already fall back to the CPU. GPU throughput only")
call console.WriteLine(" shows up with large batched tensors.")
else
call console.WriteLine()
call console.WriteLine("=== 4-6) skipped ===")
call console.WriteLine($" GPU not enabled, CPU result = loss {cpu_loss:R}, {cpu_correct}/{cpu_total} ({cpu_correct / cpu_total:P2})")
end if
call console.WriteLine()
call console.WriteLine("done: mnist-cnn")
03 Results
The full session, verbatim
Environment probe, both training runs, the per-operator backend audit and the final comparison — the complete 60-line console log:
=== 1) Environment probe ===
default backend = SIMD
random seed = 12345 (weight init)
training config = 5 passes x 1000 images
=== 2) CPU(SIMD) training + evaluation ===
pass 1/5 loss=2.390096552476107
pass 2/5 loss=0.8209807836912226
pass 3/5 loss=0.5247906421962469
pass 4/5 loss=0.4266240644466655
pass 5/5 loss=0.3594098222432395
backend=SIMD loss=0.3594098222432395 correct=915/1000 (91.50%) elapsed=77.354s
=== 3) Register the CUDA compute engine ===
[OK] current backend = CUDA
thresholds : MinGpuElements=4096, MinGemmElements=65536
IL2Cuda double-precision kernels: all translated and registered successfully
=== 4) CUDA(GPU) training + evaluation ===
pass 1/5 loss=2.3900965524761064
pass 2/5 loss=0.8209807836912223
pass 3/5 loss=0.5247906421962472
pass 4/5 loss=0.42662406444666534
pass 5/5 loss=0.3594098222432393
backend=CUDA loss=0.3594098222432393 correct=915/1000 (91.50%) elapsed=107.877s
=== 5) Actual execution backend of every tensor operator ===
backend = CUDA; single sample (N=1); thresholds MinGpuElements=4096, MinGemmElements=65536
operator size gating operand verdict
conv1 fwd 784 input x CPU fallback
conv1 bwd 25088 gradOutput GPU
pool1 fwd 25088 input x GPU
pool1 bwd 6272 gradOutput GPU
relu1 fwd/bwd 25088 input x GPU
conv2 fwd 6272 input x GPU
conv2 bwd 12544 gradOutput GPU
pool2 fwd 12544 input x GPU
pool2 bwd 3136 gradOutput CPU fallback
relu2 fwd/bwd 12544 input x GPU
fc MatMul 31360 m*k*n CPU fallback
softmax 10 input x CPU fallback
=== 6) CPU(SIMD) vs CUDA(GPU) ===
backend loss accuracy elapsed(s)
SIMD(CPU) 0.3594098222432395 91.50% 77.354
CUDA(GPU) 0.3594098222432393 91.50% 107.877
loss max abs error = 1.665E-016 OK
correct count = CPU 915/1000 GPU 915/1000 OK
elapsed ratio = CPU 77.354s GPU 107.877s ratio=0.72x
note: a loss difference only at the rounding level means the GPU and CPU
numeric implementations agree. With a single sample (N=1) the end-to-end
run is not faster than the CPU -- ILCudaTensor uses a copy-based
execution model that performs an H2D upload + kernel + D2H read-back
on every operator call, and pooling/full-connected/softmax are below
the GPU threshold and already fall back to the CPU. GPU throughput only
shows up with large batched tensors.
done: mnist-cnn
ILCudaTensor uses a copy-based execution model
(H2D upload + kernel + D2H read-back per operator call), and pooling / fully-connected / softmax sit below
the GPU thresholds and fall back to the CPU. What the log does prove is exactness: the loss curves
agree to 1.665E-016, the loss max abs error is at the rounding level, and both backends
classify exactly the same 915 digits. GPU throughput shows up once tensors arrive in large batches —
exactly what the thresholds encode.