IISc Bangalore / DLFA capstone / 2024
A phone with work to do has two bad options.
Run the job on the device’s own CPU and watch the battery go: chip power grows with the cube of clock speed, so running twice as fast costs about eight times the power. Or push the job over the air to a server at the base station, and hope the radio holds. Neither option wins outright, and the right answer changes every millisecond.
This was our capstone for the DLFA programme at the Centre for Continuing Education, IISc Bangalore. Three phones, one base station, one power dial per device reset every millisecond, no central scheduler, and no device with any view of the other two. We built the simulator, then dropped two learners into it. Two years later I read the code again, found three defects, fixed them, and re-ran it properly. This page is what changed — and it is a better result than the one we submitted.
Mean reward per episode — device 1
rewardDDPGDQN
Device 1, both learners, on the corrected code: DQN finishes marginally ahead here, one of the two lighter devices where discrete power levels suffice. Reward is always negative; closer to zero is better. Mean of five seeds, 10-episode moving average; the original single-seed 2024 curve and the defects behind it are in section 06.
Show the numbers
| Episode | DDPG | DQN |
|---|---|---|
| 1 | -24.23 | -6.18 |
| 2 | -26.47 | -6.17 |
| 3 | -28.12 | -6.20 |
| 4 | -27.99 | -6.17 |
| 5 | -26.75 | -6.10 |
| 6 | -26.62 | -6.08 |
| 7 | -27.18 | -6.03 |
| 8 | -27.84 | -5.95 |
| 9 | -29.77 | -6.14 |
| 10 | -30.92 | -6.21 |
| 11 | -33.01 | -6.07 |
| 12 | -34.91 | -6.01 |
| 13 | -36.01 | -5.96 |
| 14 | -37.06 | -6.13 |
| 15 | -38.60 | -6.08 |
| 16 | -38.59 | -5.96 |
| 17 | -41.34 | -5.91 |
| 18 | -42.94 | -6.07 |
| 19 | -42.62 | -5.86 |
| 20 | -43.06 | -5.82 |
| 21 | -42.80 | -5.82 |
| 22 | -40.72 | -5.90 |
| 23 | -40.33 | -5.79 |
| 24 | -39.02 | -5.64 |
| 25 | -37.62 | -5.56 |
| 26 | -36.76 | -5.75 |
| 27 | -32.86 | -5.73 |
| 28 | -30.77 | -5.75 |
| 29 | -29.46 | -5.73 |
| 30 | -27.19 | -5.56 |
| 31 | -25.01 | -5.52 |
| 32 | -25.46 | -5.40 |
| 33 | -26.22 | -5.40 |
| 34 | -26.83 | -5.31 |
| 35 | -27.57 | -5.51 |
| 36 | -30.18 | -5.30 |
| 37 | -32.87 | -5.22 |
| 38 | -34.32 | -5.17 |
| 39 | -36.14 | -5.18 |
| 40 | -39.11 | -5.25 |
| 41 | -41.52 | -5.24 |
| 42 | -41.07 | -5.13 |
| 43 | -39.79 | -5.16 |
| 44 | -39.28 | -5.00 |
| 45 | -37.99 | -4.99 |
| 46 | -34.33 | -4.97 |
| 47 | -31.67 | -4.92 |
| 48 | -29.82 | -4.78 |
| 49 | -27.35 | -4.70 |
| 50 | -23.36 | -4.78 |
| 51 | -20.18 | -4.81 |
| 52 | -19.19 | -4.92 |
| 53 | -17.63 | -4.84 |
| 54 | -16.02 | -5.07 |
| 55 | -15.90 | -4.94 |
| 56 | -16.53 | -4.88 |
| 57 | -16.31 | -4.88 |
| 58 | -16.89 | -4.90 |
| 59 | -16.72 | -4.91 |
| 60 | -16.57 | -4.78 |
| 61 | -16.25 | -4.93 |
| 62 | -16.08 | -5.05 |
| 63 | -16.27 | -4.96 |
| 64 | -16.16 | -4.71 |
| 65 | -15.45 | -4.55 |
| 66 | -14.12 | -4.74 |
| 67 | -13.67 | -4.79 |
| 68 | -12.02 | -4.66 |
| 69 | -11.97 | -4.62 |
| 70 | -12.02 | -4.57 |
| 71 | -12.57 | -4.27 |
| 72 | -12.23 | -3.98 |
| 73 | -13.02 | -4.04 |
| 74 | -13.57 | -4.08 |
| 75 | -13.45 | -4.19 |
| 76 | -13.57 | -4.00 |
| 77 | -13.21 | -3.87 |
| 78 | -13.26 | -3.89 |
| 79 | -12.70 | -3.94 |
| 80 | -12.37 | -3.89 |
| 81 | -11.12 | -3.87 |
| 82 | -10.59 | -3.98 |
| 83 | -9.28 | -4.02 |
| 84 | -8.10 | -3.99 |
| 85 | -7.57 | -3.95 |
| 86 | -6.84 | -4.01 |
| 87 | -6.20 | -4.03 |
| 88 | -5.83 | -4.05 |
| 89 | -5.43 | -3.99 |
| 90 | -5.09 | -4.07 |
| 91 | -5.03 | -4.03 |
| 92 | -4.91 | -4.11 |
| 93 | -4.74 | -4.04 |
| 94 | -4.69 | -4.12 |
| 95 | -4.73 | -4.22 |
| 96 | -4.68 | -4.13 |
| 97 | -4.92 | -4.18 |
| 98 | -4.86 | -4.17 |
| 99 | -4.87 | -4.21 |
| 100 | -4.76 | -4.11 |
| 101 | -4.64 | -4.29 |
| 102 | -4.75 | -4.21 |
01Two bad options
Work arrives on a phone faster than the phone wants to deal with it. It has two ways out.
The first is to compute locally, which means clocking the CPU up. Silicon charges steeply for that. Dynamic power in a chip scales with the cube of clock frequency, so the relationship runs the other way when you read it as throughput: quadruple the power and you get about sixty percent more work done; to actually double the work you need eight times the power. The first watt is cheap. The eighth is nearly wasted. In the simulator this is the whole local path — power in, a cube root out, a small dent in the queue.
The second is to transmit, sending the work over the uplink to a server sitting at the base station. Here the penalty is logarithmic rather than cubic, which is Shannon’s law: doubling transmit power does not double the data rate, it adds a fixed amount to it, and how much it adds depends entirely on how good the link happens to be at that instant. On a good link a little power moves a lot of data. On a bad one you can push the dial to the ceiling and barely dent the queue.
So the trade is not between a good option and a bad one. It is between two different shapes of diminishing return, and which shape is currently cheaper depends on the radio channel — which nobody controls and which drifts continuously. The channel is modelled as a first-order autoregressive process with correlation 0.95: most of this millisecond’s value is inherited from the last one, with a fresh random nudge on top. You can never plan around it. You can only react to it, every slot, forever.
02What we simulated
One cell. Three mobile devices, one transmit antenna each, all sitting 100 units from a base station that has four receive antennas and hosts the edge server. Each device keeps a buffer of unprocessed work. Work arrives as a Poisson process — average one unit per slot for device one, two for device two, three for device three — and leaves the buffer two ways: bits computed locally, and bits transmitted to the server.
A slot is one millisecond, and one slot runs like this:
- Each device reads its current channel vector — the complex gain of its path to each of the four receive antennas, after path loss.
- Each device sets its two dials: transmit power and local CPU power, each anywhere in the range 0 to 2.
- The base station stacks the three channel vectors into a 4 × 3 matrix and takes the Moore–Penrose pseudo-inverse. That is zero-forcing: the receiver algebraically cancels the other users’ signals, leaving each device with an effective noise level set purely by channel geometry. The reciprocal of that noise is what the code calls SINR and feeds back to the device.
- Bits leave the buffer along both paths at once —
log2(1 + P_transmit · SINR)over the air, and a cube root of local power converted through a fixed cycles-per-bit constant. - A random Poisson batch of new work arrives.
- Whatever is left is backlog, and the channel advances one autoregressive step.
Three things we assumed away, all of them load-bearing.
The edge server is infinite. Unlimited CPU, unlimited storage, everything offloaded processed instantly and for free. The optimisation models the device and the uplink, not the server.
Zero-forcing is perfect. The function that computes SINR takes the users’ transmit powers as an argument and never reads it. In the simulator, one phone’s transmit power therefore never degrades another phone’s link. Combined with the infinite server, that means no device’s action enters any other device’s transition or reward at all. The three devices are coupled on paper and independent in the code. “Decentralised” here is an honest description of what each agent can see. It is not a claim that we tested how agents interfere.
Delay is a proxy. There is no Little’s-law computation and no delay variable anywhere in the codebase. What the report plots as “Average Delay” is mean buffer occupancy — backlog. A long queue means someone is waiting, which is the right direction, but it is not a measured latency and this page never calls it one.
03Turning it into a learning problem
None of the above is yet a learning problem. It becomes one when you stop trying to derive the right power split and instead let each device discover it by being scored.
The vocabulary is small and worth pinning down, because all of it is about to get used. An agent is the thing making decisions — here, one per device, and nothing more exotic than a small neural network. A policy is the mapping the agent is trying to learn: from what it can see to what it should do. A reward is a single number handed to the agent after every decision, saying how that moment went. An episode is one run of the system from a reset state to a stopping point; here, 99 consecutive slots. The agent does not get told what the right action was. It only ever gets told what the last action was worth.
What the agent seesis three numbers. How much work is queued right now. How good the link was, as the SINR the base station fed back. And the strength of its own radio path, the squared channel norm normalised by noise power. That is the entire observation — no other device’s queue, no other device’s power, no arrival-rate parameter, no clock.
One detail worth stating because it is a real property of the setup rather than a bug: the device acts on the previous slot’s SINR. It sets its dials from a state whose SINR belongs to the last slot, even though the channel_gain component beside it has already advanced to this one — a mismatched pair, not a blind one.
What the agent does is set two numbers, each between 0 and 2: transmit power and local CPU power. Both draw on the same battery.
What the agent is scored on is one number per slot, and it is always negative. This is a problem where you can only lose; the job is to lose less.
reward = − t_factor · (P_transmit + P_local) · 10 − (1 − t_factor) · backlogP_transmit + P_local is the total power drawn this slot, taken straight from the action. backlogis the queue length after this slot’s work has drained and this slot’s arrivals have landed. t_factor is a single trade-off knob between 0 and 1 that sets both weights at once, and it is 0.2 in every script that shipped.
That knob is the only place in the entire system where a human’s preference enters. Setting it to 0.2 gives the power term a weight of 0.2 and the backlog term a weight of 0.8 — we were saying that a unit of queue matters four times as much as a unit of power. That was the intent, and it is the intent the report describes.
The shipped arithmetic is not quite that. A hard-coded ×10 sits on the power term, undocumented and unexplained anywhere in the source or the seventeen design notes, so the coefficients the agent actually optimises against are 2.0 on power and 0.8 on backlog. Per unit, power carries the larger coefficient. Whether the tilt survived in practice depends on the ranges: backlog runs in the tens across most of training while total power is capped at 4, so my expectation is that the queue term still dominates the magnitude. That is an expectation, not a measurement. We never ran it the other way, and nothing in the results isolates the two terms.
Nothing in that expression tells an agent what to do. It never says “offload when the channel is good.” It says only that power costs you something and a queue costs you something. What it does predict is a cross-device pattern: a device that takes this reward seriously should spend power to keep its queue short, and the device with the heaviest arrival stream should spend the most. Section 05 has the power numbers, and section 06 explains why two thirds of them cannot be used to check that prediction.
| group | symbol | meaning | why the agent needs it |
|---|---|---|---|
| state | DataBuf | work queued at this device right now | the thing being punished |
| state | sinr | link quality after zero-forcing, previous slot | whether transmitting is currently cheap |
| state | channel_gain | squared channel norm over noise power | its own path strength, before the receiver’s algebra |
| action | Power[0] | transmit power, 0 – 2 | buys bits logarithmically, against the link |
| action | Power[1] | local CPU power, 0 – 2 | buys bits as a cube root, unconditionally |
| reward | r | −2.0 · ΣP − 0.8 · DataBuf | the whole objective, in one line |
Learning, concretely, is bookkeeping across an enormous number of these moments. After every slot the agent files a five-part record — the state it saw, the dials it set, the score it got, whether the episode ended, and the state it landed in — into a replay buffer, a store of past experience holding up to 250,000 records. Then it pulls 64 of them back out at random, old ones included, and adjusts its network on that batch.
What it adjusts toward is the part worth being precise about, because it is the part people usually get wrong when explaining this. The agent does not wait to see how the episode turned out and then compare. It keeps a running estimate of how bad the future looks from any given state. One slot later, holding one real score, it has a slightly better-informed estimate of the same quantity — and the gap between the old estimate and the new one is the entire learning signal. A guess corrected by one step of reality. That is why an agent can learn in the middle of an episode rather than at the end of it, and it is why both learners here need a second copy of their own network, which is the subject of the next section.
Each agent got 111 episodes of 99 slots: 10,989 decisions per device, about 33,000 training updates across the three. That is a small number for reinforcement learning, and it matters later.
04Two ways to learn the same thing
We ran two learners over the identical environment. They differ in one structural way, and everything else follows from it.
DQN cannot emit a real number. It learns a value function: for a given state, an estimate of the total future score obtainable by taking each available action. To use it, you compute all those estimates and take the largest. That requires the action list to be finite, so the two power dials were quantised onto five notches each — 0, 0.5, 1.0, 1.5, 2.0 — and the pair flattened into a single index over 25 joint actions. The network takes the three-number state and outputs 25 numbers.
DDPG does emit real numbers, by splitting the job across two networks. An actor takes the state and outputs a power pair directly, anywhere in the continuous box from 0 to 2. A critic takes the state andthat pair and scores it, exactly as DQN’s value function does but for one specific action rather than all of them. The actor is then trained by asking the critic which direction its score is increasing in, and stepping that way. Neither network ever enumerates an action space; the critic’s gradient is what stands in for the argmax.
Two pieces of machinery are shared, and both exist to fix a specific pathology.
The first is the replay buffer already described. Consecutive slots in this system are almost the same slot — the channel is 95% inherited, the queue moves by a fraction — so training on experience in the order it arrives means feeding a network a stream of near-copies. It overfits the last few seconds and forgets everything else. Sampling 64 records at random from a long history breaks that correlation.
The second is the target network, and this is the one to hold on to. The learning signal above compares the agent’s current estimate against its own estimate one step later. Both come from the same network. Improve the network and you move the thing you were measuring against, which is a feedback loop that can chase itself out of the sensible range entirely. The fix is to keep a second, slowly-updated copy of the network and compute the comparison against that. Here every target network trails its online counterpart by a soft update: after each training step, the target absorbs 0.1% of the online weights and keeps 99.9% of what it had. A moving target, moved slowly enough to be treated as fixed.
That is the mechanism this project got wrong, and the audit is mostly about it.
05What the corrected runs show
These are not the 2024 numbers. The original submission was a single run per algorithm, seeded from the wall clock, on code with two defects the next section describes. What follows is the fixed code, run five times per algorithm with controlled seeds; every curve is the mean across those seeds, and every table cell carries a bootstrap confidence interval in the repository. The 2024 figures, and the story of how they changed, are in the audit below.
The saved arrays are per-episode averages, 111 episodes by 3 users, and the numbers below are the mean of the last twenty episodes — the closest thing to an end-of-training figure the data supports. One caveat for the middle column: the saved power array is not the raw P_transmit + P_local the reward is computed from; it is that sum minus over_power, the refund the simulator credits back when the local CPU was set to drain more work than the buffer held.
| final 20 episodes | mean reward | mean power (net of over-power) | mean backlog |
|---|---|---|---|
| DDPG · device 1 | −4.83 | 0.70 | 3.70 |
| DDPG · device 2 | −10.79 | 1.80 | 6.47 |
| DDPG · device 3 | −12.38 | 2.86 | 5.69 |
| DQN · device 1 | −4.16 | 0.68 | 3.04 |
| DQN · device 2 | −7.03 | 1.35 | 5.01 |
| DQN · device 3 | −15.53 | 2.21 | 13.30 |
results/summary.md. All six rows are now valid: the target-network wiring that corrupted devices 2 and 3 is fixed, and the DQN explores on a schedule scaled to the training budget rather than at near-random throughout. Reward is higher-is-better; power and backlog are lower-is-better.The movement across training is the more telling half. Mean reward over the first twenty episodes against the last twenty: DDPG started at −28.6, −65.3 and −108.1 for the three users and ended at −4.8, −10.8 and −12.4. DQN started at −6.1, −9.9 and −29.7 and ended at −4.2, −7.0 and −15.5.
Both learners improve now — and that is the first thing the fix changed. DDPG begins far worse and climbs a long way, steepest on the heaviest user. DQN, which in the original run had barely moved from start to finish, now clearly learns too: on the busiest device it roughly halves its backlog cost over training. The 2024 report had read DQN’s flatness as stability from a small discrete action space. It was not stability. It was a comparator that never really trained — the audit has the reason.
The head-to-head is no longer a clean win for either side. On the two lighter devices DQN finishes ahead — five discrete power levels are enough when the queue is easy to hold down. On the heaviest device DDPG wins, and wins with a much tighter confidence interval: continuous control lets it spend power smoothly to keep the queue short, which is exactly what a reward that weights delay four times heavier than power asks for. So the advantage of continuous control is real but specific — it shows up on the hardest user, not as the blanket victory the original write-up claimed.
The two metrics behind the reward, for device 1
Reward is a weighted sum of power drawn and backlog carried, so the only way to see what an agent traded is to plot both. These are device 1 in each algorithm.
Mean total power per slot — device 1
power drawnDDPGDQN
The saved power array is the two dials summed, minus the over-provisioning refund the simulator credits back when the local CPU drains more than the buffer held. It is not the raw action sum the reward is computed from. Mean of five seeds, 10-episode moving average.
Show the numbers
| Episode | DDPG | DQN |
|---|---|---|
| 1 | 0.33 | 1.20 |
| 2 | 0.23 | 1.19 |
| 3 | 0.19 | 1.19 |
| 4 | 0.20 | 1.18 |
| 5 | 0.20 | 1.16 |
| 6 | 0.21 | 1.16 |
| 7 | 0.21 | 1.15 |
| 8 | 0.21 | 1.14 |
| 9 | 0.21 | 1.15 |
| 10 | 0.19 | 1.15 |
| 11 | 0.18 | 1.14 |
| 12 | 0.17 | 1.13 |
| 13 | 0.15 | 1.12 |
| 14 | 0.15 | 1.12 |
| 15 | 0.14 | 1.11 |
| 16 | 0.14 | 1.10 |
| 17 | 0.13 | 1.10 |
| 18 | 0.12 | 1.10 |
| 19 | 0.12 | 1.08 |
| 20 | 0.12 | 1.07 |
| 21 | 0.13 | 1.06 |
| 22 | 0.14 | 1.06 |
| 23 | 0.14 | 1.05 |
| 24 | 0.15 | 1.04 |
| 25 | 0.16 | 1.03 |
| 26 | 0.17 | 1.02 |
| 27 | 0.17 | 1.01 |
| 28 | 0.18 | 1.01 |
| 29 | 0.18 | 1.00 |
| 30 | 0.18 | 0.98 |
| 31 | 0.18 | 0.98 |
| 32 | 0.18 | 0.96 |
| 33 | 0.17 | 0.95 |
| 34 | 0.17 | 0.94 |
| 35 | 0.16 | 0.95 |
| 36 | 0.15 | 0.94 |
| 37 | 0.14 | 0.93 |
| 38 | 0.13 | 0.93 |
| 39 | 0.13 | 0.92 |
| 40 | 0.11 | 0.92 |
| 41 | 0.12 | 0.92 |
| 42 | 0.12 | 0.90 |
| 43 | 0.14 | 0.90 |
| 44 | 0.15 | 0.90 |
| 45 | 0.18 | 0.89 |
| 46 | 0.22 | 0.89 |
| 47 | 0.25 | 0.88 |
| 48 | 0.28 | 0.86 |
| 49 | 0.31 | 0.85 |
| 50 | 0.33 | 0.85 |
| 51 | 0.35 | 0.85 |
| 52 | 0.37 | 0.86 |
| 53 | 0.39 | 0.85 |
| 54 | 0.40 | 0.86 |
| 55 | 0.40 | 0.85 |
| 56 | 0.39 | 0.83 |
| 57 | 0.39 | 0.83 |
| 58 | 0.40 | 0.82 |
| 59 | 0.40 | 0.82 |
| 60 | 0.43 | 0.81 |
| 61 | 0.45 | 0.81 |
| 62 | 0.46 | 0.82 |
| 63 | 0.47 | 0.81 |
| 64 | 0.48 | 0.79 |
| 65 | 0.49 | 0.78 |
| 66 | 0.51 | 0.79 |
| 67 | 0.52 | 0.80 |
| 68 | 0.52 | 0.79 |
| 69 | 0.55 | 0.78 |
| 70 | 0.57 | 0.77 |
| 71 | 0.61 | 0.74 |
| 72 | 0.63 | 0.71 |
| 73 | 0.67 | 0.71 |
| 74 | 0.71 | 0.71 |
| 75 | 0.75 | 0.71 |
| 76 | 0.79 | 0.70 |
| 77 | 0.82 | 0.68 |
| 78 | 0.86 | 0.67 |
| 79 | 0.86 | 0.67 |
| 80 | 0.84 | 0.66 |
| 81 | 0.81 | 0.66 |
| 82 | 0.80 | 0.67 |
| 83 | 0.78 | 0.67 |
| 84 | 0.75 | 0.66 |
| 85 | 0.71 | 0.65 |
| 86 | 0.68 | 0.66 |
| 87 | 0.65 | 0.66 |
| 88 | 0.62 | 0.66 |
| 89 | 0.63 | 0.66 |
| 90 | 0.63 | 0.67 |
| 91 | 0.63 | 0.66 |
| 92 | 0.63 | 0.67 |
| 93 | 0.61 | 0.67 |
| 94 | 0.61 | 0.67 |
| 95 | 0.62 | 0.68 |
| 96 | 0.64 | 0.67 |
| 97 | 0.68 | 0.68 |
| 98 | 0.70 | 0.67 |
| 99 | 0.73 | 0.68 |
| 100 | 0.74 | 0.66 |
| 101 | 0.75 | 0.69 |
| 102 | 0.77 | 0.69 |
Mean buffer occupancy — device 1
backlogDDPGDQN
Buffer occupancy is the delay proxy the report plots as average delay. It is a queue length, not a measured latency. Mean of five seeds, 10-episode moving average.
Show the numbers
| Episode | DDPG | DQN |
|---|---|---|
| 1 | 29.15 | 2.77 |
| 2 | 32.38 | 2.82 |
| 3 | 34.65 | 2.89 |
| 4 | 34.46 | 2.89 |
| 5 | 32.90 | 2.85 |
| 6 | 32.72 | 2.87 |
| 7 | 33.41 | 2.87 |
| 8 | 34.24 | 2.83 |
| 9 | 36.66 | 3.09 |
| 10 | 38.12 | 3.19 |
| 11 | 40.78 | 3.04 |
| 12 | 43.18 | 2.99 |
| 13 | 44.58 | 2.96 |
| 14 | 45.90 | 3.20 |
| 15 | 47.85 | 3.18 |
| 16 | 47.84 | 3.06 |
| 17 | 51.30 | 3.02 |
| 18 | 53.34 | 3.23 |
| 19 | 52.95 | 3.05 |
| 20 | 53.50 | 3.04 |
| 21 | 53.16 | 3.11 |
| 22 | 50.54 | 3.25 |
| 23 | 50.03 | 3.16 |
| 24 | 48.40 | 3.02 |
| 25 | 46.62 | 2.97 |
| 26 | 45.52 | 3.25 |
| 27 | 40.61 | 3.29 |
| 28 | 37.96 | 3.34 |
| 29 | 36.30 | 3.31 |
| 30 | 33.45 | 3.15 |
| 31 | 30.74 | 3.10 |
| 32 | 31.29 | 3.00 |
| 33 | 32.25 | 3.07 |
| 34 | 33.02 | 3.00 |
| 35 | 33.95 | 3.25 |
| 36 | 37.25 | 3.01 |
| 37 | 40.65 | 2.91 |
| 38 | 42.49 | 2.87 |
| 39 | 44.80 | 2.92 |
| 40 | 48.54 | 3.04 |
| 41 | 51.55 | 3.08 |
| 42 | 50.92 | 2.95 |
| 43 | 49.23 | 2.99 |
| 44 | 48.48 | 2.85 |
| 45 | 46.71 | 2.87 |
| 46 | 41.92 | 2.89 |
| 47 | 38.38 | 2.88 |
| 48 | 35.85 | 2.76 |
| 49 | 32.61 | 2.70 |
| 50 | 27.47 | 2.82 |
| 51 | 23.34 | 2.89 |
| 52 | 21.99 | 3.07 |
| 53 | 19.99 | 3.00 |
| 54 | 17.95 | 3.29 |
| 55 | 17.83 | 3.17 |
| 56 | 18.66 | 3.13 |
| 57 | 18.44 | 3.16 |
| 58 | 19.22 | 3.22 |
| 59 | 19.02 | 3.24 |
| 60 | 18.72 | 3.14 |
| 61 | 18.26 | 3.34 |
| 62 | 17.99 | 3.45 |
| 63 | 18.16 | 3.39 |
| 64 | 17.91 | 3.13 |
| 65 | 16.91 | 2.97 |
| 66 | 15.15 | 3.22 |
| 67 | 14.50 | 3.28 |
| 68 | 12.42 | 3.16 |
| 69 | 12.29 | 3.15 |
| 70 | 12.35 | 3.10 |
| 71 | 12.98 | 2.81 |
| 72 | 12.56 | 2.55 |
| 73 | 13.48 | 2.64 |
| 74 | 14.13 | 2.72 |
| 75 | 13.95 | 2.86 |
| 76 | 14.07 | 2.67 |
| 77 | 13.57 | 2.58 |
| 78 | 13.55 | 2.64 |
| 79 | 12.85 | 2.72 |
| 80 | 12.48 | 2.71 |
| 81 | 10.99 | 2.70 |
| 82 | 10.29 | 2.83 |
| 83 | 8.68 | 2.90 |
| 84 | 7.29 | 2.89 |
| 85 | 6.71 | 2.85 |
| 86 | 5.88 | 2.93 |
| 87 | 5.18 | 2.95 |
| 88 | 4.78 | 2.96 |
| 89 | 4.30 | 2.90 |
| 90 | 3.88 | 2.96 |
| 91 | 3.87 | 2.93 |
| 92 | 3.84 | 3.02 |
| 93 | 3.78 | 2.92 |
| 94 | 3.78 | 3.00 |
| 95 | 3.87 | 3.12 |
| 96 | 3.82 | 3.01 |
| 97 | 4.05 | 3.06 |
| 98 | 3.96 | 3.06 |
| 99 | 3.92 | 3.09 |
| 100 | 3.75 | 3.00 |
| 101 | 3.54 | 3.15 |
| 102 | 3.55 | 3.05 |
The other two devices
In the original code these panels were meaningless — devices 2 and 3 trained against device 1’s network. With the wiring fixed they are now genuine, and they carry the finding: DQN holds its own on device 2, while device 3 is where DDPG separates.
Mean reward per episode — device 2
rewardDDPGDQN
Corrected: device 2 now trains against its own target network. Mean of five seeds, 10-episode moving average.
Show the numbers
| Episode | DDPG | DQN |
|---|---|---|
| 1 | -57.74 | -10.06 |
| 2 | -63.81 | -10.19 |
| 3 | -68.81 | -9.75 |
| 4 | -69.36 | -9.60 |
| 5 | -69.33 | -9.24 |
| 6 | -68.98 | -9.41 |
| 7 | -71.30 | -9.51 |
| 8 | -72.13 | -9.52 |
| 9 | -73.18 | -9.85 |
| 10 | -73.42 | -9.67 |
| 11 | -72.88 | -9.81 |
| 12 | -74.54 | -9.62 |
| 13 | -75.28 | -9.49 |
| 14 | -74.85 | -9.47 |
| 15 | -76.09 | -9.52 |
| 16 | -76.27 | -9.36 |
| 17 | -74.34 | -9.36 |
| 18 | -73.20 | -9.21 |
| 19 | -70.93 | -8.82 |
| 20 | -69.54 | -9.13 |
| 21 | -69.78 | -8.97 |
| 22 | -68.21 | -9.43 |
| 23 | -66.14 | -9.74 |
| 24 | -63.79 | -9.40 |
| 25 | -60.95 | -9.27 |
| 26 | -58.23 | -9.25 |
| 27 | -56.16 | -9.19 |
| 28 | -53.71 | -9.05 |
| 29 | -50.78 | -8.93 |
| 30 | -48.49 | -8.47 |
| 31 | -43.67 | -8.44 |
| 32 | -39.70 | -8.00 |
| 33 | -36.69 | -7.85 |
| 34 | -33.77 | -8.03 |
| 35 | -31.48 | -7.93 |
| 36 | -29.25 | -8.17 |
| 37 | -27.95 | -7.96 |
| 38 | -26.73 | -8.13 |
| 39 | -25.61 | -7.91 |
| 40 | -23.92 | -7.95 |
| 41 | -22.98 | -7.93 |
| 42 | -21.99 | -7.76 |
| 43 | -22.47 | -7.84 |
| 44 | -22.83 | -7.72 |
| 45 | -22.29 | -7.83 |
| 46 | -22.20 | -7.70 |
| 47 | -20.68 | -7.71 |
| 48 | -19.45 | -7.45 |
| 49 | -18.09 | -7.55 |
| 50 | -16.65 | -7.70 |
| 51 | -15.51 | -7.86 |
| 52 | -14.48 | -8.24 |
| 53 | -12.87 | -8.23 |
| 54 | -11.70 | -8.42 |
| 55 | -11.08 | -8.15 |
| 56 | -10.56 | -7.90 |
| 57 | -10.54 | -7.96 |
| 58 | -10.09 | -8.11 |
| 59 | -10.73 | -8.18 |
| 60 | -10.57 | -7.87 |
| 61 | -10.83 | -7.68 |
| 62 | -11.11 | -7.83 |
| 63 | -10.82 | -7.53 |
| 64 | -11.19 | -7.34 |
| 65 | -11.17 | -7.31 |
| 66 | -10.96 | -7.33 |
| 67 | -11.05 | -7.48 |
| 68 | -11.47 | -7.49 |
| 69 | -11.06 | -7.29 |
| 70 | -11.26 | -7.35 |
| 71 | -10.94 | -7.39 |
| 72 | -10.67 | -7.03 |
| 73 | -10.79 | -7.23 |
| 74 | -10.68 | -7.34 |
| 75 | -10.48 | -7.46 |
| 76 | -10.50 | -7.34 |
| 77 | -10.55 | -7.20 |
| 78 | -10.24 | -7.09 |
| 79 | -10.13 | -7.09 |
| 80 | -10.05 | -7.26 |
| 81 | -10.25 | -7.17 |
| 82 | -10.34 | -7.18 |
| 83 | -10.42 | -7.06 |
| 84 | -10.24 | -6.96 |
| 85 | -10.17 | -6.84 |
| 86 | -10.29 | -6.87 |
| 87 | -10.04 | -6.78 |
| 88 | -10.03 | -6.61 |
| 89 | -10.00 | -6.64 |
| 90 | -9.96 | -6.49 |
| 91 | -9.80 | -6.47 |
| 92 | -9.77 | -6.37 |
| 93 | -9.70 | -6.58 |
| 94 | -9.59 | -6.52 |
| 95 | -9.88 | -6.53 |
| 96 | -9.77 | -6.69 |
| 97 | -10.24 | -6.69 |
| 98 | -10.72 | -7.00 |
| 99 | -11.29 | -7.05 |
| 100 | -11.79 | -7.41 |
| 101 | -11.89 | -7.66 |
| 102 | -11.82 | -7.69 |
Mean reward per episode — device 3
rewardDDPGDQN
The heaviest arrival stream, and where continuous control pays off: DDPG ends ahead of DQN and with less seed-to-seed spread. Mean of five seeds, 10-episode moving average.
Show the numbers
| Episode | DDPG | DQN |
|---|---|---|
| 1 | -105.9 | -28.05 |
| 2 | -113.5 | -26.22 |
| 3 | -117.0 | -25.51 |
| 4 | -115.5 | -26.49 |
| 5 | -114.5 | -27.61 |
| 6 | -112.4 | -27.06 |
| 7 | -112.3 | -28.77 |
| 8 | -112.6 | -27.93 |
| 9 | -111.9 | -29.46 |
| 10 | -111.1 | -30.56 |
| 11 | -110.3 | -31.28 |
| 12 | -113.9 | -31.73 |
| 13 | -116.0 | -32.74 |
| 14 | -117.3 | -32.28 |
| 15 | -119.3 | -32.03 |
| 16 | -120.0 | -33.25 |
| 17 | -118.6 | -31.21 |
| 18 | -118.6 | -30.96 |
| 19 | -115.7 | -29.36 |
| 20 | -111.6 | -27.87 |
| 21 | -106.4 | -28.49 |
| 22 | -98.98 | -28.77 |
| 23 | -91.70 | -27.99 |
| 24 | -83.42 | -27.58 |
| 25 | -73.43 | -27.96 |
| 26 | -66.47 | -27.72 |
| 27 | -59.34 | -28.47 |
| 28 | -48.91 | -28.48 |
| 29 | -41.12 | -27.38 |
| 30 | -35.28 | -28.34 |
| 31 | -31.13 | -26.69 |
| 32 | -26.52 | -25.22 |
| 33 | -22.57 | -24.32 |
| 34 | -19.98 | -24.34 |
| 35 | -17.94 | -23.30 |
| 36 | -15.22 | -22.26 |
| 37 | -14.01 | -22.05 |
| 38 | -14.80 | -21.66 |
| 39 | -15.44 | -23.03 |
| 40 | -16.12 | -23.28 |
| 41 | -16.13 | -22.70 |
| 42 | -16.46 | -22.56 |
| 43 | -16.25 | -22.37 |
| 44 | -16.68 | -21.38 |
| 45 | -17.73 | -21.55 |
| 46 | -18.66 | -21.14 |
| 47 | -18.96 | -20.77 |
| 48 | -19.32 | -20.07 |
| 49 | -19.46 | -18.43 |
| 50 | -19.61 | -16.82 |
| 51 | -20.02 | -17.06 |
| 52 | -20.69 | -17.36 |
| 53 | -20.58 | -17.54 |
| 54 | -19.68 | -17.29 |
| 55 | -19.04 | -16.30 |
| 56 | -18.92 | -16.14 |
| 57 | -19.38 | -15.12 |
| 58 | -18.33 | -14.97 |
| 59 | -17.56 | -15.63 |
| 60 | -16.67 | -16.05 |
| 61 | -16.18 | -16.23 |
| 62 | -15.18 | -16.17 |
| 63 | -14.91 | -15.94 |
| 64 | -14.90 | -16.15 |
| 65 | -14.57 | -16.04 |
| 66 | -13.57 | -15.90 |
| 67 | -12.82 | -17.68 |
| 68 | -12.65 | -17.42 |
| 69 | -12.61 | -16.49 |
| 70 | -12.54 | -15.76 |
| 71 | -12.32 | -15.51 |
| 72 | -12.21 | -16.11 |
| 73 | -12.06 | -20.16 |
| 74 | -12.03 | -22.35 |
| 75 | -11.91 | -22.86 |
| 76 | -11.94 | -22.89 |
| 77 | -11.88 | -21.20 |
| 78 | -11.93 | -22.25 |
| 79 | -11.94 | -22.15 |
| 80 | -12.03 | -22.81 |
| 81 | -12.27 | -25.48 |
| 82 | -12.65 | -25.86 |
| 83 | -12.80 | -21.86 |
| 84 | -12.76 | -19.63 |
| 85 | -12.57 | -19.41 |
| 86 | -12.59 | -19.10 |
| 87 | -12.61 | -19.16 |
| 88 | -12.66 | -19.14 |
| 89 | -12.70 | -18.89 |
| 90 | -12.76 | -20.46 |
| 91 | -12.47 | -17.98 |
| 92 | -12.36 | -16.52 |
| 93 | -12.47 | -16.58 |
| 94 | -12.59 | -16.55 |
| 95 | -12.63 | -17.41 |
| 96 | -12.71 | -17.32 |
| 97 | -12.68 | -18.14 |
| 98 | -12.70 | -17.59 |
| 99 | -12.55 | -17.52 |
| 100 | -12.54 | -15.08 |
| 101 | -12.60 | -14.75 |
| 102 | -12.41 | -14.53 |
06The audit — and the fix
I went back to this code two years after submission to write it up properly, and read it harder than I had read it while we were building it. Three defects came out of that. I confirmed the first two by running the code rather than by reasoning about it, because they are exactly the kind of thing that reads as fine on the page. The third is visible in two lines of source.
This section diagnoses the original 2024 code. All three defects have since been fixed — the corrected results in the previous section are what the fixes produced, and a regression test now guards the one that mattered most. Each defect below ends with what the fix was. The original submission is preserved verbatim under a git tag, so the before and after are both reproducible.
The target networks are cross-wired between agents
Every user gets its own agent, its own networks, its own target networks and its own replay buffer. They share only the TensorFlow session and the environment object. That was the design and it is what the code appears to describe.
It is not what the code does. The DDPG critic slices TensorFlow’s global list of trainable variables using a per-agent variable countas if it were an absolute index into that list. For the first agent constructed into a fresh graph, the count and the index coincide and everything is correct. For the second and third agent they do not, and the slice lands back at the top of the list — on user 1’s weights. The DQN Q-network is blunter still — it takes the whole global list with no offset, so each successive agent’s parameter set swallows every agent built before it (measured lengths 6, 18, 30 across the three). The shapes match where it matters, so nothing raises, nothing warns, and the training loop runs to completion.
Two probes, both on the DQN:
Printing the variable each target network actually reads from returns the same tensor for all three agents: user 1’s first fully-connected weight matrix.
Then the behavioural version, which is the one that settles it. Stamp the three online networks with the constants 1, 2 and 3. Zero all three target networks. Run exactly one soft update at τ = 0.001. Correct wiring gives 0.001, 0.002, 0.003 — each target absorbing a thousandth of its own online network.
expected 0.001 0.002 0.003
measured 0.001 0.001 0.001Users 2 and 3 spent the entire run being pulled toward user 1’s network.
The scope is worth being precise about, because it is not uniform. The DDPG actoris wired correctly — it captures its variables as a delta around its own construction rather than as an absolute offset, which is the pattern the other two classes should have copied. So the actor is clean. But the actor’s only learning signal is the gradient the critic hands it, and the critic is broken for users 2 and 3. Corrupt input to a correct mechanism is still corrupt output. DDPG users 2 and 3 are compromised end to end.
User 1 was correctly wired in both algorithms — in the original code, the single reason any curve was trustworthy. The fix: capture each network’s variables as a delta around its own construction (the pattern the actor already used) instead of an absolute offset. The same behavioural probe now returns 0.001, 0.002, 0.003, and a regression test asserts it on every build so the bug cannot return silently. It turned out to be inherited verbatim from the paper authors’ own released code, not something we introduced.
The exploration rate never decayed
DQN explores with ε-greedy: with probability ε take a uniformly random action, otherwise take the one the network currently rates highest. ε starts at 1.0 — pure random — and decays toward zero as the agent learns, so the run shifts gradually from gathering experience to using it.
The decay factor is 0.999976 per decision. That is a schedule sized for the hard-coded step budget of 100,000 that sits next to it in the file. Our run is 111 episodes of 99 steps: 10,989 decisions. Over 10,989 decisions, ε ends at 0.768.
Stacked on top of that is a warm-up constant of 5,000 steps, during which actions are forced uniformly random regardless of what the network thinks. That is 45.5% of the entire run before the policy gets a vote at all, and ε is still 0.887 when it ends.
Put together: roughly nine percent of the DQN’s actions across the whole run came from its policy. The rest were coin flips. The replay log agrees. It records all three agents’ transitions — 32,967 lines — and across them 24 of the 25 joint actions appear between 1,118 and 1,320 times each, against a uniform expectation of 1,319. The one exception is action 0, no power on either dial, which appears 4,205 times, and whose share of the agents’ choices climbs steadily through training with the jump landing exactly at the 5,000-step warm-up boundary. That climb is the only visible policy in the whole run.
This is also the reading to apply to DQN’s power column. Uniform random over the 25-action grid draws 2.0 on average, so device 1’s plotted 1.100 is not a learned frugality: it sits below the random draw mainly because the saved array subtracts the over-provisioning refund, which is large when the buffer is near empty.
This is what makes the 2024 reading of the flat DQN curve wrong. The report credited the flatness to fast convergence in a small discrete action space — the agent finding a good policy early and holding it. The available explanation is simpler and does not require the agent to have learned anything: a policy that never stops exploring never changes. You cannot distinguish “converged” from “still random” by looking at a flat line, and here the arithmetic on the ε schedule tells you which one it was.
It also means DQN was never a tuned baseline. It was a mostly-random controller with a small learned tilt toward switching both dials off, and DDPG was compared against that.
The fix: replace the fixed decay constant with a linear schedule scaled to the actual training budget — anneal from 1.0 to a 0.05 floor over the first 80% of the run, after a short warm-up. ε now reaches its floor around episode 90 instead of ending at 0.77, and the corrected DQN in the previous section is the result: a comparator that actually trains. Like the wiring bug, the original schedule was inherited from upstream, where it was correct for a run 36× longer than ours.
The two evaluation runs did not start from the same place
The environment’s reset takes an isTrain flag. With it false, buffers and SINR start at zero; with it true, the starting buffer is randomised. The DDPG evaluation driver passes false. The DQN evaluation driver calls reset with no argument at all, and the default is true.
So the two evaluation runs began from different buffer distributions. It is a one-word difference between two files that were plainly written by copying one from the other, and it is the smallest of the three defects. Alongside it, the DQN evaluation driver constructs a fresh agent with its exploration rate back at its starting value, which means the DQN “evaluation” re-runs a near-random agent rather than measuring a policy.
It did not corrupt anything on this page, because neither driver produced anything on this page: both shipped result files are training runs. That is the larger point. The project has no evaluation protocol at all — the two drivers disagree with each other, and the quantity they exist to measure was never measured.
The fix: both evaluation drivers now reset the same way and load the corrected, seeded checkpoints, so a greedy evaluation is finally possible — though the numbers on this page are still the training curves, which is what the multi-seed campaign produced.
And there is nothing to compare against
Not a defect so much as an absence, but it constrains the conclusions harder than any of the three. The repository contains no baselines. No uniform-random controller, no fixed-power controller, no greedy all-local or all-offload policy, no per-slot analytic optimum. So there is no evidence anywhere in this project that either learner beats a device that simply transmits at 1.0 every slot and never thinks about it.
Two dead helper functions invert the offload path — power required to send a given number of bits, once from the channel norm and once from SINR. Nothing calls either. The matching local-path inverse is live, used for the over-provisioning refund. They are the residue of a greedy baseline that was removed; the environment still carries a comment describing itself as specially designed for greedy agent training.
None of these three are exotic. Two of them are a variable-scoping mistake and an arithmetic mismatch between a decay constant and a loop bound. They survived because nothing in the project ever checked them: no test asserted that two agents held different weights, and nobody divided 100,000 by 10,989.
07What survives
Being precise about this matters more than being generous in either direction.
The simulator stands. The physics is implemented as documented: Shannon capacity for the uplink, a cube-root frequency-to-power model for local compute, AR(1) Rayleigh fading with a path-loss term, and a genuine pseudo-inverse zero-forcing receiver over the 4 × 3 channel matrix. The defects are all in the learning code. Nothing about them touches the environment. Its limitations are the ones declared above — infinite server, perfect zero-forcing, backlog as a delay proxy — plus one more: the buffer-overflow penalty is commented out in every terminal class, so the nominal capacity of 100 is never enforced and the saved overflow array is identically zero in both runs.
All three users are now correctly wired, in both algorithms, and the run is repeated over five seeds — so the three-user comparison, which the original code could not support, is now a real measurement with confidence intervals.
Both learners train. DDPG improves steeply from a poor start; DQN, which in the original run looked flat, now clearly learns once its exploration is scaled to the budget. On the corrected numbers, neither dominates: DQN finishes ahead on the two lighter devices, and DDPG wins on the heaviest — and with a tighter confidence interval, which is its own result about stability under load.
What does not survive:
The headline.The 2024 write-up’s claim that DDPG “significantly outperforms” DQN was an artifact of a crippled comparator. Once DQN explores properly and every agent trains against its own network, the blanket win becomes a workload-dependent split. Continuous control helps — specifically on the hardest user — but it is not the across-the-board victory the report described.
The mechanism the report credited.The claim that DQN’s original flatness demonstrated fast convergence in a discrete action space is not supported. The ε arithmetic supplied a simpler explanation that required no learning at all — and once the schedule is fixed, the flatness is gone.
Any absolute claim of competence, for either learner. The repository still has no baseline — no random, fixed-power or greedy controller — so “DQN beats DDPG on device 1” is a statement about the two learners relative to each other, not evidence that either beats a device that simply transmits at 1.0 every slot and never thinks about it. Adding those baselines is the next real step.
And one thing we could never have claimed either way: what the policy actually did.The split between bits offloaded and bits computed locally is calculated on every single slot and written to disk on none of them. No array, log or figure anywhere in the project records the offload fraction against link quality. So the most natural sentence to write about a system like this — that the agent learned to transmit when the channel was good and fall back on the CPU when it wasn’t — is one I have no evidence for. It may well be true. Nothing here shows it.
08What I would do differently
Roughly in order of how much each would change the conclusions. The first three are done — they are what produced the corrected results above; the rest are the honest next steps.
Done — a test that would have caught the wiring. Construct three agents in one graph, assert that no two of them share a target variable, and assert the behavioural version: stamp the online networks with distinct constants, zero the targets, run one soft update, check you get three distinct numbers. That is about fifteen lines and it fails loudly on the shipped code. The general lesson is narrower than “write tests” — silent shape-compatible aliasing between supposedly independent models is a failure mode with no symptom, so it needs a test that asserts independence directly rather than a test that checks the loss goes down.
Done — size the exploration schedule to the run you are actually doing. A decay constant is meaningless without the step count next to it. And the broader version of that mistake is the one worth naming: the comparator has to be tuned as hard as the method you are promoting, or the comparison measures your attention rather than the algorithms.
Done — fixed, recorded seeds with confidence bands on every curve. The original seed was derived from the wall clock, so not one figure could be regenerated. It now runs five fixed seeds per algorithm with bootstrap confidence intervals; a single run of 111 episodes takes about four and a half minutes, so there was never a resource reason for n=1. (Five, not the ten first planned — enough to separate the curves that separate; more would tighten the bands.)
Real baselines, built before the learners. Uniform random, all-local, all-offload, and the per-slot convex optimum by one-dimensional bisection — the last of which is computable here, since the reward is well behaved in each power dial. Without a floor and a ceiling, a learning curve is a shape with no scale.
Log the offload split.Both quantities already exist in the step function. Adding them to the saved arrays is a one-line change and it is the difference between “reward improved” and a policy you can actually look at and argue with.
Initialise the target networks with a hard copy. The function named init_target_network runs the same 0.1% soft blend as every other step, so the targets begin at their own random initialisation and absorb a thousandth of the online weights on the first call. It is a smaller error than the cross-wiring, but it is the same class of error: a name that describes an intent the body does not implement.
TD3 and SAC, not more DDPG.DDPG’s characteristic failure is that the critic overestimates action values and the actor then optimises straight into the overestimate. TD3 addresses that with two critics and a pessimistic minimum over them, by updating the actor less often than the critics, and by smoothing the target action with noise so the critic cannot exploit a single sharp peak. SAC changes the objective instead: it adds the policy’s own entropy to what is being maximised, so exploration is something the agent learns to modulate rather than something injected from outside at a fixed rate — which is precisely the knob this project got wrong on the DQN side and never scheduled at all on the DDPG side, where the exploration noise ran at constant amplitude for the entire run.
Not MADDPG, and not QMIX.These are the obvious names to reach for on a page with the words “multi-agent” and “decentralised” on it, and they would do nothing here. Both exist to handle non-stationarity caused by other agents’ policies changing — agent A’s environment shifting because agent B learned something. That effect does not exist in this simulator. The SINR function ignores the transmit powers, and the server is infinite, so no device’s action appears anywhere in another device’s transition or reward. What we have is three independent decision processes whose observations happen to be statistically correlated through a shared channel matrix. Centralised training with decentralised execution has nothing to centralise. If I wanted the multi-agent problem to be real, the first change would be to the environment, not the algorithm: an MMSE receiver instead of perfect zero-forcing, so one device’s power genuinely raises another’s noise floor, and a finite server so offloaded work contends for something. Then the coupling exists and modelling it means something.
09Code, and the people
The full technical README documents the environment, both algorithms, every constant, the configuration reference, and a longer version of the defect list than this page has room for. It is the manual; this page is the argument.
The system was built from the setup in Chen & Wang, Decentralized computation offloading for multi-user mobile edge computing: a deep reinforcement learning approach, EURASIP Journal on Wireless Communications and Networking, 2020, article 188 — same environment model, same reward structure, same DDPG-versus-DQN framing. We never compared our numbers against theirs.
Team 5, DLFA programme, Centre for Continuing Education, IISc Bangalore, November 2024: Anaz Jaleel, Raghu Koratagere, Santosh Bagali, Shivajith Mutteal. The simulator and both learners were built by the four of us. The audit in the last three sections is mine and postdates the submission by two years.