ATLAS Offline Software
Loading...
Searching...
No Matches
TileNNEmulator.cxx
Go to the documentation of this file.
1/*
2 Copyright (C) 2002-2026 CERN for the benefit of the ATLAS collaboration
3*/
4
5#include "TileNNEmulator.h"
6#include <nlohmann/json.hpp>
7#include <cmath>
8#include <stdexcept>
9
10int64_t TileNNEmulator::rshiftRoundHalfEven(int64_t v, int k) {
11 if (k <= 0) return v;
12 int64_t q = v >> k;
13 int64_t r = v - (q << k);
14 int64_t half = int64_t(1) << (k - 1);
15 if (r > half || (r == half && (q & 1))) ++q;
16
17 return q;
18}
19
20int64_t TileNNEmulator::castTo(int64_t v, int fracFrom, const FixSpec& spec) {
21 int d = fracFrom - spec.frac();
22 if (d > 0) {
23 v = (spec.round == Round::RND_CONV) ? rshiftRoundHalfEven(v, d) : (v >> d);
24 } else if (d < 0) {
25 v <<= -d;
26 }
27
28 int64_t lim = int64_t(1) << (spec.w - 1);
29 switch (spec.sat) {
30 case Sat::SAT:
31 if (v < -lim) v = -lim;
32 else if (v >= lim) v = lim - 1;
33 break;
34 case Sat::SAT_SYM:
35 if (v <= -lim) v = -lim + 1;
36 else if (v >= lim) v = lim - 1;
37 break;
38 case Sat::WRAP:
39 v = ((v + lim) & ((int64_t(1) << spec.w) - 1)) - lim;
40 break;
41 }
42
43 return v;
44}
45
46float TileNNEmulator::sValue(float hg, float lg) const {
47 return static_cast<float>((hg + m_lgScale * lg) / m_denominator);
48}
49
50int64_t TileNNEmulator::run(const float* s) const {
51 std::vector<int64_t> x(m_nSamples);
52
53 for (int k = 0; k < m_nSamples; ++k) {
54 // float -> in_t: ldexp scales by 2^frac exactly, llrint rounds the real
55 // value half-to-even (FE_TONEAREST), i.e, AP_RND_CONV.
56 int64_t c = std::llrint(std::ldexp(static_cast<double>(s[k]), m_inT.frac()));
57 c = castTo(c, m_inT.frac(), m_inT);
58 x[k] = castTo(c, m_inT.frac(), m_castT);
59 }
60
61 int frac = m_castT.frac();
62 std::vector<int64_t> y;
63
64 for (const Layer& l : m_layers) {
65 if (l.op == Op::Dense) {
66 y.assign(l.nOut, 0);
67
68 for (int i = 0; i < l.nIn; ++i) {
69 for (int j = 0; j < l.nOut; ++j) {
70 int64_t p = x[i] * l.w[i * l.nOut + j];
71 y[j] += castTo(p, frac + l.weightT.frac(), l.accumT);
72 }
73 }
74
75 int up = l.biasT.frac() - l.accumT.frac();
76
77 for (int j = 0; j < l.nOut; ++j) {
78 y[j] = castTo((y[j] << up) + l.b[j], l.biasT.frac(), l.outT);
79 }
80 } else {
81 y.resize(l.nOut);
82
83 for (int j = 0; j < l.nOut; ++j) {
84 // Negative branch: multiplying by alpha = 2^-alphaShift is a
85 // reinterpretation of the code at alphaShift more fractional bits.
86 int from = (x[j] > 0) ? frac : frac + l.alphaShift;
87 y[j] = castTo(x[j], from, l.outT);
88 }
89 }
90
91 frac = l.outT.frac();
92 x.swap(y);
93 }
94
95 return x[0];
96}
97
98bool TileNNEmulator::load(const std::string& jsonText, std::string& error) {
99 using nlohmann::json;
100
101 auto fix = [](const json& j) {
102 FixSpec t;
103 t.w = j.at("w").get<int>();
104 t.i = j.at("i").get<int>();
105 std::string r = j.at("round").get<std::string>();
106 std::string s = j.at("sat").get<std::string>();
107 if (r == "RND_CONV") t.round = Round::RND_CONV;
108 else if (r == "TRN") t.round = Round::TRN;
109 else throw std::runtime_error("unknown rounding mode '" + r + "'");
110 if (s == "SAT") t.sat = Sat::SAT;
111 else if (s == "SAT_SYM") t.sat = Sat::SAT_SYM;
112 else if (s == "WRAP") t.sat = Sat::WRAP;
113 else throw std::runtime_error("unknown saturation mode '" + s + "'");
114
115 return t;
116 };
117
118 try {
119 json cfg = json::parse(jsonText);
120 if (cfg.at("format_version").get<int>() != 1) {
121 throw std::runtime_error("unsupported format_version");
122 }
123
124 const json& in = cfg.at("input");
125 m_nSamples = in.at("n_samples").get<int>();
126 m_lgScale = in.at("lg_scale").get<double>();
127 m_denominator = in.at("denominator").get<double>();
128 m_inT = fix(in.at("in_t"));
129 m_castT = fix(in.at("cast_t"));
130 m_ampScale = cfg.at("output").at("amplitude_scale").get<double>();
131 m_layers.clear();
132 int width = m_nSamples;
133
134 for (const json& jl : cfg.at("layers")) {
135 Layer l;
136 std::string op = jl.at("op").get<std::string>();
137 if (op == "dense") {
138 l.op = Op::Dense;
139 l.nIn = jl.at("n_in").get<int>();
140 l.nOut = jl.at("n_out").get<int>();
141 l.weightT = fix(jl.at("weight_t"));
142 l.accumT = fix(jl.at("accum_t"));
143 l.biasT = fix(jl.at("bias_t"));
144 l.outT = fix(jl.at("out_t"));
145 l.w = jl.at("weights").get<std::vector<int64_t>>();
146 l.b = jl.at("bias").get<std::vector<int64_t>>();
147 if (l.w.size() != size_t(l.nIn) * l.nOut || l.b.size() != size_t(l.nOut)) {
148 throw std::runtime_error("dense layer weight/bias size mismatch");
149 }
150
151 if (l.biasT.frac() < l.accumT.frac()) {
152 throw std::runtime_error("bias grid coarser than accumulator grid");
153 }
154 } else if (op == "leaky_relu") {
155 l.op = Op::LeakyRelu;
156 l.nIn = l.nOut = jl.at("n").get<int>();
157 l.alphaShift = jl.at("alpha_shift").get<int>();
158 l.outT = fix(jl.at("out_t"));
159 } else {
160 throw std::runtime_error("unknown op '" + op + "'");
161 }
162
163 if (l.nIn != width) {
164 throw std::runtime_error("layer input width " + std::to_string(l.nIn)
165 + " does not chain from " + std::to_string(width));
166 }
167
168 width = l.nOut;
169 m_layers.push_back(std::move(l));
170 }
171
172 if (m_layers.empty() || width != 1) {
173 throw std::runtime_error("network must end in a single output");
174 }
175 } catch (const std::exception& e) {
176 error = e.what();
177 m_layers.clear();
178
179 return false;
180 }
181
182 return true;
183}
nlohmann::json json
const double width
#define y
#define x
static int64_t castTo(int64_t v, int fracFrom, const FixSpec &spec)
float sValue(float hg, float lg) const
Combined dual-gain input sample S = (hg + lgScale*lg) / denominator.
static int64_t rshiftRoundHalfEven(int64_t v, int k)
std::vector< Layer > m_layers
int64_t run(const float *s) const
bool load(const std::string &jsonText, std::string &error)
int r
Definition globals.cxx:22