# GPU Regime Stratification + H100 Training Hang Fix — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Fix H100 training hang (cuBLAS stream rebinding inside CUDA graph capture), fix regime stratification bug, and port regime classification + prefix sum to GPU.

**Architecture:** Split single `graph_forward` into two per-stream graphs (`graph_forward_main` + `graph_forward_ddqn`), orchestrate replay with ungraphed events. Add CUDA kernels for regime classification and prefix sum. Fix one-line fold generation bug.

**Tech Stack:** Rust 1.85, CUDA 12.4 (NVRTC), cudarc, cuBLAS, half (bf16)

**Spec:** `docs/superpowers/specs/2026-04-05-gpu-regime-and-training-hang-fix.md`

---

### Task 1: Fix the fold generation bug (one-line)

**Files:**
- Modify: `crates/ml/src/trainers/dqn/trainer/mod.rs:570`

- [ ] **Step 1: Change `generate_folds` to `generate_folds_stratified`**

In `crates/ml/src/trainers/dqn/trainer/mod.rs`, line 570, replace:

```rust
let folds = wf_config.generate_folds(features.len());
```

with:

```rust
let folds = wf_config.generate_folds_stratified(training_data);
```

The `training_data: &[(FeatureVector, Vec<f64>)]` parameter is in scope (passed to `train_walk_forward` at line 543).

- [ ] **Step 2: Verify build**

Run: `SQLX_OFFLINE=true cargo check -p ml`
Expected: clean build, no errors

- [ ] **Step 3: Run walk-forward tests**

Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- smoke_tests::walk_forward --ignored --test-threads=1 --nocapture 2>&1 | grep -E "test .*\.\.\.|test result"`
Expected: 3 walk-forward tests pass

- [ ] **Step 4: Commit**

```bash
git add crates/ml/src/trainers/dqn/trainer/mod.rs
git commit -m "fix: train_walk_forward uses regime-stratified fold generation

Was calling generate_folds() (non-stratified) instead of
generate_folds_stratified(). Walk-forward folds are now balanced
across trending/ranging/volatile regimes."
```

---

### Task 2: Create second CublasForward for double_dqn_stream

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` (struct fields + constructor)

- [ ] **Step 1: Add `cublas_forward_ddqn` field to GpuDqnTrainer**

In the struct definition (around line 757), after `cublas_forward: CublasForward,` add:

```rust
/// Second cuBLAS forward context for Pass 3 (Double DQN).
/// Bound to `double_dqn_stream` — never shares a handle with `cublas_forward`.
/// Eliminates set_stream() calls during CUDA graph capture.
cublas_forward_ddqn: CublasForward,
```

- [ ] **Step 2: Initialize `cublas_forward_ddqn` in the constructor**

In the `new()` function (around line 2496-2510), after `cublas_forward` is created, add:

```rust
let cublas_forward_ddqn = CublasForward::new(
    &double_dqn_stream,
    &config,
)?;
```

And add `cublas_forward_ddqn,` to the struct initialization.

- [ ] **Step 3: Verify build**

Run: `SQLX_OFFLINE=true cargo check -p ml`
Expected: clean build (the new field is not used yet)

- [ ] **Step 4: Commit**

```bash
git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs
git commit -m "feat: separate CublasForward for double_dqn_stream

Each CUDA graph needs its own cuBLAS handle bound to its stream.
Eliminates set_stream() calls during graph capture that caused
the H100 training hang."
```

---

### Task 3: Split submit_forward_ops into two per-stream functions

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Create `submit_forward_ops_main` (Pass 1 + Pass 2 + loss/grad/backward)**

Extract from `submit_forward_ops()` (lines ~4339-4720). The new function contains everything EXCEPT the Pass 3 Double DQN section (lines 4698-4718) and the event sync (lines 4679-4688, 4714-4718). Keep the loss, gradient, bf16 cast, and backward pass in this function.

```rust
/// Submit forward ops for main stream graph: Pass 1 (online forward) +
/// Pass 2 (target forward) + loss + gradient + backward.
/// Does NOT include Pass 3 (Double DQN) — that runs on double_dqn_stream.
fn submit_forward_ops_main(&mut self) -> Result<(), MLError> {
    // Zero accumulators (existing code from submit_forward_ops)
    // ... Pass 1: self.launch_cublas_forward_pass1() ...
    // ... Stochastic depth ...
    // ... Pass 2: target forward on next_states (main stream) ...
    // ... Loss + gradient (C51/MSE blend) ...
    // ... bf16 cast + backward ...
    Ok(())
}
```

The key change: remove lines 4679-4718 (event record, double_dqn wait, Pass 3, set_stream calls, join). Pass 1 + stochastic depth + Pass 2 + loss + grad + backward stay.

- [ ] **Step 2: Create `submit_forward_ops_ddqn` (Pass 3 only)**

New function using `self.cublas_forward_ddqn`:

```rust
/// Submit Pass 3 (Double DQN online forward on next_states) for
/// the double_dqn_stream graph. Uses its own CublasForward handle.
fn submit_forward_ops_ddqn(&mut self) -> Result<(), MLError> {
    let param_sizes = compute_param_sizes(&self.config);
    let on_w_ptrs = bf16_weight_ptrs_from_base(self.ptrs.params_buf, &param_sizes);

    self.cublas_forward_ddqn.forward_online_raw(
        &self.double_dqn_stream, self.ptrs.next_states_buf, &on_w_ptrs,
        self.ptrs.on_next_h_s1_scratch, self.ptrs.on_next_h_s2_scratch,
        self.ptrs.on_next_h_v_scratch,
        self.ptrs.on_next_h_b_scratch, self.ptrs.on_next_h_b_scratch,
        self.ptrs.on_next_h_b_scratch,
        self.ptrs.on_next_v_logits_buf, self.ptrs.on_next_b_logits_buf,
    )?;
    Ok(())
}
```

No `set_stream()` call. No event operations. Just the cuBLAS GEMMs on `double_dqn_stream`.

- [ ] **Step 3: Update submit_forward_ops_mse_only similarly**

The MSE-only path (`submit_forward_ops_mse_only`, line ~4453) also needs the same split — it currently doesn't have Pass 3 (it's MSE warmup), but verify it doesn't contain any `set_stream()` or event calls. If clean, leave it as-is.

- [ ] **Step 4: Verify build**

Run: `SQLX_OFFLINE=true cargo check -p ml`
Expected: warnings about unused `submit_forward_ops` (old function) — that's fine, we'll remove it in the next task.

- [ ] **Step 5: Commit**

```bash
git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs
git commit -m "refactor: split forward ops into per-stream functions

submit_forward_ops_main: Pass 1 + 2 + loss + grad + backward (main stream)
submit_forward_ops_ddqn: Pass 3 Double DQN (double_dqn_stream)

No set_stream() or event ops inside either function.
Graph capture will record each on its own stream."
```

---

### Task 4: Restructure capture_training_graphs for dual-graph capture

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs`

- [ ] **Step 1: Add `graph_forward_ddqn` field**

In the struct (near line 601), after `graph_forward_mse`:

```rust
/// Double DQN forward graph (Pass 3 on double_dqn_stream).
pub(crate) graph_forward_ddqn: Option<SendSyncGraph>,
```

- [ ] **Step 2: Rewrite `capture_training_graphs` to capture two forward graphs**

Replace the single `graph_forward` capture with:

```rust
// ── Capture graph_forward_main (Pass 1 + 2 + loss + grad + backward) ──
self.stream.begin_capture(...)?;
self.submit_forward_ops_main()?;
let graph_main = self.stream.end_capture(...)?.ok_or(...)?;

// ── Capture graph_forward_mse (MSE-only warmup, main stream) ──
self.stream.begin_capture(...)?;
self.submit_forward_ops_mse_only()?;
let graph_mse = self.stream.end_capture(...)?.ok_or(...)?;

// ── Capture graph_forward_ddqn (Pass 3, double_dqn_stream) ──
// Sync double_dqn_stream before capture
unsafe { cudarc::driver::sys::cuStreamSynchronize(self.double_dqn_stream.cu_stream()); }
self.double_dqn_stream.begin_capture(...)?;
self.submit_forward_ops_ddqn()?;
let graph_ddqn = self.double_dqn_stream.end_capture(...)?.ok_or(...)?;

// ── Capture graph_adam (unchanged) ──
// ... existing adam capture code ...
```

Store all four: `graph_forward`, `graph_forward_mse`, `graph_forward_ddqn`, `graph_adam`.

- [ ] **Step 3: Rewrite `replay_forward` with ungraphed event orchestration**

```rust
pub fn replay_forward(&self) -> Result<(), MLError> {
    // 1. Replay main stream graph (Pass 1 + 2 + loss + grad + backward)
    let main_graph = if self.c51_alpha < 1e-6 {
        self.graph_forward_mse.as_ref().or(self.graph_forward.as_ref())
    } else {
        self.graph_forward.as_ref()
    };
    if let Some(g) = main_graph {
        g.0.launch().map_err(|e| MLError::ModelError(format!("graph_forward_main replay: {e}")))?;
    }

    // 2. Record event after Pass 1 completes (main stream)
    self.pass1_event.record(&self.stream)
        .map_err(|e| MLError::ModelError(format!("pass1 event record: {e}")))?;

    // 3. double_dqn_stream waits for Pass 1
    self.double_dqn_stream.wait(&self.pass1_event)
        .map_err(|e| MLError::ModelError(format!("ddqn wait pass1: {e}")))?;

    // 4. Replay Pass 3 on double_dqn_stream (concurrent with Pass 2)
    if let Some(ref g) = self.graph_forward_ddqn {
        g.0.launch().map_err(|e| MLError::ModelError(format!("graph_forward_ddqn replay: {e}")))?;
    }

    // 5. Main stream waits for Pass 3 to complete (join before training step reads)
    self.pass3_event.record(&self.double_dqn_stream)
        .map_err(|e| MLError::ModelError(format!("pass3 event record: {e}")))?;
    self.stream.wait(&self.pass3_event)
        .map_err(|e| MLError::ModelError(format!("join pass3: {e}")))?;

    Ok(())
}
```

Events are OUTSIDE graph replay — in ungraphed Rust code. Each graph replays on its own stream with its own cuBLAS handle. No deadlock.

- [ ] **Step 4: Update `invalidate_training_graph` to clear `graph_forward_ddqn`**

Add `self.graph_forward_ddqn = None;` alongside the other graph invalidations.

- [ ] **Step 5: Remove old `submit_forward_ops` (now dead code)**

Delete the original `submit_forward_ops()` function that contained `set_stream()` calls.

- [ ] **Step 6: Verify build**

Run: `SQLX_OFFLINE=true cargo check -p ml`
Expected: clean build

- [ ] **Step 7: Run NaN regression + smoke tests**

Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- smoke_tests::regression::test_no_nan_after_graph_capture --ignored --nocapture`
Expected: passes

Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- smoke_tests::training_stability::test_production_training_stability --ignored --nocapture`
Expected: passes, 3 epochs complete, zero NaN

- [ ] **Step 8: Commit**

```bash
git add crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs
git commit -m "fix: dual-stream CUDA graph capture eliminates H100 hang

Split graph_forward into graph_forward_main (Pass 1+2+loss+backward
on main stream) and graph_forward_ddqn (Pass 3 on double_dqn_stream).

Event synchronization happens in ungraphed replay_forward() code,
not inside captured functions. Each graph uses its own CublasForward
handle — no set_stream() during capture.

Fixes: H100 training hang with gpu_n_episodes=4096."
```

---

### Task 5: GPU regime classification kernel

**Files:**
- Create: CUDA kernel source in `crates/ml/src/cuda_pipeline/dqn_utility_kernels.cu` (append)
- Modify: `crates/ml/src/cuda_pipeline/gpu_walk_forward.rs`

- [ ] **Step 1: Add CUDA kernel to dqn_utility_kernels.cu**

Append to `crates/ml/src/cuda_pipeline/dqn_utility_kernels.cu`:

```cuda
/* ══════════════════════════════════════════════════════════════════════
 * REGIME CLASSIFICATION KERNEL
 *
 * Classifies each bar into a market regime based on pre-computed
 * ADX (feature index 40) and CUSUM (feature index 41).
 *   ADX > 0.25        → 0 (Trending)
 *   |CUSUM| > 0.7     → 2 (Volatile)
 *   else              → 1 (Ranging)
 *
 * Launch config: grid=(ceil(total_bars/256), 1, 1), block=(256, 1, 1).
 * ══════════════════════════════════════════════════════════════════════ */
extern "C" __global__ void regime_classify_kernel(
    const float* __restrict__ features,  /* [total_bars, feat_stride] f32 */
    unsigned char* __restrict__ regimes, /* [total_bars] u8 output */
    int total_bars,
    int feat_stride                      /* 42 */
) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i >= total_bars) return;
    float adx   = features[i * feat_stride + 40];
    float cusum = features[i * feat_stride + 41];
    unsigned char regime;
    if (adx > 0.25f)             regime = 0;  /* Trending */
    else if (fabsf(cusum) > 0.7f) regime = 2;  /* Volatile */
    else                          regime = 1;  /* Ranging */
    regimes[i] = regime;
}

/* ══════════════════════════════════════════════════════════════════════
 * REGIME PREFIX SUM KERNEL
 *
 * Computes per-regime cumulative counts for O(1) range queries.
 * Output: prefix[r * (total_bars+1) + i] = count of regime r in bars [0, i).
 *
 * Simple sequential scan — runs once per training start, not latency-critical.
 * For 1.1M bars this takes <1ms on any GPU.
 *
 * Launch config: grid=(1, 1, 1), block=(3, 1, 1).  One thread per regime.
 * ══════════════════════════════════════════════════════════════════════ */
extern "C" __global__ void regime_prefix_sum_kernel(
    const unsigned char* __restrict__ regimes, /* [total_bars] */
    unsigned int* __restrict__ prefix,         /* [3, total_bars+1] output */
    int total_bars
) {
    int r = threadIdx.x;  /* regime index: 0, 1, or 2 */
    if (r >= 3) return;
    int stride = total_bars + 1;
    prefix[r * stride + 0] = 0;
    unsigned int count = 0;
    for (int i = 0; i < total_bars; i++) {
        if (regimes[i] == r) count++;
        prefix[r * stride + (i + 1)] = count;
    }
}
```

- [ ] **Step 2: Load kernels in gpu_walk_forward.rs**

Add to `gpu_walk_forward.rs` a new function `classify_regimes_gpu()` that:
1. Uploads f64 features to GPU as f32 (only indices 40 and 41 needed, but upload full stride for simplicity)
2. Allocates `regimes: CudaSlice<u8>` of size `total_bars`
3. Allocates `prefix: CudaSlice<u32>` of size `3 * (total_bars + 1)`
4. Launches `regime_classify_kernel`
5. Launches `regime_prefix_sum_kernel`
6. Downloads `prefix` to host `Vec<u32>`
7. Returns the prefix sums

```rust
/// Classify regimes on GPU and compute prefix sums for O(1) range queries.
/// Returns prefix sums: `prefix[r * (n+1) + i]` = count of regime `r` in `[0, i)`.
pub fn classify_regimes_gpu(
    stream: &Arc<CudaStream>,
    features_f32: &CudaSlice<f32>,  // [total_bars, 42] already on GPU
    total_bars: usize,
) -> Result<Vec<u32>, MLError> {
    // Load kernels from the utility cubin (same module as other training kernels)
    let context = stream.context();
    let module = context.load_cubin(super::gpu_dqn_trainer::DQN_UTILITY_CUBIN.to_vec())
        .map_err(|e| MLError::ModelError(format!("regime cubin load: {e}")))?;
    let classify_fn = module.load_function("regime_classify_kernel")
        .map_err(|e| MLError::ModelError(format!("regime_classify load: {e}")))?;
    let prefix_fn = module.load_function("regime_prefix_sum_kernel")
        .map_err(|e| MLError::ModelError(format!("regime_prefix_sum load: {e}")))?;

    // Allocate GPU buffers
    let mut regimes = stream.alloc_zeros::<u8>(total_bars)
        .map_err(|e| MLError::ModelError(format!("alloc regimes: {e}")))?;
    let prefix_len = 3 * (total_bars + 1);
    let mut prefix_gpu = stream.alloc_zeros::<u32>(prefix_len)
        .map_err(|e| MLError::ModelError(format!("alloc prefix: {e}")))?;

    // Launch classification
    let feat_stride = 42_i32;
    let n = total_bars as i32;
    let blocks = ((total_bars + 255) / 256) as u32;
    unsafe {
        stream.launch_builder(&classify_fn)
            .arg(features_f32).arg(&mut regimes).arg(&n).arg(&feat_stride)
            .launch(LaunchConfig { grid_dim: (blocks, 1, 1), block_dim: (256, 1, 1), shared_mem_bytes: 0 })
            .map_err(|e| MLError::ModelError(format!("regime_classify launch: {e}")))?;
    }

    // Launch prefix sum (3 threads, one per regime)
    unsafe {
        stream.launch_builder(&prefix_fn)
            .arg(&regimes).arg(&mut prefix_gpu).arg(&n)
            .launch(LaunchConfig { grid_dim: (1, 1, 1), block_dim: (3, 1, 1), shared_mem_bytes: 0 })
            .map_err(|e| MLError::ModelError(format!("regime_prefix_sum launch: {e}")))?;
    }

    // Download to host
    let mut host_prefix = vec![0u32; prefix_len];
    stream.memcpy_dtoh(&prefix_gpu, &mut host_prefix)
        .map_err(|e| MLError::ModelError(format!("prefix DtoH: {e}")))?;

    Ok(host_prefix)
}
```

- [ ] **Step 3: Verify build**

Run: `SQLX_OFFLINE=true cargo check -p ml`
Expected: clean build

- [ ] **Step 4: Commit**

```bash
git add crates/ml/src/cuda_pipeline/dqn_utility_kernels.cu crates/ml/src/cuda_pipeline/gpu_walk_forward.rs
git commit -m "feat: GPU regime classification + prefix sum kernels

regime_classify_kernel: parallel classification of 1.1M bars
regime_prefix_sum_kernel: cumulative counts for O(1) range queries
classify_regimes_gpu(): Rust wrapper that runs both and returns host prefix sums"
```

---

### Task 6: Update generate_folds_stratified to use GPU prefix sums

**Files:**
- Modify: `crates/ml/src/cuda_pipeline/gpu_walk_forward.rs`

- [ ] **Step 1: Add `generate_folds_stratified_gpu` method**

New method that takes a CUDA stream + GPU features buffer instead of CPU feature vectors:

```rust
/// Generate regime-stratified folds using GPU prefix sums for O(1) range queries.
pub fn generate_folds_stratified_gpu(
    &self,
    stream: &Arc<CudaStream>,
    features_f32: &CudaSlice<f32>,  // [total_bars, 42] on GPU
    total_bars: usize,
) -> Result<Vec<GpuFoldRange>, MLError> {
    // GPU: classify + prefix sum
    let prefix = classify_regimes_gpu(stream, features_f32, total_bars)?;
    let stride = total_bars + 1;

    // O(1) regime distribution for any range [start, end)
    let regime_dist = |start: usize, end: usize| -> [f64; 3] {
        let n = (end - start) as f64;
        if n < 1.0 { return [0.0, 100.0, 0.0]; }
        [
            (prefix[0 * stride + end] - prefix[0 * stride + start]) as f64 / n * 100.0,
            (prefix[1 * stride + end] - prefix[1 * stride + start]) as f64 / n * 100.0,
            (prefix[2 * stride + end] - prefix[2 * stride + start]) as f64 / n * 100.0,
        ]
    };

    // Rest of fold generation + boundary adjustment logic (same as CPU version)
    // but using regime_dist() for O(1) lookups instead of O(val_size) scans
    // ... (copy existing logic from generate_folds_stratified, replace
    //      regime_distribution_from_labels calls with regime_dist closures) ...

    Ok(folds)
}
```

- [ ] **Step 2: Update train_walk_forward to use GPU path when CUDA available**

In `trainer/mod.rs`, after features are uploaded to GPU (line 585), use the GPU stratification:

```rust
let folds = if self.cuda_stream.is_some() {
    let stream = self.cuda_stream.as_ref().unwrap();
    // Features are already on GPU from init_from_fxcache — get the f32 buffer
    let gpu_data = self.gpu_data.as_ref().unwrap();
    wf_config.generate_folds_stratified_gpu(stream, &gpu_data.features_f32, features.len())?
} else {
    wf_config.generate_folds_stratified(training_data)
};
```

- [ ] **Step 3: Verify build + run tests**

Run: `SQLX_OFFLINE=true cargo check -p ml`
Run: `SQLX_OFFLINE=true cargo test -p ml --lib -- smoke_tests::walk_forward --ignored --test-threads=1`
Expected: 3 walk-forward tests pass

- [ ] **Step 4: Commit**

```bash
git add crates/ml/src/cuda_pipeline/gpu_walk_forward.rs crates/ml/src/trainers/dqn/trainer/mod.rs
git commit -m "feat: GPU-accelerated regime-stratified fold generation

Boundary adjustment search uses O(1) prefix sum range queries
instead of O(val_size) per-candidate distribution recomputation.
1.1M bars: boundary search drops from ~1000ms to ~3ms."
```

---

### Task 7: Full test suite validation + H100 deployment test

**Files:**
- Modify: `crates/ml/src/trainers/dqn/smoke_tests/regression.rs` (add hang regression test)

- [ ] **Step 1: Run full unit test suite**

Run: `SQLX_OFFLINE=true cargo test -p ml --lib 2>&1 | grep "test result"`
Expected: 891+ passed, 0 failed

- [ ] **Step 2: Run full smoke test suite**

```bash
ps aux | grep "ml-f1" | grep -v grep | awk '{print $2}' | xargs kill -9 2>/dev/null
sleep 2
SQLX_OFFLINE=true cargo test -p ml --lib -- smoke_tests --ignored --test-threads=1 2>&1 | grep -E "test .*\.\.\. |test result"
```
Expected: 19/19 passed, 0 failed

- [ ] **Step 3: Commit all changes and push**

```bash
git push origin main
```

- [ ] **Step 4: Deploy to H100 via Argo**

```bash
./scripts/argo-train.sh dqn --baseline
```

Monitor first epoch:
```bash
argo logs <workflow-name> -n foxhunt -c main 2>&1 | grep "train-best" | grep -E "Epoch|EPOCH|loss=|NaN|error" | tail -5
```

Expected: Epoch 1 completes within 60 seconds, loss is finite, no NaN, no hang.

- [ ] **Step 5: Commit final validation**

```bash
git commit --allow-empty -m "validated: H100 training completes epoch 1 — no hang, no NaN"
```

---

## File Summary

| File | Tasks | Changes |
|------|-------|---------|
| `crates/ml/src/trainers/dqn/trainer/mod.rs` | 1, 6 | Fix fold generation bug, use GPU stratification |
| `crates/ml/src/cuda_pipeline/gpu_dqn_trainer.rs` | 2, 3, 4 | Dual cuBLAS handle, split forward ops, dual-graph capture + replay |
| `crates/ml/src/cuda_pipeline/dqn_utility_kernels.cu` | 5 | regime_classify_kernel + regime_prefix_sum_kernel |
| `crates/ml/src/cuda_pipeline/gpu_walk_forward.rs` | 5, 6 | classify_regimes_gpu(), generate_folds_stratified_gpu() |
| `crates/ml/src/cuda_pipeline/batched_forward.rs` | — | No changes (CublasForward::new already independent) |

## Task Dependencies

```
Task 1 (fold bug fix) ─────────────────────── independent
Task 2 (second cuBLAS) ──→ Task 3 (split ops) ──→ Task 4 (dual graph capture)
Task 5 (GPU kernels) ──→ Task 6 (GPU stratification)
Task 4 + Task 6 ──→ Task 7 (full validation + H100 deploy)
```

Tasks 1, 2, and 5 can start in parallel. Task 7 is the final gate.
