A Simple Training of an AI Transformer to Play
Tic-Tac-Toe in C/C++

Materials available at: http://forejune.co/cuda/

Tic-Tac-Toe Game

See also

Decoder-Only vs Encoder-Decoder Transformer

  • Transformers

  • Input and output in different "languages" or formats, go with encoder-decoder

  • Generating new content from scratch based on a prompt, use decoder-only

  • For tic-tac-toe, decoder-only is more natural

  • Previous moves --> next move
      P(next move | previous moves)
      		 O |   | 
      		---------
      		   | X | 
      		---------
      		   |   | X
      		

  • GPT-style (decoder-only) transformer is ideal for this

  • Encoder-decoder is an overkill
    No need for a separate encoder to process the input and a decoder to generate output
  • Representing Tic-Tac-Toe as Tokens

  • A Tic-Tac-Toe board has 9 squares:
    		0 | 1 | 2
    		---------
    		3 | 4 | 5
    		---------
    		6 | 7 | 8
    		
  • We can represent moves as token IDs:
    
    		Token		Meaning
    		0		move at square 0
    		1		move at square 1
    		.		.
    		.		.
    		8		move at square 8
    		9		SOS (start of sequence)
    		10		EOS (end of sequence)
    		
    	So token 0 means "mark square 0"
    	token 6 means "mark square 6"
    
    	This encoding enables the model to learn game sequences autoregressively, 
    	predicting the next move based on all previous moves. 
  • Building a Database for Training

  • Minimax can include all optimal moves.

  • For simplicity, we build a training dataset manually.

    collectData.cpp:

    /* http://forejune.co/cuda
     * collectData.cpp : Collecting tic-tac-toe game data by playing 
     *
     */
    
    #include <GL/gl.h>
    #include <GL/glu.h>
    #include <GL/glut.h>
    #include <iostream>
    #include <fstream>
    #include <vector>
    //#include "player.h"
    const int SOS = 9;    // start of sequence
    const int EOS = 10;   // end of sequence
    
    enum Player {EMPTY=0, X, O};
    
    using namespace std;
    
    vector<int>sequence;
    bool saved = false;
    
    Player ai = X;
    Player opp = O;
    Player board[9];
    bool playing = false;
    Player turn;		//determine if it's ai or opp's turn
    const float d = 1;	//drawing distance between lines = 2*d
    int	whoWon = -1;
    
    void resetBoard()
    {
      for (int i = 0; i < 9; i++)
        board[i] = EMPTY;
    
      playing = false;
      whoWon = -1;
      sequence.clear();
      saved = false;
    }
    
    void displayMessage(float x, float y, void* font, const string& str) 
    {
        glRasterPos2f(x, y);
    
        // Loop through each character of the string and draw it
        for (char const& c : str) 
            glutBitmapCharacter(font, c);
    }
    
    void printBoard()
    {
       float di = -d/2.0;		//align image center at center of square
       // positions to display X or O
       float cx[9] = {-2*d, 0, 2*d, -2*d, 0, 2*d, -2*d, 0, 2*d};
       float cy[9] = {2*d, 2*d, 2*d, 0, 0, 0, -2*d, -2*d, -2*d};
       for (int i = 0; i < 9; i++ ) {
           if (board[i] != EMPTY) {
    	  if (board[i] == X) {
    	     glColor3f(1, 0, 0);	//red color
                 displayMessage(cx[i]+di, cy[i]+di, GLUT_BITMAP_TIMES_ROMAN_24, "X");
    	  }else {
    	     glColor3f(0, 1, 0);	//use green color
                 displayMessage(cx[i]+di, cy[i]+di, GLUT_BITMAP_TIMES_ROMAN_24, "O");
    	  }
           } 
       } // for
     
       if ( whoWon >= 0 ) {
         string str[] = {"AI has won!", "You have won!", "It was a draw!"};
         displayMessage(-3*d, -5*d, GLUT_BITMAP_TIMES_ROMAN_24, str[whoWon]);
       } 
       glFlush();
    }
    
    int window;
    int screenWidth = 500, screenHeight = 500;
    FILE *fp = NULL;
    
    void init(void)
    {   
      glClearColor(1, 1, 1, 0);     //clear color buffer with white color
      glClear(GL_COLOR_BUFFER_BIT); //clear color buffer
      //define coordinate system
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      gluOrtho2D(-10, 10, -10, 10);
      glPointSize( 3 );
      glColor3f(0.0, 0.0, 0.0);             //draw with black color
    
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
      char fname[] = "data.txt";
      if ( (fp = fopen(fname, "w+")) == NULL ) {
        printf("\nError opening file %s\n", fname);
        exit( 1 );
      }
      fseek(fp, 0,  SEEK_END);
      long fileSize = ftell(fp);
      if (fileSize > 1){	//file not empty
        fseek(fp, -4, SEEK_CUR);	//get rid of the -1 marker
      }
      cout << "p -- play game" << endl;
      cout << "r -- reset board" << endl;
    }
    
    void line (float x0, float y0, float x1, float y1)
    {
      glBegin(GL_LINES);
        glVertex2f(x0, y0);
        glVertex2f(x1, y1);
      glEnd();
    }
    void display(void)
    {
       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
       glLineWidth( 3 );
       glColor3f(0, 0, 0);
       line(-3*d, d, 3*d, d);		//upper horizontal line 
       line(-3*d, -d, 3*d, -d);		//lower horizontal line 
       line(-d, 3*d, -d, -3*d);		//left vertical line
       line(d, 3*d, d, -3*d);		//right vertical line
    
       glFlush();
    }
      
    bool isWinning(Player player)
    {
        const int w[8][3] = {               //winning positions
            {0,1,2},{3,4,5},{6,7,8},        //rows
            {0,3,6},{1,4,7},{2,5,8},        //columns
            {0,4,8},{2,4,6}                 //diagonals
        };
        for (int i = 0; i < 8; i++){
          if (board[w[i][0]]==player && board[w[i][1]]==player
                          && board[w[i][2]]==player)
            return true;
         }
    
        return false;
    }
    
    bool emptyCell()
    {
        //chekc if more empty space
        for (int i = 0; i < 9; i++)
           if (board[i] == EMPTY)
             return true;
    
        return false;   //no more empty space
    }
    
    int checkStatus()
    {
       if ( isWinning( ai ) ) 
            return 0;	
       else if ( isWinning( opp ) ) 
            return 1;	
        else if ( !emptyCell() ) 
            return 2;	
    
        return -1;
    }
    
    void play()
    {
      if ( playing )
        return;
      
      //ai goes first
      int move = 4;
      turn = ai;
      board[move] = ai;
      printBoard();
      sequence.push_back(SOS);
      sequence.push_back(move); 
    }
    
    void saveSequence()
    {
      int n = sequence.size();
      fprintf(fp, "%4d", n);
      printf("%4d", n);
      for (int i = 0; i < n; i++) {
        fprintf(fp, "%4d", sequence[i]);
        printf("%4d", sequence[i]);		// for reference
      }
      fprintf(fp, "\n");
      printf("\n");
    }
    
    void keyboard(unsigned char key, int x, int y)
    {
      switch(key) {
        case 27: /* escape */
            glutDestroyWindow(window);
    	if ( fp != NULL ) {
    	  fprintf(fp, "%4d", -1);
    	  fclose( fp );
    	}
            exit(0);
        case 'p':	//play game
    	if ( !playing ) {
    	  play();
    	  playing = true;
    	}
    	break;
        case 'r':	//reset board
    	resetBoard();
    	glutPostRedisplay();
    	break;
      }
    }
    
    int getLocation(float wx, float wy)
    {
      if (wx < -d) {
        if (wy > d)
     	return 0;
        else if (wy > -d)
    	return 3;
        else
    	return 6;
       }else if (wx < d) {
        if (wy > d)
     	return 1;
        else if (wy > -d)
    	return 4;
        else
    	return 7;
       }else {
        if (wy > d)
     	return 2;
        else if (wy > -d)
    	return 5;
        else
    	return 8;
       }
    }
    
    
    void mouse(int button, int state, int mx, int my)
    {
       if ( !playing )
         return;
       int x = mx, y = screenHeight - my;
       float wx = (float) x * 20.0/screenWidth - 10;	//world coordinates
       float wy = (float) y * 20.0/screenHeight - 10;	
       if ( button == GLUT_LEFT_BUTTON && state == GLUT_DOWN ){
          turn = (turn == ai) ? opp : ai ;
          if (whoWon < 0 ) {	//game not done
            int loc = getLocation(wx, wy);
            if (board[loc] == EMPTY) {
    	  sequence.push_back(loc);
    	  board[loc] = turn;
    	  whoWon = checkStatus();
    	  printBoard();
            }
          }
       }
       if (whoWon >= 0 && !saved){
        if ( whoWon == 0 || whoWon == 2 ) {  //AI winning or draw
         sequence.push_back( EOS ); 
         saveSequence();
         saved = true;
        } else
         resetBoard();		// discard user winning sequence
       }
    
    }
    
    int graphics(int argc, char** argv)
    {
       glutInit(&argc, argv);
       glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);
       glutInitWindowSize(screenWidth, screenHeight);
       glutInitWindowPosition(100, 100);
       window = glutCreateWindow(argv[0]);
       init();
       glutDisplayFunc(display);
       glutKeyboardFunc(keyboard);
       glutMouseFunc( mouse );
       glutMainLoop();
       return 0; 
    }
    
    
    int main(int argc, char** argv)
    {
      graphics(argc, argv);
    
      return 0;
    }
    					 

  • Transformer classes and Helper Functions

    // player.h
    // http://forejune.co/cuda/
    #ifndef __player_H__
    #define __player_H__
    
    const int SOS = 9;    // start of sequence
    const int EOS = 10;   // end of sequence
    
    enum Player {EMPTY=0, X, O};
    
    #endif
    
    // util.h
    
    #ifndef __UTIL_H__
    #define __UTIL_H__
    #include <vector>
    #include <algorithm>
    #include <math.h>
    #include <iostream>
    #include <iomanip>
    #include <random>
    
    using namespace std;
    
    typedef vector<vector<double>>  matrixd;
    
    // an activation function
    double f(double z);
    
    // Relu activation function with matrix input
    matrixd relu(const matrixd &X);
    
    // Derivative of Relu
    matrixd relu_deriv (const matrixd &X);
    
    
    // calculates entropy loss
    double crossEntropy(const matrixd &P, const vector<int> &target);
    
    // add two matrices
    matrixd addMat(const matrixd &A, const matrixd &B);
    
    // Transpose matrix n x m, B = A^T : m x n
    matrixd transpose(const matrixd& A);
    
    // Matrix multiplication   C = A X B   A: nxm, B: mxr, C: nxr
    matrixd matmul(const matrixd& A, const matrixd& B);
    
    // multiply a matrix by a scalar
    matrixd mulMats (const matrixd &A, const double s);
    
    // scale a matrix
    void scaleMat(matrixd &A, const double s);
    
    // apply masking to S
    matrixd causalMask(matrixd S);
    
    // update weight matrix W -= dL_dW * eta
    void updateWeight(matrixd &W, const matrixd &dL_dW, const double eta);
    
    // initialize an input matrix with certain random values
    void initMatrix(matrixd& M, const int n, const int m);
    
    void initMatrix(matrixd& M, const int n, const int m, double dModel);
    
    void saveMatrix(const matrixd &W, const int n, const int m, FILE *fp);
    
    void readMatrix(matrixd &W, const int n, const int m, FILE *fp);
    
    // print a token dictionary
    void printDictionary(unordered_map<string, int> &d);
    
    // print a matrix
    void printMatrix(const matrixd &y);
    
    // print a vector
    void printVec(const vector<int>& v);
    
    // Add & Norm
    matrixd addNnorm(const matrixd &A, const matrixd &B);
    
    void clip(matrixd &A, double max_val);
    
    // softmax, 1D vector input
    vector<double> softmax(const vector<double> &v);
    
    // softmax activation function, 2D input vector
    matrixd softmax(const matrixd& input);
    
    // derivative function of softmax
    matrixd softmax_derivative(const matrixd &dL_dP, const matrixd &P);
    #endif
    
    // transGpt.h
    #ifndef TRANSGPT_H
    #define TRANSGPT_H
    
    #include "player.h"
    
    class Tokenizer
    {
    private:
        unordered_map<string, int> words;
        unordered_map<int, string> wordIndex;
        int nTokens;        // number of words
    
        // special tokens
        const string PAD = "<PAD>";
        const string UNK = "<UNK>";
        const string BOS = "<BOS>";
        const string EOS = "<EOS>";
        const string SEP = "<SEP>";
    
        void addWord(const string &w) 
        {
            if (words.find(w) == words.end()) {
                int id = nTokens;
                words[w] = id;
                wordIndex[id] = w;
                nTokens++;
            }
        }
    
        vector<string> split(const string &str) 
        {
            vector<string> tokens;
            stringstream ss(str);
            string word;
            while (ss >> word) {
                tokens.push_back(word);
            }
            return tokens;
        }
    
    public:
        Tokenizer();
        Tokenizer(ifstream &fs);
        unordered_map<string, int>  getTokensWords();
        vector<int> tokenize(const string& str);
        string detokenize(const vector<int>& tokenIDs);
        int get_nTokens() const;
        unordered_map<int, string> get_wordIndex();
    };
    
    class Embedding
    {
    private:
        int nTokens;        // number of words
        int dim;            // embedding dimension
        matrixd em_matrix;  // embedding matrix;
    public:
        Embedding(int sequence_length, int embeddingDimension);
        // create embedding matrix with tokenIDs
        matrixd embed(const vector<int>& tokenIDs);
        int get_embedDim() const;
        int get_nTokens() const;
        void backProp(const vector<int>& tokenIDs, const matrixd& dL_dX,
                             double eta);
    
    };
    
    // Positional Encoding
    class PositionalEncoding
    {
    private:
        int maxTokens; // maximum sequence length
        int dModel;    // embedding dimension
        matrixd PE;    // positional_encodings matrix;
    public:
        PositionalEncoding(int max_seq_length, int embedding_dimension);
        matrixd addPE(const matrixd& embeddings);
        matrixd getPE();
    };
    
    // Add & Norm
    class AddnNorm
    {
    private:
        vector<double> gamma;   // size dModel
        vector<double> beta;    // size dModel
    
        matrixd Zhat;           // normalized values (nt x d)
        vector<double> mean;    // per row (nt)
        vector<double> var;     // per row (nt)
    
        double eps;             // small value
    
    public:
       // constructor
       AddnNorm(int dModel);
    
       // forward computation
       matrixd  forward (const matrixd &A, const matrixd &B);
    
       // Assume forward already filled: Zhat, mean, var
       matrixd backProp(const matrixd &X, const matrixd &dL_dY, double eta);
    
       // Optional setters (to plug in forward results)
       void setCache(const matrixd& zhat, const vector<double>& m,
                      const vector<double>& v);
    
        // Accessors (optional)
        const vector<double>& getGamma();
        const vector<double>& getBeta();  
        void saveGammaBeta(FILE *fp);
        void readGammaBeta(FILE *fp);
    };
    
    // Feed-forward Network
    class FeedForward
    {
    private:
        matrixd W1, W2;
        int dModel, d_ff;
    
        // cache
        matrixd F1;   // after activation function ReLU
        matrixd Z;    // before ReLU (needed for derivative)
    
    public:
        // constructor
        FeedForward(int dModel, int d_ff_);
        // ------------------------------
        // Forward (for cache)
        // ------------------------------
        matrixd FFoutput(const matrixd &X);
     // ------------------------------
        // Backprop
        // ------------------------------
        matrixd backProp(const matrixd &X, const matrixd &dL_dY,  double eta);
        void saveWeights(FILE *fp);
        void readWeights(FILE *fp);
    };
    
    // ----------- MultiHeadAttention -----------
    class MultiHeadAttention {
    private:
        int dModel, nHeads, d_k;
    
        vector<matrixd> WQ, WK, WV; // per head
        matrixd WO;
    
        // cache
        vector<matrixd> Qs, Ks, Vs, Ps, As;
    public:
        MultiHeadAttention(int dModel, int nHeads);
        matrixd computeAttention(const matrixd &X_Q, const matrixd &X_K, const matrixd &X_V,
                                 bool mask);
         matrixd backProp( const matrixd &X_Q, const matrixd &X_K, const matrixd &X_V,
            const matrixd &dL_dH, double eta, bool mask);
         void saveWeights(FILE *fp);
         void readWeights(FILE *fp);
    };
    
    // ----------------- OutputLayer ---------------
    class OutputLayer {
    private:
        matrixd W;   // dModel x vocab_size
        int dModel, vocab_size;
    
    public:
        OutputLayer(int dModel, int vocab_size);
        matrixd forward(const matrixd &X);
        matrixd backward(const matrixd &X, const matrixd &P,
                         const vector<int> &targets, double eta);
        void saveWeights(FILE *fp);
        void readWeights(FILE *fp);
    };
    
    // ------------------- GPT-style Transformer ---------------
    class GPTBlock {
    private:
        MultiHeadAttention mha;
        AddnNorm norm1;
        FeedForward ffn;
        AddnNorm norm2;
    
    public:
        GPTBlock(int dModel, int nHeads, int d_ff);
        matrixd forward(const matrixd &X);
        matrixd backward(const matrixd &X, const matrixd &dL_dY, double eta);
        void saveWeights(FILE *fp);
        void readWeights(FILE *fp);
    };
    
    class MiniGPT {
    private:
        Embedding embed;
        PositionalEncoding pe;
        vector<GPTBlock> layers;
        OutputLayer output;
    
        int seq_len, vocab_size;
    
    public:
        MiniGPT(int vocab_size, int seq_len, int dModel, 
    		    int nHeads, int d_ff, int nLayers);
        matrixd forward(const vector<int> &tokens);
        double trainStep(const vector<int> &tokens, double eta);
        int saveWeights(char fname[]);
        int readWeights(char fname[]);
    };
    #endif
    // util.cpp: helper functions
    // http://forejune.co/cuda/
    
    #include "util.h"
    #include <random>
    
    using namespace std;
    
    // Activation function: using ReLU (Rectified Linear Unit)
    double f(double z)
    {
      return max(0.0, z);
    }
    
    // Relu activation function with matrix input
    matrixd relu(const matrixd &X)
    {
        int rows = X.size();
        int cols = X[0].size();
        matrixd Y = X;	// same dimension of X
       
        for(int i = 0; i < rows; i++)
            for(int j = 0; j < cols; j++)
                Y[i][j] = max(0.0, X[i][j]);
    
        return Y;
    }
    
    // Derivative of Relu
    matrixd relu_deriv(const matrixd &X)
    {
        int rows = X.size();
        int cols = X[0].size();
        matrixd Y = X;	// same dimension as X
       
        for(int i = 0; i < rows; i++)
            for(int j = 0; j < cols; j++)
    	  if ( X[i][j] > 0 )
                Y[i][j] = 1;
    	  else
                Y[i][j] = 0;
    
        return Y;
    }
    
    // calculates entropy loss 
    double crossEntropy(const matrixd &P, const vector<int> &target)
    {
      double L = 0;
      int nt = P.size();
      for (int i = 0; i < nt; i++){
        int k = target[i];
        L -= log( P[i][k] + 1e-12 );
      }
    
      return L / nt;
    }
    
    // C = A + B
    matrixd addMat(const matrixd &A, const matrixd &B)
    {
         int n = A.size();          // rows
         int m = A[0].size();       // cols
    
         matrixd C = A;       // same dimension as A
         for (int i = 0; i < n; i++)
           for(int j = 0; j < m; j++)
             C[i][j] = A[i][j] + B[i][j];
    
         return C;
    }
    
    // Transpose matrix n x m, B = A^T : m x n
    matrixd transpose(const matrixd& A)
    {
       int n = A.size();    //number of rows
       int m = A[0].size(); //number of columns
    
       matrixd B(m, vector<double>(n));
       for (int i = 0; i < n; i++)
         for (int j = 0; j < m; j++)
           B[j][i] = A[i][j];
    
       return B;
    }
    
    // Matrix multiplication   C = A X B   A: nxm, B: mxr, C: nxr
    matrixd matmul(const matrixd& A, const matrixd& B)
    {
      int n  = A.size();
      int m = A[0].size();
      int r = B[0].size();
    
      matrixd C(n, vector<double>(r, 0));
    
      for (int i = 0; i < n; i++)
        for (int j = 0; j < r; j++)
          for (int k = 0; k < m; k++)
            C[i][j] += A[i][k] * B[k][j];
    
       return C;
    }
    
    // scale a matrix
    void scaleMat(matrixd &A, const double s)
    {
       int m = A.size();	// number of rows
       int n = A[0].size();    // number of columns
    
       for (int i = 0; i < m; i++)
         for (int j = 0; j < n; j++)
           A[i][j] *= s;
    }
    
    // maultiply a matrix by a scalar s
    matrixd mulMats (const matrixd &A, const double s)
    {
       int m = A.size();	// number of rows
       int n = A[0].size();    // number of columns
      
       matrixd B = A;
    
       for (int i = 0; i < m; i++)
         for (int j = 0; j < n; j++)
           B[i][j] *= s;
    
       return B;
    }
    
    void initMatrix(matrixd& M, const int n, const int m)
    {
       // resizing 2D vector to n x m with initial values 0
       M.assign(n, vector<double>(m, 0));
       for (int i = 0; i < n; ++i)
         for (int j = 0; j < m; ++j)
            M[i][j] = ((double)rand() / RAND_MAX - 0.5);
    }
    
    void saveMatrix(const matrixd &W, const int n, const int m, FILE *fp)
    {
       for (int i = 0; i < n; i++){
        for (int j = 0; j < m; j++)
          fprintf(fp, "%9.4f", W[i][j]);
        fprintf(fp, "\n");
      }
    }
    
    void readMatrix(matrixd &W, const int n, const int m, FILE *fp)
    {
       for (int i = 0; i < n; i++){
        for (int j = 0; j < m; j++)
          fscanf(fp, "%lf", &W[i][j]);
      }
    }
    
    void printDictionary(unordered_map<string, int> &d)
    {
      unordered_map<string, int>::iterator it = d.begin();
      int k = 0;
      while ( it != d.end() ){
        cout << right << setw(12) << it->first << ": " << setw(3) << it->second;
        it++;
        k++;
        if ( k % 5 == 0 )
          cout << endl;
      }
    }
    
    void printMatrix(const matrixd &y)
    {
        int cols = y.size();
        int rows = y[0].size();
    
        for (int i = 0; i < cols; ++i) {
            cout << "Token " << i << ": [";
            for (int j = 0; j < rows; ++j) {
                cout << fixed << setw(7) << setprecision(3) << y[i][j];
                if (j < 4) cout << ", ";
            }
            cout << " ]\n";
        }
    }
    
    void printVec(const vector<int>& v) 
    {
        for (int x : v) cout << x << " ";
        cout << endl;
    }
    
    matrixd addNnorm(const matrixd &A, const matrixd &B)
    {
         auto x = addMat(A, B);     // add two matrices
         int n = x.size();          // rows
         int m = x[0].size();       // cols
         matrixd y = x;             // output same size as x 
    
         // normalize the result
         double epsilon = 1e-5;
    
         for (int i = 0; i < n; i++) {
           double sum = 0;          // sum of one row
           double std = 0;          //sigma_i square
           for (int j = 0; j < m; j++)
             sum += x[i][j];
            double mean = sum / m;  // mu_i, mean of i-th row
    
            sum = 0;
            for (int j = 0; j < m; j++) {
              double diff = x[i][j] - mean;
              sum += diff * diff;
            }
            std = sum / m;
            for (int j = 0; j < m; j++) {
              double xd = (x[i][j] - mean) / sqrt(std + epsilon);
              y[i][j] = xd;         // gamma = 1, beta = 0;
            }
         }
    
         return y;  // final output of Add & norm
    }
    
    matrixd scaledAttention(const matrixd &Q, const matrixd &K, const matrixd &V,
            bool mask)
    {
      double d_k, scale;
      matrixd KT, A;	// K transpose, attention
      
      d_k = Q[0].size();
      scale = 1.0 / sqrt( d_k );
      KT = transpose ( K );
      A = matmul(Q, KT);
      scaleMat(A, scale); 
    
      if ( mask ) {
         for (int i = 0; i < A.size(); i++)
           for (int j = i+1; j < A[0].size(); j++) 
             A[i][j] = -1e9;	// causal mask
      }
    
      A = softmax( A );
      
      matrixd output = matmul(A, V);
    
      return output;
    }
    
    matrixd causalMask(matrixd S) 
    {
        int n = S.size();
        for (int i = 0; i < n; i++) {
            for (int j = i+1; j < n; j++) {
                S[i][j] = -1e9;  // effectively -∞
            }
        }
        return S;
    }
    
    void updateWeight(matrixd &W, const matrixd &dL_dW, const double eta)
    {
       for (int i = 0; i < W.size(); i++)
         for (int j = 0; j < W[0].size(); j++) {
    	 double grad = dL_dW[i][j];
    	 grad = max(-1.0, min(1.0, grad));	// clipping the gradient
             W[i][j] -= eta * grad;
          }
    }
    
    void clip(matrixd &A, double max_val = 5.0)
    {
        for (auto &row : A)
            for (auto &x : row)
                if (x > max_val) x = max_val;
                else if (x < -max_val) x = -max_val;
    }
    
    // softmax activation function, 1D input vector
    vector<double> softmax(const vector<double> &z)
    {
      int m = z.size();
      vector<double> y(m, 0);
    
       //maximum value of vector
      double maxVal = *max_element(z.begin(), z.end());
    
      double sum = 0;
      // Subtract max for numerical stability
      for (int j = 0; j < m; j++) {
        y[j] = exp(z[j] - maxVal);
        sum += y[j];
      }
    
      // Normalize
      for (int j = 0; j < m; j++)
        y[j] /= sum;
    
      return y;
    }
    
    // softmax activation function, 2D input vector
    matrixd softmax(const matrixd& input)
    {
      int n = input.size(), m = input[0].size();    // n x m matrix 
    
      // 2D output vector n x m y[n][m]
      vector<vector<double>> y(n, vector<double>(m));
    
      for (int i = 0; i < n; i++) {
        //maximum value of the row
        double maxVal = *max_element(input[i].begin(), input[i].end());
    
        double sum = 0;
        // Subtract max for numerical stability
        for (int j = 0; j < m; j++) {
          y[i][j] = exp(input[i][j] - maxVal);
          sum += y[i][j];
        }
    
        // Normalize
        for (int j = 0; j < input[i].size(); j++)
          y[i][j] /= sum;
      }
    
      return y;
    }
    
    // derivative function of softmax
    matrixd softmax_derivative(const matrixd &dL_dP, const matrixd &P)
    {
        int nt = P.size(), ns = P[0].size();
        matrixd dL_dS(nt, vector<double>(ns,0));
    
        for (int i = 0; i < nt; i++)
        {
              double dot = 0.0;
              for (int j = 0; j < ns; j++)
                dot += P[i][j] * dL_dP[i][j];  // dot product of two row-vectors
    
              for (int j = 0; j < ns; j++)  // dot used by all columns of dL/dA
                 dL_dS[i][j] = P[i][j] * (dL_dP[i][j] - dot);
        }
    
        return dL_dS;
    }
    
    // http://forejune.co/cuda/
    // transGpt.cpp : GPT-style (decoder-only) transformer classes 
    
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <unordered_map>
    #include <cmath>
    #include <random>
    #include <algorithm>
    #include <iomanip>
    #include "util.h"
    #include "transGpt.h"
    
    using namespace std;
    
    // ------------------------ Tokenizer class --------------------------
    // skip here, see earlier videos
    
    //------------------   Embedding Class ------------------
    Embedding::Embedding(int sequence_length, int embeddingDimension)
    { 
      nTokens = sequence_length;
      dim = embeddingDimension;
      initMatrix(em_matrix, nTokens, dim);	
    }
        
    // create embedding matrix with tokenIDs 
    matrixd Embedding::embed(const vector<int>& tokenIDs) 
    {
      matrixd embeddings;
      embeddings.reserve(tokenIDs.size()); // set capacity of embeddings vector
      for(int i = 0; i < tokenIDs.size(); i++) {
        if (tokenIDs[i] >= 0 && tokenIDs[i] < nTokens)
           embeddings.push_back(em_matrix[tokenIDs[i]]);
        else 
          // Out-of-dictionary tokens
          embeddings.push_back(vector<double>(dim, 0.0));
       }
        
       return embeddings;
    }
    
    int Embedding::get_embedDim() const 
    { 
           return dim; 
    }
    
    int Embedding::get_nTokens() const 
    { 
           return nTokens; 
    }
    
    void Embedding::backProp(const vector<int>& tokenIDs,
                             const matrixd& dL_dX,
                             double eta)
    {
        int nt = tokenIDs.size();
    
        for (int i = 0; i < nt; i++) {
            int token = tokenIDs[i];
    
            for (int j = 0; j < dim; j++){
    	   double grad = dL_dX[i][j];
    	   grad = max(-1.0, min(1.0, grad));      // clipping the gradient
               //em_matrix[token][j] -= eta * dL_dX[i][j];
               em_matrix[token][j] -= eta * grad;
    	 }
        }
    }
    
    // --------------------- Positional Encoding class ---------------
    PositionalEncoding::PositionalEncoding(int max_seq_length, int embed_dimension) 
    {
          maxTokens = max_seq_length;
          dModel = embed_dimension;
    
          PE.resize(maxTokens, vector<double>(dModel));
          for (int pos = 0; pos < maxTokens; pos++) {
        for (int i = 0; i < dModel/2; i++) {
        		PE[pos][2*i] = sin( pos / pow(10000, 2.0*i/dModel) );
    	    	PE[pos][2*i+1] = cos( pos / pow(10000, 2.0*i/dModel) );
    	}
          }
    }
    
    // add positional encodings to embeddings
    matrixd PositionalEncoding::addPE(const matrixd& embeddings) 
    {
        matrixd x = embeddings;
        int nTokens = embeddings.size();
    
        for (int pos = 0; pos < nTokens; pos++) 
          for (int i = 0; i < dModel; i++) 
             x[pos][i] += PE[pos][i];
    
         return x;
    }
    
    matrixd PositionalEncoding::getPE() 
    {
        return PE;
    }
    
    // -------------------- Add & Norm Class---------------------------------
    // constructor
    AddnNorm::AddnNorm(int dModel)
    {
        gamma.assign(dModel, 1.0);
        beta.assign(dModel, 0.0),
        eps = 1e-5;
    }
    
    matrixd AddnNorm::forward (const matrixd &A, const matrixd &B)
    {
        matrixd x = addMat(A, B); // add two matrices
        int n = x.size();         // rows
        int m = x[0].size();      // cols
        mean.resize( n );
        var.resize( n );
        Zhat.resize(n, vector<double>(m, 0));
    
        // normalize the result
        for (int i = 0; i < n; i++) {
           double sum = 0;      // sum of one row
           var[i] = 0;
           for (int j = 0; j < m; j++)
              sum += x[i][j];
           mean[i] = sum / m;  // mu_i, mean of i-th row
    
           sum = 0;
           for (int j = 0; j < m; j++) {
              double diff = x[i][j] - mean[i];
              sum += diff * diff;
           }
           var[i] = sum / m;
           for (int j = 0; j < m; j++) {
              double xd = (x[i][j] - mean[i]) / sqrt(var[i] + eps); //epsilon);
              Zhat[i][j] = gamma[j] * xd + beta[j];
           }
        }
    
        return Zhat;  // final output of Add & norm
    }
    
    // Assume forward already filled: Zhat, mean, var
    matrixd AddnNorm::backProp(const matrixd &X, const matrixd &dL_dY, double eta)
    {
        int nt = X.size();
        int m  = X[0].size();
    
        matrixd dL_dZ(nt, vector<double>(m, 0.0));
    
        vector<double> dL_dGamma(m, 0.0);	// gradient wrt gamma
        vector<double> dL_dBeta(m, 0.0);	// gradient wrt beta
    
        // 1. Compute dL_dGamma and dL_dBeta for each token
        for (int i = 0; i < nt; i++)
        {
            for (int j = 0; j < m; j++){
               dL_dBeta[j]  += dL_dY[i][j];
               dL_dGamma[j] += dL_dY[i][j] * Zhat[i][j];
            }
        }
    
    
        // 3. Backprop through LayerNorm (row-wise)
        for (int i = 0; i < nt; i++)
        {
            double inv_sigma = 1.0 / sqrt(var[i] + eps);
    
            // Step A: compute intermediate values
            vector<double> dL_dZhat(m);
            for (int j = 0; j < m; j++)
            {
                dL_dZhat[j] = dL_dY[i][j] * gamma[j];
            }
    
            double sum_dL_dZhat = 0.0;
            double sum_dL_dZhat_zhat = 0.0;
    
            for (int j = 0; j < m; j++)
            {
                sum_dL_dZhat += dL_dZhat[j];
                sum_dL_dZhat_zhat += dL_dZhat[j] * Zhat[i][j];
            }
    
            // Final gradient
            for (int j = 0; j < m; j++)
            {
                dL_dZ[i][j] = inv_sigma * (dL_dZhat[j] - sum_dL_dZhat / m
                     - Zhat[i][j] * sum_dL_dZhat_zhat / m);
            }
        }
    
        // 2. Update parameters
        for (int j = 0; j < m; j++)
        {
            gamma[j] -= eta * dL_dGamma[j];
            beta[j]  -= eta * dL_dBeta[j];
        }
    
        return dL_dZ;
    }
    
    // Optional setters (to plug in forward results)
    void AddnNorm::setCache(const matrixd& zhat, const vector<double>& m, 
    		                 const vector<double>& v)
    {
        Zhat = zhat;
        mean = m;
        var  = v;
    }
    
    void AddnNorm::saveGammaBeta(FILE *fp)
    {
      int dModel = gamma.size();
    
      for(int i = 0; i < dModel; i++) 
        fprintf(fp, "%9.4f, %9.4f", gamma[i], beta[i] );
    }
    
    void AddnNorm::readGammaBeta(FILE *fp)
    {
      int dModel = gamma.size();
    
      for(int i = 0; i < dModel; i++) 
        fscanf(fp, "%lf, %lf", &gamma[i], &beta[i] );
    }
    
    // ---------------------------  Feed Forward -----------------------------------
    FeedForward::FeedForward(int dModel0, int d_ff0)
    {
        dModel = dModel0;
        d_ff = d_ff0;
    
        W1.resize(dModel, vector<double>(d_ff));
        W2.resize(d_ff, vector<double>(dModel));
        initMatrix(W1, dModel, d_ff);
        initMatrix(W2, d_ff, dModel);
    }
    
    // ------------------------------
    // Forward (for cache)
    // ------------------------------
    matrixd FeedForward::FFoutput(const matrixd &X)
    {
        Z  = matmul(X, W1);   // pre-activation
        F1 = relu( Z );  //f(Z);        // F1 = f(XW1)
        matrixd F2 = matmul(F1, W2); // F2 = F1 W2
    
        return F2;
    }
    
    // ------------------------------
    // Backprop
    // ------------------------------
    matrixd FeedForward::backProp(const matrixd &X, const matrixd &dL_dY,  // dL/dF2
                 double eta)
    {
        // ---- W2 ----
        matrixd dL_dF2 = dL_dY;
    
        // dL/dW2 = F1^T * dL_dF2
        matrixd F1T = transpose(F1);
        matrixd dL_dW2 = matmul(F1T, dL_dF2);
    
        // dL_dF1 = dL_dF2 * W2^T
        matrixd W2T = transpose(W2);
        matrixd dL_dF1 = matmul(dL_dF2, W2T);
    
        // ---- find dL_dZ1 ----
        matrixd fd_Z  = relu_deriv(Z); // f_deriv(Z);
        int rows = dL_dF1.size();
        int cols = dL_dF1[0].size();
    
        matrixd dL_dZ1 = dL_dF1;	  // same dimension
        for (int i = 0; i < rows; i++)
           for (int j = 0; j < cols; j++)
              dL_dZ1[i][j] = dL_dF1[i][j] * fd_Z[i][j];
    
        // ---- W1 ----
        // dL/dW1 = X^T * dL/dZ1
        matrixd XT = transpose(X);
        matrixd dL_dW1 = matmul(XT, dL_dZ1);
    
        // dL/dX = dL/dZ1 * W1^T
        matrixd W1T = transpose(W1);
        matrixd dL_dX = matmul(dL_dZ1, W1T);
    
        // ---- Update weights ----
        updateWeight(W1, dL_dW1, eta);
        updateWeight(W2, dL_dW2, eta);
    
        return dL_dX;	// This becomes the dL_dY of next stage
    }
    
    void FeedForward::saveWeights(FILE *fp )
    {
       saveMatrix(W1, dModel, d_ff, fp);
       saveMatrix(W2, d_ff, dModel, fp);
    }
    
    void FeedForward::readWeights(FILE *fp )
    {
       readMatrix(W1, dModel, d_ff, fp);
       readMatrix(W2, d_ff, dModel, fp);
    }
    
    // ------------------ MultiHeadAttention --------------------
    MultiHeadAttention::MultiHeadAttention(int dModel0, int nHeads0)
    {
        dModel = dModel0;
        nHeads = nHeads0;
        d_k = dModel / nHeads;
    
        WQ.resize(nHeads);
        WK.resize(nHeads);
        WV.resize(nHeads);
    
        for (int h = 0; h < nHeads; h++) {
            initMatrix(WQ[h], dModel, d_k);
            initMatrix(WK[h], dModel, d_k);
            initMatrix(WV[h], dModel, d_k);
        }
    
        initMatrix(WO, dModel, dModel);
    }
    
    matrixd MultiHeadAttention::computeAttention(const matrixd &X_Q,
         const matrixd &X_K, const matrixd &X_V, bool mask)
    {
        Qs.clear(); Ks.clear(); Vs.clear();
        Ps.clear(); As.clear();
    
        vector<matrixd> heads;
    
        int nq = X_Q.size();
        int nk = X_K.size();
    
        for (int h = 0; h < nHeads; h++) {
            matrixd Qh = matmul(X_Q, WQ[h]);  // nq x d_k
            matrixd Kh = matmul(X_K, WK[h]);  // nk x d_k
            matrixd Vh = matmul(X_V, WV[h]);  // nk x d_k
    
            matrixd S = matmul(Qh, transpose(Kh));
            scaleMat(S, 1.0 / sqrt(d_k));
    
    
            if (mask)
              S = causalMask(S);
    
            matrixd Ph = softmax(S);  
            matrixd Ah = matmul(Ph, Vh);  // nq x d_k
    
            Qs.push_back(Qh);
            Ks.push_back(Kh);
            Vs.push_back(Vh);
            Ps.push_back(Ph);
            As.push_back(Ah);
    
            heads.push_back(Ah);
        }
    
        // Concatenate heads
        int n = heads[0].size();
        matrixd concat(n, vector<double>(dModel, 0.0));
    
        for (int h = 0; h < nHeads; h++) 
           for (int i = 0; i < n; i++) 
              for (int j = 0; j < d_k; j++) 
                 concat[i][h * d_k + j] = heads[h][i][j];
    
            // Final projection
        matrixd H = matmul(concat, WO);
    
        return H;
    }
    
    // backpropagation
    matrixd MultiHeadAttention::backProp( const matrixd &X_Q, const matrixd &X_K,
        const matrixd &X_V, const matrixd &dL_dH, double eta, bool mask)
    {
       if (Ps.empty() || As.empty()) {
          cout << "ERROR: MHA cache empty in backProp!" << endl;
          exit(1);
       }
    
        // ---- WO gradients ----
        matrixd A_cat; // rebuild concat from cached As
     //   int nt = As[0].size();
        int nt = X_Q.size();
        double s = 1.0 / sqrt(d_k);   // for scaling 
    
        A_cat = matrixd(nt, vector<double>(dModel, 0.0));
        for (int h = 0; h < nHeads; h++) 
           for (int i = 0; i < nt; i++) 
              for (int j = 0; j < d_k; j++) 
                 A_cat[i][h*d_k + j] = As[h][i][j];
    
        matrixd dL_dWO = matmul(transpose(A_cat), dL_dH);
    
        // Compute before updating WO
        matrixd dL_dAcat = matmul(dL_dH, transpose(WO));
     
        // ---- initialize accumulators ----
        matrixd dL_dXQ(X_Q.size(), vector<double>(dModel, 0.0));
        matrixd dL_dXK(X_K.size(), vector<double>(dModel, 0.0));
        matrixd dL_dXV(X_V.size(), vector<double>(dModel, 0.0));
    
        // ---- per head ----
        for (int h = 0; h < nHeads; h++) {
           // slice gradient
           matrixd dL_dA(nt, vector<double>(d_k));
           for (int i = 0; i < nt; i++)
              for (int j = 0; j < d_k; j++)
                 dL_dA[i][j] = dL_dAcat[i][h*d_k + j];
    
           matrixd P = Ps[h];
           matrixd V = Vs[h];
           matrixd Q = Qs[h];
           matrixd K = Ks[h];
    
           // ---- A = P V ----
           matrixd dL_dP = matmul(dL_dA, transpose(V));
           matrixd dL_dV = matmul(transpose(P), dL_dA);
    
           // ---- softmax ----
           matrixd dL_dS = softmax_derivative(dL_dP, P);
    
             // apply mask if used
            if (mask) {
                for (int i = 0; i < nt; i++)
                    for (int j = i+1; j < nt; j++)
                        dL_dS[i][j] = 0.0;
            }
    
    
           // ---- S = QK^T ----
           matrixd dL_dQ = matmul(dL_dS, K);
           matrixd dL_dK = matmul(transpose(dL_dS), Q);
    
           scaleMat(dL_dQ, s);
           scaleMat(dL_dK, s);
    
           // ---- Weight gradients ----
           matrixd dL_dWQ = matmul(transpose(X_Q), dL_dQ);
           matrixd dL_dWK = matmul(transpose(X_K), dL_dK);
           matrixd dL_dWV = matmul(transpose(X_V), dL_dV);
    
           // ---- input gradients ----
           matrixd dXQ = matmul(dL_dQ, transpose(WQ[h]));
           matrixd dXK = matmul(dL_dK, transpose(WK[h]));
           matrixd dXV = matmul(dL_dV, transpose(WV[h]));
    
           // accumulate
           dL_dXQ = addMat(dL_dXQ, dXQ);
           dL_dXK = addMat(dL_dXK, dXK);
           dL_dXV = addMat(dL_dXV, dXV);
    
            // =====================
            // Update weights after gradients
            // =====================
            updateWeight(WQ[h], dL_dWQ, eta);
            updateWeight(WK[h], dL_dWK, eta);
            updateWeight(WV[h], dL_dWV, eta);
    
        }  // for h
    
        updateWeight(WO, dL_dWO, eta);
    
        // For self-attention, these are the same
        return dL_dXQ;
    }
    
    void MultiHeadAttention::saveWeights(FILE *fp)
    {
      for (int h = 0; h < nHeads; h++) {
            saveMatrix(WQ[h], dModel, d_k, fp);
            saveMatrix(WK[h], dModel, d_k, fp);
            saveMatrix(WV[h], dModel, d_k, fp);
        }
    
        saveMatrix(WO, dModel, dModel, fp);
    }
    
    void MultiHeadAttention::readWeights(FILE *fp)
    {
      for (int h = 0; h < nHeads; h++) {
            readMatrix(WQ[h], dModel, d_k, fp);
            readMatrix(WK[h], dModel, d_k, fp);
            readMatrix(WV[h], dModel, d_k, fp);
        }
    
        readMatrix(WO, dModel, dModel, fp);
    }
    
    // ---------- OutputLayer ----------------------
    OutputLayer::OutputLayer(int dModel0, int vocab_size0)
    {
        dModel = dModel0;
        vocab_size = vocab_size0;
    
        initMatrix(W, dModel, vocab_size);
    }
    
    // Forward: logits → probabilities
    matrixd OutputLayer::forward(const matrixd &X) {
        // X: (seq_len x dModel)
        // output: (seq_len x vocab_size)
        matrixd logits = matmul(X, W);
    
        return softmax(logits);
    }
    
    // Backward: returns dL/dX
    matrixd OutputLayer::backward(const matrixd &X, const matrixd &P,
                 const vector<int> &targets, double eta)
    {
        int nt = P.size();
    
        // dL/dY = P - T (cross-entropy + softmax)
        matrixd dL_dY = P;
    
        for (int i = 0; i < nt; i++) 
            dL_dY[i][targets[i]] -= 1.0;
    
          // optional but recommended: normalize
        for (int i = 0; i < nt; i++)
            for (int j = 0; j < P[0].size(); j++)
                dL_dY[i][j] /= nt;
    
        // Gradient wrt weights
        matrixd dL_dW = matmul(transpose(X), dL_dY);
    
        // Gradient wrt input (back to decoder)
        matrixd dL_dX = matmul(dL_dY, transpose(W));
    
        //update weights after gradient computation
        updateWeight(W, dL_dW, eta);
    
        return dL_dX;
    }
    
    void OutputLayer::saveWeights(FILE *fp)
    {
       saveMatrix(W, dModel, vocab_size, fp);
    }
    
    void OutputLayer::readWeights(FILE *fp)
    {
       readMatrix(W, dModel, vocab_size, fp);
    }
    
    // ------------ GPT-style Transformer (Decoder-only) -------------------
    GPTBlock::GPTBlock(int dModel, int nHeads, int d_ff)   // Decoder
        : mha(dModel, nHeads), norm1(dModel), ffn(dModel, d_ff), norm2(dModel) 
    {
    }
    
    matrixd GPTBlock::forward(const matrixd &X) 
    {
        matrixd H = mha.computeAttention(X, X, X, true); // masked
        matrixd X1 = norm1.forward(X, H);
    
        matrixd F = ffn.FFoutput(X1);
        matrixd Y = norm2.forward(X1, F);
    
        return Y;
    }
    
    matrixd GPTBlock::backward(const matrixd &X, const matrixd &dL_dY, double eta) 
    {
    
        matrixd dF = norm2.backProp(X, dL_dY, eta);
        matrixd dX1 = ffn.backProp(X, dF, eta);
        matrixd dH = norm1.backProp(X, dX1, eta);
    
        return mha.backProp(X, X, X, dH, eta, true);
    }
    
    void GPTBlock::saveWeights(FILE *fp)
    {
      fprintf(fp, "#GPTBlock: Multihead Attention mha:\n");
      mha.saveWeights( fp );
      fprintf(fp, "\n");
      fprintf(fp, "#GPTBlock: AddnNorm norm1:\n");
      norm1.saveGammaBeta( fp );
      fprintf(fp, "\n");
      fprintf(fp, "#GPTBlock: FeedForward ffn:\n");
      ffn.saveWeights( fp );
      fprintf(fp, "\n");
      fprintf(fp, "#GPTBlock: AddnNorm norm2:\n");
      norm2.saveGammaBeta( fp );
      fprintf(fp, "\n");
    }
    
    void readComment(FILE *fp)
    {
      char buffer[256];
      do {
        if (fgets(buffer, sizeof(buffer), fp) == NULL) break;
      } while ( buffer[0] != '#' );
    }
    
    void GPTBlock::readWeights(FILE *fp)
    {
      char buffer[256];
      readComment( fp );
      mha.readWeights( fp );
      readComment( fp );
      norm1.readGammaBeta( fp );
      readComment( fp );
      ffn.readWeights( fp );
      readComment( fp );
      norm2.readGammaBeta( fp );
    }
    
    // ----------------------- MiniGPT -----------------------
    MiniGPT::MiniGPT(int vocab_size, int seq_len, int dModel, int nHeads, int d_ff, 
    	int nLayers) : embed(vocab_size, dModel), pe(seq_len, dModel),
          	output(dModel, vocab_size), seq_len(seq_len), vocab_size(vocab_size)
    {
        for (int i = 0; i < nLayers; i++) {
           GPTBlock gptBlock(dModel, nHeads, d_ff);
           layers.push_back(gptBlock);
        }
    }
    
    matrixd MiniGPT::forward(const vector<int> &tokens) 
    {
        matrixd X = embed.embed(tokens);
        X = pe.addPE(X);
    
        for(int i = 0; i < layers.size(); i++)
            X = layers[i].forward(X);
    
        return output.forward(X);
    }
    
    double MiniGPT::trainStep(
            const vector<int> &tokens,
            double eta)
    {
        // ---------------------------------
        // shifted sequences
        // ---------------------------------
    
        vector<int> input(tokens.begin(), tokens.end() - 1);
    
        vector<int> target(tokens.begin() + 1, tokens.end());
    
        // ---------------------------------
        // embedding
        // ---------------------------------
    
        matrixd X = embed.embed(input);
    
        X = pe.addPE(X);
    
        // ---------------------------------
        // transformer forward
        // ---------------------------------
    
        vector<matrixd> cache;
    
        for (int i = 0; i < layers.size(); i++)
        {
            cache.push_back(X);   // IMPORTANT, cache input
    
            X = layers[i].forward(X);
        }
    
        // ---------------------------------
        // output
        // ---------------------------------
    
        matrixd P = output.forward(X);
    
        double loss =
            crossEntropy(P, target);
    
        // ---------------------------------
        // output backward
        // ---------------------------------
    
        matrixd dL_dX = output.backward(X, P, target, eta);
    
        // ---------------------------------
        // transformer backward
        // ---------------------------------
    
        for (int i = layers.size()-1; i >= 0; i--)
        {
            dL_dX = layers[i].backward(cache[i], dL_dX, eta);
        }
    
        // ---------------------------------
        // embedding backward
        // ---------------------------------
    
        embed.backProp(input, dL_dX, eta);
    
        return loss;
    }
    
    int MiniGPT::saveWeights(char fname[])
    {
        FILE *fp;
    
      if ( (fp = fopen(fname, "wt")) == NULL ) {
        printf("\nError opening file %s\n", fname);
        return -1;
      }
    
      fprintf(fp, "# Ouptut Layer\n");
      output.saveWeights( fp );	// Output Layer
      for (int i = 0; i < layers.size(); i++)
        layers[i].saveWeights( fp ); // GPT blocks
      fclose( fp );
    
      return 1;
    }	
    
    int MiniGPT::readWeights(char fname[])
    {
        FILE *fp;
    
      if ( (fp = fopen(fname, "rt")) == NULL ) {
        printf("\nError opening file %s\n", fname);
        return -1;
      }
    
      readComment( fp );
      output.readWeights( fp );	// Output Layer
    
      for (int i = 0; i < layers.size(); i++)
        layers[i].readWeights( fp ); // GPT blocks
    
      fclose( fp );
    
      return 1;
    }	
    					 

    Testing Functions and Main

    // http://forejune.co/cuda/
    // testFunc.cpp : helper functions for testing
    
    #ifndef __TESTFUNC_H__
    #define __TESTFUNC_H__
    #include <fstream>
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <random>
    #include "util.h"
    #include "transGpt.h"
    
    using namespace std;
    
    struct Database {
        vector<vector<int>> sequences;
    };
    
    void addSample(Database &db, const vector<int> &seq);
    int buildDatabase(Database &db, char fname[]);
    
    vector<int> generate(MiniGPT &model, vector<int> prompt, int max_len);
    int predictMove(MiniGPT &model, const vector<int>& history,
        	const vector<Player>& board);
    void printBoard(const vector<Player> &board);
    int gameStatus(const vector<Player> &board, Player player);
    int getMove(vector<Player> &board);
    bool validMove(int move, const vector<Player> &board);
    
    #endif
    
    // http://forejune.co/cuda/
    // testFunc.cpp -- helper functions for testing
    
    #include "testFunc.h"
    
    using namespace std;
    
    void addSample(Database &db, const vector<int> &seq)
    {
        if (seq.size() < 2)
            return;
    
        db.sequences.push_back(seq);
    }
    
    int buildDatabase(Database &db, char fname[])
    {
      FILE *fp;
      
      if ( (fp = fopen(fname, "rt")) == NULL ) {
        printf("\nError opening file %s\n", fname);
        return -1;
      }
      int n = 0;
      while (n >= 0){
        fscanf(fp, "%d", &n);
        if (n < 0) break;
        vector<int> seq;
        int x;
        for (int i = 0; i < n; i++) {
          fscanf(fp, "%d", &x);
          seq.push_back( x );
        }
        addSample(db, seq);
      }
      if ( fp != NULL )
       fclose( fp );
    
      return 1;
    }
    
    vector<int> generate(MiniGPT &model, vector<int> prompt, int max_len)
    {
        vector<int> tokens = prompt;
    
        for (int step = 0; step < max_len; step++) {
    	vector<int> input(tokens.begin(), tokens.end());
            matrixd P = model.forward(input);
    
            vector<double> last = P.back();   //last row of matrix P
    
            int next = max_element(last.begin(), last.end()) - last.begin();
    
    	// stop if EOS
            if (next == EOS)
                break;
    
            tokens.push_back(next);
        }
    
        return tokens;
    }
    
    int predictMove(MiniGPT &model, const vector<int>& history,
        const vector<Player>& board)
    {
        cout << "Predicting a move!" << endl;
        matrixd P = model.forward(history);
    
        int last = P.size() - 1;
    
        vector<pair<double,int>> probs;
    
        for (int v = 2; v <= 10; v++)
            probs.push_back({P[last][v], v});
    
        sort(probs.rbegin(), probs.rend());
    
        for (auto &p : probs)
        {
            int move = p.second;
    
            if (board[move] == EMPTY)
                return move;
        }
    
        return -1;
    }
    
    void printBoard(const vector<Player> &board)
    {
      char b[9];
      for (int i = 0; i < 9; i++)
        if (board[i] == X)
          b[i] = 'X';
        else if (board[i] == O)
          b[i] = 'O';
        else
          b[i] = '.';
    
      cout << b[0] << " " << b[1] << " " << b[2] << endl;
      cout << b[3] << " " << b[4] << " " << b[5] << endl;
      cout << b[6] << " " << b[7] << " " << b[8] << endl;
    }
    
    int gameStatus(const vector<Player> &board, Player player)
    {
    	const int w[8][3] = {               //winning positions
            {0,1,2},{3,4,5},{6,7,8},        //rows
            {0,3,6},{1,4,7},{2,5,8},        //columns
            {0,4,8},{2,4,6}                 //diagonals
        };
        for (int i = 0; i < 8; i++){
          if (board[w[i][0]]==player && board[w[i][1]]==player
                          && board[w[i][2]]==player)
            return (int) player;
         }
    
         for (int k = 0; k < 9; k++)
           if (board[k] == EMPTY)
    	 return -1;	// game not over yet
    
        return (int) EMPTY;	// no more empty space
    }
    
    int getMove(vector<Player> &board)
    {
      cout << "Manual move" << endl;
      int n = 0;
      int moves[9];
      for (int i = 0; i < 9; i++){
        if (board[i] == EMPTY) 
          moves[n++] = i;
      }
    
      if ( n == 0 ) return -1; 		// no empty cell
      
      for (int i = 0; i < n; i++) {
         board[moves[i]] = O;	// check if need to block O
         if (gameStatus(board, O) == (int) O){
    	board[moves[i]] = EMPTY;	// restore board
    	return moves[i];		// move to block Player O
         }
      }
    
      return moves[0];	//return first available move
    }
    
    bool validMove(int move, const vector<Player> &board)
    {
      if (move < 0 || move > 8)
        return false;
    
      if (board[move] != EMPTY) 
        return false;
    
      return true;
    }
    
    // http://forejune.co/cuda/
    // testMain.cpp -- main routine for testing
    
    #include "testFunc.h"
    
    using namespace std;
    
    int main(int argc, char *argv[])
    {
       bool training = false;
       if ( argc > 1 && atoi(argv[1]) > 0 )
          training = true; 
    
       // Build  database 
       Database db;
       char fname[] = "data.txt";
    
       buildDatabase(db, fname);
    
       cout << "Number of training sequences: " << db.sequences.size() << endl;
    
       // Model
       int vocab_size = 20;
       int seq_len = 12;
       int dModel = 32;
       int nHeads = 4;
       int d_ff = 64;
       int nLayers = 2;
    
       MiniGPT model(vocab_size, seq_len, dModel, nHeads, d_ff, nLayers);
    
       // Extract dataset
       vector<vector<int>> dataset = db.sequences;
    
       // shuffle support
       random_device rd;
       mt19937 gen(rd());
    
       int nEpoches;
       if ( training ){
          // Training
          cout << "\n==== Training GPT Tic-Tac-Toe ====\n";
          nEpoches = 421;
       } else	// no training, get weights from file
          nEpoches = 0; 
    
       for (int epoch = 0; epoch < nEpoches; epoch++)
       {
          shuffle(dataset.begin(), dataset.end(), gen);
    
          double loss = 0.0;
    
          for (int i = 0; i < dataset.size(); i++) {
    	 vector<int>seq = dataset[i];
             loss += model.trainStep(seq, 0.003);
          }
    
          if (epoch % 20 == 0)
                cout << "Epoch " << epoch << " Loss: " << loss / dataset.size() << endl;
       }
       if ( training )
          model.saveWeights( (char *) "weights.txt" );
       else
          model.readWeights( (char *) "weights.txt" );
    
       // Inference
       cout << "\n==== GPT Inference ====\n";
    
       vector<int> prompt;
       prompt.push_back(SOS);	// Start of sequence
       prompt.push_back(4);	
       prompt.push_back(2);
       cout << "\nSource sequence:\n";
       for (int t : prompt)
            cout << t << " ";
       cout << endl;
    
       vector<int> generated = generate(model, prompt, 11);
       cout << "\nGenerated token sequence:\n";
       for (int t : generated)
            cout << t << " ";
    
       cout << endl;
    
       // playing an interactive game
       vector<Player>board;
       board.resize( 9 );
       for (int i = 0; i < 9; i++)
          board[i] = EMPTY;
    
       Player player;
       vector<int> history;
       int state;
       int k = 0;    // indexing a move in sequence
       history.push_back(SOS);
       k++;
       history.push_back(4);	// X at center
       k++;
       board[4] = X;
       printBoard(board);
       while ( true )
       {
          // user move
          int move;
          player = O;
          while ( true ) {	// prompt for a legal move
            cout << "Enter a legal move (0-8): ";
            cin >> move;
            if (!validMove(move, board)) continue;  //prompt until valid input
    
            break;
          }
          board[move] = player;
          state = gameStatus(board, player); 
          printBoard(board);
          if (state >= 0)	// game finished
              break;
    
          history.push_back(move);  // record user move
          k++;
          generated = generate(model, history, 11);  // generate new sequence
    
          int aiMove = generated[k]; // AI move is next, at k-th token
          if (!validMove(aiMove, board)) {  // If fail try max probability next move
             aiMove = predictMove(model, history, board);
             if (!validMove(aiMove, board))  // If still fail, use manual move
                aiMove = getMove(board);
          }
          if ( aiMove < 0 ) break;	      // no more move (should not happen)
          player = X;
          board[aiMove] = player;
          cout << "AI move: " << aiMove << endl;
          printBoard(board);
        
          state = gameStatus(board, player); 
          int token = aiMove;
          history.push_back(token);
          k++;
          printVec( history );
          if (state >= 0)	// game over
             break;
       }
    
       if ( (Player) state == X )
          cout << "AI won!";
       else if ( (Player) state == O )
          cout << "You won!";
       else if ( (Player) state == EMPTY )
          cout << "It's a draw!";
       cout << endl;
    
       return 0;
    }
    						

    Makefile:

    PROG	= testMain
    #source codes
    SRCS =  $(PROG).cpp
    #substitute .cpp by .o to obtain object filenames
    OBJS = $(SRCS:.cpp=.o) util.o  transGpt.o testFunc.o 
    
    #$< evaluates to the target's dependencies, 
    #$@ evaluates to the target
    
    $(PROG): $(OBJS)
    	g++ -o $@ $(OBJS)  
    
    $(OBJS): 
    	g++ -c  -std=c++20 $*.cpp 
    
    clean:
    	rm $(OBJS) $(PROG)
    	
    Graphics Interface

    /* http://forejune.co/cuda
     * testMaing.cpp : testing routine with graphics 
     */
    
    #include <GL/gl.h>
    #include <GL/glu.h>
    #include <GL/glut.h>
    #include <iostream>
    #include <fstream>
    #include <vector>
    #include "testFunc.h"
    
    using namespace std;
    
    vector<int>sequence;
    bool saved = false;
    
    Player ai = X;
    Player opp = O;
    Player board[9];
    bool playing = false;
    bool oppMove = false;
    vector<int>history;
    int state;
    const float d = 1;	//drawing distance between lines = 2*d
    int whoWon = -1;
    int seqIndex = 0;       //indexing a move in sequence
    
    void resetBoard()
    {
      for (int i = 0; i < 9; i++)
        board[i] = EMPTY;
    
      playing = false;
      oppMove = false;
      whoWon = -1;
      history.clear();
      saved = false;
    }
    
    void displayMessage(float x, float y, void* font, const string& str) 
    {
        glRasterPos2f(x, y);
    
        // Loop through each character of the string and draw it
        for (char const& c : str) 
            glutBitmapCharacter(font, c);
    }
    
    void printBoard()
    {
       float di = -d/2.0;		//align image center at center of square
       // positions to display X or O
       float cx[9] = {-2*d, 0, 2*d, -2*d, 0, 2*d, -2*d, 0, 2*d};
       float cy[9] = {2*d, 2*d, 2*d, 0, 0, 0, -2*d, -2*d, -2*d};
       for (int i = 0; i < 9; i++ ) {
           if (board[i] != EMPTY) {
    	  if (board[i] == X) {
    	     glColor3f(1, 0, 0);	//red color
                 displayMessage(cx[i]+di, cy[i]+di, GLUT_BITMAP_TIMES_ROMAN_24, "X");
    	  }else {
    	     glColor3f(0, 1, 0);	//use green color
                 displayMessage(cx[i]+di, cy[i]+di, GLUT_BITMAP_TIMES_ROMAN_24, "O");
    	  }
           } 
       } // for
     
       if ( whoWon >= 0 ) {
         string str[] = {"AI has won!", "You have won!", "It was a draw!"};
         displayMessage(-3*d, -5*d, GLUT_BITMAP_TIMES_ROMAN_24, str[whoWon]);
       } 
       glFlush();
    }
    
    int window;
    int screenWidth = 500, screenHeight = 500;
    MiniGPT *model;
    
    void init(void)
    {   
      glClearColor(1, 1, 1, 0);     //clear color buffer with white color
      glClear(GL_COLOR_BUFFER_BIT); //clear color buffer
      //define coordinate system
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      gluOrtho2D(-10, 10, -10, 10);
      glPointSize( 3 );
      glColor3f(0.0, 0.0, 0.0);             //draw with black color
    
      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
      cout << "p -- play game" << endl;
      cout << "r -- reset board" << endl;
    
      // initialize transformer model
       int vocab_size = 20;
       int seq_len = 12;
       int dModel = 32;
       int nHeads = 4;
       int d_ff = 64;
       int nLayers = 2;
    
       model = new MiniGPT(vocab_size, seq_len, dModel, nHeads, d_ff, nLayers);
    
       model->readWeights( (char *) "weights.txt" );
    }
    
    void line (float x0, float y0, float x1, float y1)
    {
      glBegin(GL_LINES);
        glVertex2f(x0, y0);
        glVertex2f(x1, y1);
      glEnd();
    }
    void display(void)
    {
       glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
       glLineWidth( 3 );
       glColor3f(0, 0, 0);
       line(-3*d, d, 3*d, d);		//upper horizontal line 
       line(-3*d, -d, 3*d, -d);		//lower horizontal line 
       line(-d, 3*d, -d, -3*d);		//left vertical line
       line(d, 3*d, d, -3*d);		//right vertical line
    
       glFlush();
    }
      
    bool isWinning(Player player)
    {
        const int w[8][3] = {               //winning positions
            {0,1,2},{3,4,5},{6,7,8},        //rows
            {0,3,6},{1,4,7},{2,5,8},        //columns
            {0,4,8},{2,4,6}                 //diagonals
        };
        for (int i = 0; i < 8; i++){
          if (board[w[i][0]]==player && board[w[i][1]]==player
                          && board[w[i][2]]==player)
            return true;
         }
    
        return false;
    }
    
    bool emptyCell()
    {
        //chekc if more empty space
        for (int i = 0; i < 9; i++)
           if (board[i] == EMPTY)
             return true;
    
        return false;   //no more empty space
    }
    
    int checkStatus()
    {
       if ( isWinning( ai ) ) 
            return 0;	
       else if ( isWinning( opp ) ) 
            return 1;	
       else if ( !emptyCell() ) 
            return 2;	
    
        return -1;
    }
    
    int aiMove()
    {
       whoWon = checkStatus();
       if (whoWon >= 0) return -1;       //game over
       vector<int> generated = generate(*model, history, 11);  // generate new sequence
       vector<Player> b(9);		
       for (int i = 0; i < 9; i++)
          b[i] = board[i];
    
       int aiMove = generated[seqIndex]; // AI move is next, at seqIndex token
       if (!validMove(aiMove,(vector<Player>) b)){  // If fail try max probability next move
           aiMove = predictMove(*model, history, b);
           if (!validMove(aiMove, b))    // If still fail, use manual move
              aiMove = getMove(b);
       }
    
       if ( aiMove < 0 ) return -1;         // no more move (should not happen)
    				       
     //  board[aiMove] = ai;
     //  history.push_back(aiMove);
     //  seqIndex++;
    
       return aiMove;
    }
    
    void play()
    {
      if ( playing )
        return;
      
      //ai goes first
      int move = 4;
      board[move] = ai;
      printBoard();
      seqIndex=0;
      history.push_back(SOS); 
      seqIndex++;
      history.push_back(move); 
      seqIndex++;
      oppMove = true;
    }
    
    void keyboard(unsigned char key, int x, int y)
    {
      switch(key) {
        case 27: /* escape */
            glutDestroyWindow(window);
            exit(0);
        case 'p':	//play game
    	if ( !playing ) {
    	  play();
    	  playing = true;
    	}
    	break;
        case 'r':	//reset board
    	resetBoard();
    	glutPostRedisplay();
    	break;
      }
    }
    
    int getLocation(float wx, float wy)
    {
      if (wx < -d) {
        if (wy > d)
     	return 0;
        else if (wy > -d)
    	return 3;
        else
    	return 6;
       }else if (wx < d) {
        if (wy > d)
     	return 1;
        else if (wy > -d)
    	return 4;
        else
    	return 7;
       }else {
        if (wy > d)
     	return 2;
        else if (wy > -d)
    	return 5;
        else
    	return 8;
       }
    }
    
    
    void mouse(int button, int state, int mx, int my)
    {
       if ( !playing || !oppMove )
         return;
    
       int x = mx, y = screenHeight - my;
       float wx = (float) x * 20.0/screenWidth - 10;	//world coordinates
       float wy = (float) y * 20.0/screenHeight - 10;	
       if ( button == GLUT_LEFT_BUTTON && state == GLUT_DOWN ){
          if (whoWon < 0 ) {	//game not done
            int loc = getLocation(wx, wy);
            if (board[loc] == EMPTY) {
    	  history.push_back(loc);
    	  seqIndex++;
    	  board[loc] = opp;
    	  printBoard();
    	  oppMove = false;
    	  loc = aiMove();	
    	  history.push_back(loc);
    	  seqIndex++;
    	  board[loc] = ai;
    	  whoWon = checkStatus();
    	  printBoard();
    	  oppMove = true;
            }
          }
       }
    }
    
    int graphics(int argc, char** argv)
    {
       glutInit(&argc, argv);
       glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB | GLUT_DEPTH);
       glutInitWindowSize(screenWidth, screenHeight);
       glutInitWindowPosition(100, 100);
       window = glutCreateWindow(argv[0]);
       init();
       glutDisplayFunc(display);
       glutKeyboardFunc(keyboard);
       glutMouseFunc( mouse );
       glutMainLoop();
       return 0; 
    }
    
    int main(int argc, char** argv)
    {
      graphics(argc, argv);
    
      return 0;
    }