Netdev List
 help / color / mirror / Atom feed
From: Taehee Yoo <ap420073@gmail.com>
To: "Alex Deucher" <alexander.deucher@amd.com>,
	"Alexei Starovoitov" <ast@kernel.org>,
	amd-gfx@lists.freedesktop.org,
	"Andrew Lunn" <andrew+netdev@lunn.ch>,
	"Andrii Nakryiko" <andrii@kernel.org>,
	"Bill Wendling" <morbo@google.com>,
	bpf@vger.kernel.org, "Christian König" <christian.koenig@amd.com>,
	"Daniel Borkmann" <daniel@iogearbox.net>,
	"David Airlie" <airlied@gmail.com>,
	"David S. Miller" <davem@davemloft.net>,
	"Donald Hunter" <donald.hunter@gmail.com>,
	dri-devel@lists.freedesktop.org,
	"Eduard Zingerman" <eddyz87@gmail.com>,
	"Emil Tsalapatis" <emil@etsalapatis.com>,
	"Eric Dumazet" <edumazet@google.com>,
	"Felix Kuehling" <Felix.Kuehling@amd.com>,
	"Hoyeon Lee" <hoyeon.rhee@gmail.com>,
	"Ilias Apalodimas" <ilias.apalodimas@linaro.org>,
	"Jakub Kicinski" <kuba@kernel.org>,
	"Jesper Dangaard Brouer" <hawk@kernel.org>,
	"Jiri Olsa" <jolsa@kernel.org>,
	"John Fastabend" <john.fastabend@gmail.com>,
	"Justin Stitt" <justinstitt@google.com>,
	"Kees Cook" <kees@kernel.org>,
	"Kumar Kartikeya Dwivedi" <memxor@gmail.com>,
	"Leon Romanovsky" <leon@kernel.org>,
	linaro-mm-sig@lists.linaro.org, linux-hardening@vger.kernel.org,
	linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
	linux-media@vger.kernel.org, linux-rdma@vger.kernel.org,
	llvm@lists.linux.dev, "Mark Bloch" <mbloch@nvidia.com>,
	"Martin KaFai Lau" <martin.lau@linux.dev>,
	"Michael Chan" <michael.chan@broadcom.com>,
	"Nathan Chancellor" <nathan@kernel.org>,
	netdev@vger.kernel.org,
	"Nick Desaulniers" <ndesaulniers@google.com>,
	"Paolo Abeni" <pabeni@redhat.com>,
	"Pavan Chebbi" <pavan.chebbi@broadcom.com>,
	"Saeed Mahameed" <saeedm@nvidia.com>,
	"Shuah Khan" <shuah@kernel.org>,
	"Simona Vetter" <simona@ffwll.ch>,
	"Simon Horman" <horms@kernel.org>, "Song Liu" <song@kernel.org>,
	"Stanislav Fomichev" <sdf@fomichev.me>,
	"Sumit Semwal" <sumit.semwal@linaro.org>,
	"Taehee Yoo" <ap420073@gmail.com>,
	"Tariq Toukan" <tariqt@nvidia.com>,
	"Yonghong Song" <yonghong.song@linux.dev>
Subject: [RFC PATCH net-next 09/13] drm/amdkfd: add BPF-to-GPU JIT offload
Date: Sun, 19 Jul 2026 17:58:53 +0000	[thread overview]
Message-ID: <20260719175857.4071636-10-ap420073@gmail.com> (raw)
In-Reply-To: <20260719175857.4071636-1-ap420073@gmail.com>

Add the knod BPF feature: an XDP program attached in offload mode is
JIT-compiled from eBPF to an AMD GCN shader and dispatched on the GPU
against packets DMA'd into GPU memory, keeping the PASS/DROP/TX verdict
path off the host CPU.  Built as a separate module (knod_bpf).

Signed-off-by: Taehee Yoo <ap420073@gmail.com>
(cherry picked from commit 132d51819ffced59b5890e14bd39cab2e9c12eb4)
---
 drivers/gpu/drm/amd/amdkfd/Kconfig         |    11 +
 drivers/gpu/drm/amd/amdkfd/Makefile        |     2 +
 drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.c | 11554 +++++++++++++++++++
 drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.h |   597 +
 4 files changed, 12164 insertions(+)
 create mode 100644 drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.c
 create mode 100644 drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.h

diff --git a/drivers/gpu/drm/amd/amdkfd/Kconfig b/drivers/gpu/drm/amd/amdkfd/Kconfig
index d93f1af749ff..708aa5fc051b 100644
--- a/drivers/gpu/drm/amd/amdkfd/Kconfig
+++ b/drivers/gpu/drm/amd/amdkfd/Kconfig
@@ -49,3 +49,14 @@ config HSA_AMD_KNOD
 
 	  Say N to drop the KNOD core along with the BPF and IPsec offloads
 	  layered on top of it.  If unsure, say Y.
+
+config HSA_AMD_KNOD_BPF
+	tristate "KNOD BPF"
+	depends on HSA_AMD_KNOD
+	help
+	  GPU-accelerated XDP/BPF packet processing via KNOD.  BPF programs
+	  attached in XDP mode are JIT-compiled to AMD GCN shaders and run
+	  on the GPU against packets DMA'd directly into GPU memory, keeping
+	  the verdict path (PASS/DROP/TX) off the host CPU.
+
+	  If unsure, say N.
diff --git a/drivers/gpu/drm/amd/amdkfd/Makefile b/drivers/gpu/drm/amd/amdkfd/Makefile
index 1834faa54863..4df3850e1466 100644
--- a/drivers/gpu/drm/amd/amdkfd/Makefile
+++ b/drivers/gpu/drm/amd/amdkfd/Makefile
@@ -75,3 +75,5 @@ endif
 ifneq ($(CONFIG_HSA_AMD_KNOD),)
 AMDKFD_FILES += $(AMDKFD_PATH)/kfd_knod.o
 endif
+
+obj-$(CONFIG_HSA_AMD_KNOD_BPF) += $(AMDKFD_PATH)/knod/knod_bpf.o
diff --git a/drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.c b/drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.c
new file mode 100644
index 000000000000..f4f48e1b9f1c
--- /dev/null
+++ b/drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.c
@@ -0,0 +1,11554 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com>
+ * Copyright (c) 2021 Hoyeon Lee <hoyeon.rhee@gmail.com>
+ */
+
+#include <linux/cpumask.h>
+#include <linux/types.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/delay.h>
+#include <linux/sched.h>
+#include <linux/workqueue.h>
+#include <linux/file.h>
+#include <linux/jhash.h>
+#include <drm/ttm/ttm_tt.h>
+#include <net/page_pool/helpers.h>
+#include "kfd_priv.h"
+#include "kfd_hsa.h"
+#include "knod_bpf.h"
+#include "kfd_migrate.h"
+#include "kfd_events.h"
+#include "kfd_device_queue_manager.h"
+#include <linux/reciprocal_div.h>
+#include <linux/jhash.h>
+#include <net/knod.h>
+#include <net/netdev_rx_queue.h>
+
+/*+--------+---------+-------+------+--+-----+------+------+--------+
+ *| v0-v21 | v22-v59 |v60-v61| v62  |63|64-65|66-67 |68-69 | v70-127|
+ *+--------+---------+-------+------+--+-----+------+------+--------+
+ *|BPF REGS|TMP REGS |CTX REG| WIDX |R |DATA |D_END |PGBASE|PKTCACHE|
+ *+--------+---------+-------+------+--+-----+------+------+--------+
+ *+-----------------+
+ *| v128-v255       |
+ *+-----------------+
+ *| BPF STACK(512B) |
+ *+-----------------+
+ */
+
+/* Temp register map
+ *+-------------+-------------+---------------+---------------+
+ *|TREG0 - TREG2|TREG3 - TREG9|TREG10 - TREG16|TREG17 - TREG18|
+ *+-------------+-------------+---------------+---------------+
+ *| General Use | Key cache A |   Key in MAP  | JHASH Temp Reg|
+ *+-------------+-------------+---------------+---------------+
+ * Available Key cache size is 56.
+ * So, key size of map can't be exceed 56B.
+ */
+
+#define KNOD_AMDGPU_VREG0_LO		0 /* v0 */
+#define KNOD_AMDGPU_VREG0_HI		1
+#define KNOD_AMDGPU_VREG1_LO		2
+#define KNOD_AMDGPU_VREG1_HI		3
+#define KNOD_AMDGPU_VREG2_LO		4
+#define KNOD_AMDGPU_VREG2_HI		5
+#define KNOD_AMDGPU_VREG3_LO		6
+#define KNOD_AMDGPU_VREG3_HI		7
+#define KNOD_AMDGPU_VREG4_LO		8
+#define KNOD_AMDGPU_VREG4_HI		9
+#define KNOD_AMDGPU_VREG5_LO		10
+#define KNOD_AMDGPU_VREG5_HI		11
+#define KNOD_AMDGPU_VREG6_LO		12
+#define KNOD_AMDGPU_VREG6_HI		13
+#define KNOD_AMDGPU_VREG7_LO		14
+#define KNOD_AMDGPU_VREG7_HI		15
+#define KNOD_AMDGPU_VREG8_LO		16
+#define KNOD_AMDGPU_VREG8_HI		17
+#define KNOD_AMDGPU_VREG9_LO		18
+#define KNOD_AMDGPU_VREG9_HI		19
+#define KNOD_AMDGPU_FRAME_POINTER_VREG_LO 20 /* v20 */
+#define KNOD_AMDGPU_FRAME_POINTER_VREG_HI 21 /* v20 */
+
+#define KNOD_AMDGPU_TMP_VREG0_LO	22
+#define KNOD_AMDGPU_TMP_VREG0_HI	23
+#define KNOD_AMDGPU_TMP_VREG1_LO	24
+#define KNOD_AMDGPU_TMP_VREG1_HI	25
+#define KNOD_AMDGPU_TMP_VREG2_LO	26
+#define KNOD_AMDGPU_TMP_VREG2_HI	27
+#define KNOD_AMDGPU_TMP_VREG3_LO	28
+#define KNOD_AMDGPU_TMP_VREG3_HI	29
+#define KNOD_AMDGPU_TMP_VREG4_LO	30
+#define KNOD_AMDGPU_TMP_VREG4_HI	31
+#define KNOD_AMDGPU_TMP_VREG5_LO	32
+#define KNOD_AMDGPU_TMP_VREG5_HI	33
+#define KNOD_AMDGPU_TMP_VREG6_LO	34
+#define KNOD_AMDGPU_TMP_VREG6_HI	35
+#define KNOD_AMDGPU_TMP_VREG7_LO	36
+#define KNOD_AMDGPU_TMP_VREG7_HI	37
+#define KNOD_AMDGPU_TMP_VREG8_LO	38
+#define KNOD_AMDGPU_TMP_VREG8_HI	39
+#define KNOD_AMDGPU_TMP_VREG9_LO	40
+#define KNOD_AMDGPU_TMP_VREG9_HI	41
+#define KNOD_AMDGPU_TMP_VREG10_LO	42
+#define KNOD_AMDGPU_TMP_VREG10_HI	43
+#define KNOD_AMDGPU_TMP_VREG11_LO	44
+#define KNOD_AMDGPU_TMP_VREG11_HI	45
+#define KNOD_AMDGPU_TMP_VREG12_LO	46
+#define KNOD_AMDGPU_TMP_VREG12_HI	47
+#define KNOD_AMDGPU_TMP_VREG13_LO	48
+#define KNOD_AMDGPU_TMP_VREG13_HI	49
+#define KNOD_AMDGPU_TMP_VREG14_LO	50
+#define KNOD_AMDGPU_TMP_VREG14_HI	51
+#define KNOD_AMDGPU_TMP_VREG15_LO	52
+#define KNOD_AMDGPU_TMP_VREG15_HI	53
+#define KNOD_AMDGPU_TMP_VREG16_LO	54
+#define KNOD_AMDGPU_TMP_VREG16_HI	55
+#define KNOD_AMDGPU_TMP_VREG17_LO	56
+#define KNOD_AMDGPU_TMP_VREG17_HI	57
+#define KNOD_AMDGPU_TMP_VREG18_LO	58
+#define KNOD_AMDGPU_TMP_VREG18_HI	59
+#define KNOD_AMDGPU_TMP_VREG_MAX	KNOD_AMDGPU_TMP_VREG18_HI
+#define KNOD_AMDGPU_CTX_VREG_LO		60
+#define KNOD_AMDGPU_CTX_VREG_HI		61
+#define KNOD_AMDGPU_IDX_VREG		62
+#define KNOD_AMDGPU_RESERVED		63
+/*
+ * After prologue step 4, IDX_VREG is no longer needed.
+ * v62:v63 are repurposed to hold slot_addr (spsc_bd GTT address)
+ * through BPF execution and into the epilogue.
+ *
+ * BACKLOG_IDX_VREG (v58) saves the backlog index from IDX_VREG
+ * before step 6 overwrites it.  Used in epilogue for XDP_PASS.
+ */
+#define KNOD_AMDGPU_BACKLOG_IDX_VREG	KNOD_AMDGPU_TMP_VREG18_LO /* v58 */
+#define KNOD_AMDGPU_SLOT_VREG_LO	KNOD_AMDGPU_IDX_VREG	/* v62 */
+#define KNOD_AMDGPU_SLOT_VREG_HI	KNOD_AMDGPU_RESERVED	/* v63 */
+/*
+ * DATA/DATA_END VGPRs: hold packet gaddr and end address.
+ * Set in prologue, read by BPF ctx->data / ctx->data_end accesses.
+ * Replaces GTT round-trip (prologue store -> BPF load).
+ */
+#define KNOD_AMDGPU_DATA_VREG_LO	64
+#define KNOD_AMDGPU_DATA_VREG_HI	65
+#define KNOD_AMDGPU_DATA_END_VREG_LO	66
+#define KNOD_AMDGPU_DATA_END_VREG_HI	67
+#define KNOD_AMDGPU_PAGE_BASE_VREG_LO	68
+#define KNOD_AMDGPU_PAGE_BASE_VREG_HI	69
+#define KNOD_AMDGPU_PKT_CACHE_VREG0	70
+#define KNOD_AMDGPU_PKT_CACHE_VREG_MAX	127 /* 0 ~ 127 vgprs are available */
+#define KNOD_AMDGPU_STACK_VREG0		128
+#define KNOD_AMDGPU_STACK_VREG_MAX	255 /* 128 ~ 255 vgprs are available */
+
+#define KNOD_BPF_PROG_BUF_SIZE		32768
+
+/* Index for r64.
+ * r64[TREG64_0]
+ */
+#define TREG64_0			0
+#define TREG64_1			1
+#define TREG64_2			2
+#define TREG64_3			3
+#define KEY_IN_PKT_64			TREG64_3
+#define TREG64_4			4
+#define TREG64_5			5
+#define TREG64_6			6
+#define TREG64_7			7
+#define TREG64_8			8
+#define TREG64_9			9
+#define TREG64_10			10
+#define KEY_IN_MAP_64			TREG64_10
+#define TREG64_11			11
+#define TREG64_12			12
+#define TREG64_13			13
+#define TREG64_14			14
+#define TREG64_15			15
+#define TREG64_16			16
+#define TREG64_17			17
+#define TREG64_18			18
+
+#define MAX_MAP_KEY_SIZE		56
+
+/* Index for r32.
+ * r32[TREG32_0_LO]
+ */
+#define TREG32_0_LO			0
+#define TREG32_0_HI			1
+#define TREG32_1_LO			2
+#define TREG32_1_HI			3
+#define TREG32_2_LO			4
+#define TREG32_2_HI			5
+#define TREG32_3_LO			6
+#define KEY_IN_PKT_32			TREG32_3_LO
+#define TREG32_3_HI			7
+#define TREG32_4_LO			8
+#define TREG32_4_HI			9
+#define TREG32_5_LO			10
+#define TREG32_5_HI			11
+#define TREG32_6_LO			12
+#define TREG32_6_HI			13
+#define TREG32_7_LO			14
+#define TREG32_7_HI			15
+#define TREG32_8_LO			16
+#define TREG32_8_HI			17
+#define TREG32_9_LO			18
+#define TREG32_9_HI			19
+#define TREG32_10_LO			20
+#define KEY_IN_MAP_32			TREG32_10_LO
+#define TREG32_10_HI			21
+#define TREG32_11_LO			22
+#define TREG32_11_HI			23
+#define TREG32_12_LO			24
+#define TREG32_12_HI			25
+#define TREG32_13_LO			26
+#define TREG32_13_HI			27
+#define TREG32_14_LO			28
+#define TREG32_14_HI			29
+#define TREG32_15_LO			30
+#define TREG32_15_HI			31
+#define TREG32_16_LO			32
+#define TREG32_16_HI			33
+#define TREG32_17_LO			34
+#define TREG32_17_HI			35
+#define TREG32_18_LO			36
+#define TREG32_18_HI			37
+#define TREG32_MAX			TREG32_18_HI
+
+/*+--------+--------------------------------------+---+---+
+ *| s[0:3] |s[4:5] s[6:7] s[8:9] s[10:11] s[12:13]|s14|s15|
+ *+--------+--------------------------------------+---+---+
+ *|  PSB   | USER SGPRs (disp/queue/karg/id/flat) |WGX|QID|
+ *+--------+--------------------------------------+---+---+
+ *+----------------+------+---+---+------+-------+-------------------+
+ *|   s[16:27]     |s28:29|s30|s31|s32:33|s34:35 |    s[36:105]      |
+ *+----------------+------+---+---+------+-------+-------------------+
+ *|TMP_SREG 0-5    |PARAM |FP | - | GFX9 | DONE  | EXEC_SAVE PAIRS   |
+ *|(6 x 64-bit)    |SREG  |   |   |BROKE!| MASK  | (max 35, GFX10)   |
+ *+----------------+------+---+---+------+-------+-------------------+
+ * Implicit: VCC = s[106:107]  EXEC = s[126:127]
+ *
+ * user_sgpr_count=14, same on GFX9 and GFX10.
+ * enable_sgpr_private_segment_size is disabled so that workgroup_id_y
+ * lands at s15 and TMP_SREG0_LO stays at s16 (keeps 64-bit SGPR pair
+ * alignment; avoids shifting the entire TMP/PARAM/FRAME layout).
+ */
+#define KNOD_AMDGPU_PSB_SREG		0  /* s[0:3] private_segment_buffer */
+#define KNOD_AMDGPU_DISPATCH_PTR_SREG	4  /* s[4:5] dispatch_ptr */
+#define KNOD_AMDGPU_ARG_SREG		4  /* alias for dispatch_ptr */
+#define KNOD_AMDGPU_QUEUE_PTR_SREG	6  /* s[6:7] queue_ptr */
+#define KNOD_AMDGPU_KERNARG_PTR_SREG	8  /* s[8:9] kernarg_segment_ptr */
+#define KNOD_AMDGPU_DISPATCH_ID_SREG	10 /* s[10:11] dispatch_id */
+#define KNOD_AMDGPU_FLAT_SCR_INIT_SREG	12 /* s[12:13] flat_scratch_init */
+#define KNOD_AMDGPU_WORKGROUP_ID_X_SREG	14 /* s14 workgroup_id_x */
+#define KNOD_AMDGPU_WORKGROUP_ID_Y_SREG	15 /* s15 workgroup_id_y = queue_id */
+#define KNOD_AMDGPU_TMP_SREG0_LO	16
+#define KNOD_AMDGPU_TMP_SREG0_HI	17
+#define KNOD_AMDGPU_TMP_SREG1_LO	18
+#define KNOD_AMDGPU_TMP_SREG1_HI	19
+#define KNOD_AMDGPU_TMP_SREG2_LO	20
+#define KNOD_AMDGPU_TMP_SREG2_HI	21
+#define KNOD_AMDGPU_TMP_SREG3_LO	22
+#define KNOD_AMDGPU_TMP_SREG3_HI	23
+#define KNOD_AMDGPU_TMP_SREG4_LO	24
+#define KNOD_AMDGPU_TMP_SREG4_HI	25
+#define KNOD_AMDGPU_TMP_SREG5_LO	26
+#define KNOD_AMDGPU_TMP_SREG5_HI	27
+#define KNOD_AMDGPU_PARAM_SREG_LO	28 /* s28 */
+#define KNOD_AMDGPU_PARAM_SREG_HI	29 /* s29 */
+#define KNOD_AMDGPU_FRAME_POINTER_SREG	30 /* s30 */
+
+/* Structurized CFG: EXEC mask save/restore SGPRs.
+ * done_mask tracks lanes that have reached BPF_EXIT.
+ * exec_save pairs store EXEC at branch points for restore at merge points.
+ * GFX9: s[0:101] addressable (102 SGPRs), GFX10: s[0:105] (106 SGPRs).
+ * NOTE: s[32:33] is corrupted by GFX9 hardware - do NOT use on GFX9.
+ * GFX10 uses s[32:33] for done_mask and starts exec_save at s[34].
+ */
+/* Common SGPR special register indices (same on GFX9 and GFX10) */
+#define AMDGCN_SREG_VCC_LO		106
+#define AMDGCN_SREG_EXEC_LO		126
+#define AMDGCN_SREG_INTEGER_0		128
+#define AMDGCN_SREG_INTEGER_1		129
+
+/* s[34:35] - must not overlap TMP_SREGs */
+#define KNOD_AMDGPU_DONE_MASK_SREG	34
+#define KNOD_AMDGPU_EXEC_SAVE_SREG_BASE 36 /* s[36:37], s[38:39], ... */
+#define KNOD_AMDGPU_INITIAL_EXEC_SREG_GFX9 100
+#define KNOD_AMDGPU_INITIAL_EXEC_SREG_GFX10 104
+/* exec_save can fill up to each ISA's top usable SGPR pair. GFX9 lays the
+ * initial in-bounds EXEC snapshot immediately after the pairs a program
+ * actually uses, so small programs keep the old 64-SGPR occupancy window.
+ * GFX10 keeps the original high fixed snapshot pair.
+ */
+#define KNOD_AMDGPU_EXEC_SAVE_SREG_MAX_GFX9  99
+#define KNOD_AMDGPU_EXEC_SAVE_SREG_MAX_GFX10 103
+#define KNOD_AMDGPU_MAX_EXEC_SAVE_PAIRS_GFX9 \
+	((KNOD_AMDGPU_EXEC_SAVE_SREG_MAX_GFX9 - KNOD_AMDGPU_EXEC_SAVE_SREG_BASE + 1) / 2)
+#define KNOD_AMDGPU_MAX_EXEC_SAVE_PAIRS_GFX10 \
+	((KNOD_AMDGPU_EXEC_SAVE_SREG_MAX_GFX10 - KNOD_AMDGPU_EXEC_SAVE_SREG_BASE + 1) / 2)
+
+static u8 knod_bpf_gfx9_sgpr_granule(unsigned int sgprs_used)
+{
+	if (sgprs_used <= 16)
+		return 0;
+
+	return 2 * (DIV_ROUND_UP(sgprs_used, 16) - 1);
+}
+
+unsigned int knod_bpf_workgroups = KNOD_BPF_WORKGROUPS_DEFAULT;
+MODULE_PARM_DESC(workgroups, "Workgroup size, multiple of 64, Min(64) Default/Max(256)");
+module_param_named(workgroups, knod_bpf_workgroups, int, 0600);
+
+unsigned int knod_bpf_expire = KNOD_BPF_EXPIRE_DEFAULT;
+MODULE_PARM_DESC(queue_expire, "Queue expire time(ms), Min(1), Default(10), Max(1000)");
+module_param_named(queue_expire, knod_bpf_expire, int, 0600);
+
+unsigned int knod_bpf_pkt_cache;
+MODULE_PARM_DESC(packet_cache, "Use packet cache, 0=Off(Default), 1=On");
+module_param_named(packet_cache, knod_bpf_pkt_cache, int, 0600);
+
+unsigned int knod_bpf_wave32;
+MODULE_PARM_DESC(wave32, "Use wave32 0=Off(Default), 1=On");
+module_param_named(wave32, knod_bpf_wave32, int, 0600);
+
+#define KNOD_EA(extack, msg)   NL_SET_ERR_MSG_MOD((extack), msg)
+
+DEFINE_STATIC_KEY_FALSE(knod_stats_key);
+
+static const u32 bl_bounds[KNOD_BL_BUCKETS - 1] = {
+	16, 64, 256, 1024, 4096, 8192, 16384
+};
+
+static const char * const lat_labels[] = {
+	"< 1us", "1-2us", "2-4us", "4-8us", "8-16us",
+	"16-32us", "32-64us", "64-128us", "128-256us", ">= 256us",
+};
+
+static const char * const bl_labels[] = {
+	"1-16", "17-64", "65-256", "257-1K",
+	"1K-4K", "4K-8K", "8K-16K", ">= 16K",
+};
+
+static LIST_HEAD(priv_list);
+struct amdgcn_param64 r64[20], sr64[6], p64[4], bpf_reg64[11];
+struct amdgcn_param32 r32[40]; /* last two is CTX */
+struct amdgcn_param32 stack[128];
+struct amdgcn_param32 pkt_cache[64];
+
+struct amdgcn_label {
+	struct knod_insn_meta *meta;
+	int insn_idx;
+};
+
+struct amdgcn_branch_fixup {
+	struct amdgcn_label *target_label;
+	struct knod_insn_meta *meta;
+	int insn_idx;
+};
+
+struct knod_accel_xdp_ops accel_xdp_ops;
+
+static int knod_prog_prepare_insns(struct knod_bpf_priv *priv,
+				   struct knod_prog *knod_prog);
+static int knod_bpf_worker(void *arg);
+static void knod_bpf_drain_worker(struct knod_bpf_priv *priv);
+static void knod_prog_free(struct knod_prog *knod_prog);
+static void knod_emit_pass_addr_store(struct knod_bpf_priv *priv,
+				      struct knod_insn_meta *meta);
+static void knod_setup_bpf_prog(struct bpf_prog *prog);
+
+static void knod_bpf_gpu_mem_fence(struct knod_bpf_priv *priv)
+{
+	if (!priv)
+		return;
+
+	/* drain the WC store buffer before the GPU reads the map */
+	wmb();
+}
+
+static unsigned int knod_bpf_active_rxq_count(struct net_device *netdev)
+{
+	unsigned int nr_rxq;
+
+	if (!netdev)
+		return 0;
+
+	nr_rxq = READ_ONCE(netdev->real_num_rx_queues);
+	if (!nr_rxq)
+		nr_rxq = netdev->num_rx_queues;
+
+	return min_t(unsigned int, nr_rxq, KNOD_SPSC_MAX);
+}
+
+static void knod_bpf_fill_dispatch(struct knod_bpf_priv *priv,
+				   struct knod_bpf_work_sq *sqw,
+				   struct knod_dispatch_params *p)
+{
+	struct knod_bpf_param *param = sqw->param->kaddr;
+
+	p->workgroup_size_x = knod_bpf_workgroups;
+	p->grid_size_x = priv->batch_size;
+	p->grid_size_y = param->nr_queues;
+	p->private_segment_size = 8192;
+	p->group_segment_size = 8192;
+	p->kernel_object =
+		(u64)priv->knod->kernels[READ_ONCE(priv->active_idx)]->gaddr;
+	p->kernarg_address = sqw->param->gaddr;
+}
+
+static void debug_kernel_descriptor(struct kernel_descriptor *kernel_code)
+{
+	knod_jit_dbg(" kernel_code->group_segment_fixed_size = %d\n",
+		kernel_code->group_segment_fixed_size);
+	knod_jit_dbg(" kernel_code->private_segment_fixed_size = %d\n",
+		kernel_code->private_segment_fixed_size);
+	knod_jit_dbg(" kernel_code->kernarg_size = %d\n",
+		kernel_code->kernarg_size);
+	knod_jit_dbg(" kernel_code->kernel_code_entry_byte_offset = %lld\n",
+		kernel_code->kernel_code_entry_byte_offset);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc3.accum_offset = %d\n",
+		kernel_code->compute_pgm_rsrc3.accum_offset);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc3.reserved0 = %d\n",
+		kernel_code->compute_pgm_rsrc3.reserved0);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc3.tg_split = %d\n",
+		kernel_code->compute_pgm_rsrc3.tg_split);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc3.reserved1 = %d\n",
+		kernel_code->compute_pgm_rsrc3.reserved1);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.granulated_workitem_vgpr_count = %d\n",
+		kernel_code->compute_pgm_rsrc1.granulated_workitem_vgpr_count);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.granulated_wavefront_sgpr_count = %d\n",
+		kernel_code->compute_pgm_rsrc1.granulated_wavefront_sgpr_count);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.priority = %d\n",
+		kernel_code->compute_pgm_rsrc1.priority);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.float_round_mode_32 = %d\n",
+		kernel_code->compute_pgm_rsrc1.float_round_mode_32);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.float_round_mode_16_64 = %d\n",
+		kernel_code->compute_pgm_rsrc1.float_round_mode_16_64);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.float_denorm_mode_32 = %d\n",
+		kernel_code->compute_pgm_rsrc1.float_denorm_mode_32);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.float_denorm_mode_16_64 = %d\n",
+		kernel_code->compute_pgm_rsrc1.float_denorm_mode_16_64);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.priv = %d\n",
+		kernel_code->compute_pgm_rsrc1.priv);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.enable_dx10_clamp = %d\n",
+		kernel_code->compute_pgm_rsrc1.enable_dx10_clamp);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.debug_mode = %d\n",
+		kernel_code->compute_pgm_rsrc1.debug_mode);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.enable_ieee_mode = %d\n",
+		kernel_code->compute_pgm_rsrc1.enable_ieee_mode);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.bulky = %d\n",
+		kernel_code->compute_pgm_rsrc1.bulky);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.cdbg_user = %d\n",
+		kernel_code->compute_pgm_rsrc1.cdbg_user);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.fp16_ovfl = %d\n",
+		kernel_code->compute_pgm_rsrc1.fp16_ovfl);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.reserved0 = %d\n",
+		kernel_code->compute_pgm_rsrc1.reserved0);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.wgp_mode = %d\n",
+		kernel_code->compute_pgm_rsrc1.wgp_mode);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.mem_ordered = %d\n",
+		kernel_code->compute_pgm_rsrc1.mem_ordered);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc1.fwd_progress = %d\n",
+		kernel_code->compute_pgm_rsrc1.fwd_progress);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_private_segment = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_private_segment);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.user_sgpr_count = %d\n",
+		kernel_code->compute_pgm_rsrc2.user_sgpr_count);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_trap_handler = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_trap_handler);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_x = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_x);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_y = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_y);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_z = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_z);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_info = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_info);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_vgpr_workitem_id = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_vgpr_workitem_id);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_address_watch = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_exception_address_watch);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_memory = %d\n",
+		kernel_code->compute_pgm_rsrc2.enable_exception_memory);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.granulated_lds_size = %d\n",
+		kernel_code->compute_pgm_rsrc2.granulated_lds_size);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_ieee_754_fp_invalid_operation = %d\n",
+		kernel_code->compute_pgm_rsrc2
+			.enable_exception_ieee_754_fp_invalid_operation);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_fp_denormal_source = %d\n",
+		kernel_code->compute_pgm_rsrc2
+			.enable_exception_fp_denormal_source);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_ieee_754_fp_division_by_zero = %d\n",
+		kernel_code->compute_pgm_rsrc2
+			.enable_exception_ieee_754_fp_division_by_zero);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_ieee_754_fp_overflow = %d\n",
+		kernel_code->compute_pgm_rsrc2
+			.enable_exception_ieee_754_fp_overflow);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_ieee_754_fp_underflow = %d\n",
+		kernel_code->compute_pgm_rsrc2
+			.enable_exception_ieee_754_fp_underflow);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_ieee_754_fp_inexact = %d\n",
+		kernel_code->compute_pgm_rsrc2
+			.enable_exception_ieee_754_fp_inexact);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.enable_exception_int_divide_by_zero = %d\n",
+		kernel_code->compute_pgm_rsrc2
+			.enable_exception_int_divide_by_zero);
+	knod_jit_dbg(" kernel_code->compute_pgm_rsrc2.reserved0 = %d\n",
+		kernel_code->compute_pgm_rsrc2.reserved0);
+	knod_jit_dbg(" kernel_code->code_properties.enable_sgpr_private_segment_buffer = %d\n",
+		kernel_code->code_properties
+			.enable_sgpr_private_segment_buffer);
+	knod_jit_dbg(" kernel_code->code_properties.enable_sgpr_dispatch_ptr = %d\n",
+		kernel_code->code_properties.enable_sgpr_dispatch_ptr);
+	knod_jit_dbg(" kernel_code->code_properties.enable_sgpr_queue_ptr = %d\n",
+		kernel_code->code_properties.enable_sgpr_queue_ptr);
+	knod_jit_dbg(" kernel_code->code_properties.enable_sgpr_kernarg_segment_ptr = %d\n",
+		kernel_code->code_properties.enable_sgpr_kernarg_segment_ptr);
+	knod_jit_dbg(" kernel_code->code_properties.enable_sgpr_dispatch_id = %d\n",
+		kernel_code->code_properties.enable_sgpr_dispatch_id);
+	knod_jit_dbg(" kernel_code->code_properties.enable_sgpr_flat_scratch_init = %d\n",
+		kernel_code->code_properties.enable_sgpr_flat_scratch_init);
+	knod_jit_dbg(" kernel_code->code_properties.enable_sgpr_private_segment_size = %d\n",
+		kernel_code->code_properties.enable_sgpr_private_segment_size);
+	knod_jit_dbg(" kernel_code->code_properties.reserved0 = %d\n",
+		kernel_code->code_properties.reserved0);
+	knod_jit_dbg(" kernel_code->code_properties.enable_wavefront_size32 = %d\n",
+		kernel_code->code_properties.enable_wavefront_size32);
+	knod_jit_dbg(" kernel_code->code_properties.uses_dynamic_stack = %d\n",
+		kernel_code->code_properties.uses_dynamic_stack);
+	knod_jit_dbg(" kernel_code->code_properties.reserved1 = %d\n",
+		kernel_code->code_properties.reserved1);
+}
+
+static void kfd_kernel_gfx9_init(struct knod *knod)
+{
+	struct kernel_descriptor *kernel_code = knod->kernels[0]->kaddr;
+
+	kernel_code->group_segment_fixed_size = 0;
+	kernel_code->private_segment_fixed_size = 8192;
+	kernel_code->kernarg_size = 64;
+	kernel_code->kernel_code_entry_byte_offset = 1024;
+
+	/* GFX10+ or GFX90A+ */
+	kernel_code->compute_pgm_rsrc3.accum_offset = 0;
+	kernel_code->compute_pgm_rsrc3.reserved0 = 0;
+	kernel_code->compute_pgm_rsrc3.tg_split = 0;
+	kernel_code->compute_pgm_rsrc3.reserved1 = 0;
+
+	kernel_code->compute_pgm_rsrc1.granulated_workitem_vgpr_count =
+		(256 / 4) - 1;
+	/*
+	 * Start with the small GFX9 window. BPF install updates each slot
+	 * descriptor when a program needs a larger exec_save/initial_exec
+	 * range.
+	 */
+	kernel_code->compute_pgm_rsrc1.granulated_wavefront_sgpr_count =
+		knod_bpf_gfx9_sgpr_granule(52);
+	kernel_code->compute_pgm_rsrc1.priority = 0;
+	kernel_code->compute_pgm_rsrc1.float_round_mode_32 = 0;
+	kernel_code->compute_pgm_rsrc1.float_round_mode_16_64 = 0;
+	kernel_code->compute_pgm_rsrc1.float_denorm_mode_32 = 3;
+	kernel_code->compute_pgm_rsrc1.float_denorm_mode_16_64 = 3;
+	kernel_code->compute_pgm_rsrc1.priv = 0;
+	kernel_code->compute_pgm_rsrc1.enable_dx10_clamp = 1;
+	kernel_code->compute_pgm_rsrc1.debug_mode = 0;
+	kernel_code->compute_pgm_rsrc1.enable_ieee_mode = 1;
+	kernel_code->compute_pgm_rsrc1.bulky = 0;
+	kernel_code->compute_pgm_rsrc1.cdbg_user = 0;
+	kernel_code->compute_pgm_rsrc1.fp16_ovfl = 0;
+	kernel_code->compute_pgm_rsrc1.reserved0 = 0;
+	kernel_code->compute_pgm_rsrc1.wgp_mode = 0;
+	kernel_code->compute_pgm_rsrc1.mem_ordered = 0;
+	kernel_code->compute_pgm_rsrc1.fwd_progress = 0;
+
+	kernel_code->compute_pgm_rsrc2.enable_private_segment = 0;
+	kernel_code->compute_pgm_rsrc2.user_sgpr_count = 14; /* 4+2+2+2+2+2 */
+	kernel_code->compute_pgm_rsrc2.enable_trap_handler = 0;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_x = 1;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_y = 1;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_z = 0;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_info = 0;
+	kernel_code->compute_pgm_rsrc2.enable_vgpr_workitem_id = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_address_watch = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_memory = 0;
+	kernel_code->compute_pgm_rsrc2.granulated_lds_size = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_invalid_operation = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_fp_denormal_source = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_division_by_zero = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_overflow = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_underflow = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_ieee_754_fp_inexact = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_int_divide_by_zero = 0;
+	kernel_code->compute_pgm_rsrc2.reserved0 = 0;
+
+	/*
+	 * User SGPR layout - loaded in fixed order, disabled entries are
+	 * skipped (not reserved).  The resulting SGPR map depends on which
+	 * flags are enabled:
+	 *
+	 *   enable_sgpr_private_segment_buffer  -> 4 SGPRs  (s[0:3])
+	 *   enable_sgpr_dispatch_ptr            -> 2 SGPRs  (s[4:5])
+	 *   enable_sgpr_queue_ptr               -> 2 SGPRs
+	 *   enable_sgpr_kernarg_segment_ptr     -> 2 SGPRs
+	 *   enable_sgpr_dispatch_id             -> 2 SGPRs
+	 *   enable_sgpr_flat_scratch_init       -> 2 SGPRs
+	 *   enable_sgpr_private_segment_size    -> 1 SGPR
+	 *
+	 * System SGPRs (WorkgroupId etc.) follow immediately after the
+	 * last user SGPR.  user_sgpr_count must match the total above.
+	 */
+	/* 4 SGPRs */
+	kernel_code->code_properties.enable_sgpr_private_segment_buffer = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_dispatch_ptr = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_queue_ptr = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_kernarg_segment_ptr = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_dispatch_id = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_flat_scratch_init = 1;
+	/* disabled -> s14/s15 free for workgroup_id */
+	kernel_code->code_properties.enable_sgpr_private_segment_size = 0;
+	/* total = 14 SGPRs */
+	kernel_code->code_properties.reserved0 = 0;
+	/* GFX10+ */
+	kernel_code->code_properties.enable_wavefront_size32 = 0;
+	kernel_code->code_properties.uses_dynamic_stack = 0;
+	kernel_code->code_properties.reserved1 = 0;
+
+	debug_kernel_descriptor(kernel_code);
+}
+
+static void kfd_kernel_gfx10_init(struct knod *knod)
+{
+	struct kernel_descriptor *kernel_code = knod->kernels[0]->kaddr;
+
+	kernel_code->group_segment_fixed_size = 0;
+	kernel_code->private_segment_fixed_size = 8192;
+	kernel_code->kernarg_size = 64;
+	kernel_code->kernel_code_entry_byte_offset = 1024;
+
+	/*
+	 * User SGPR layout - loaded in fixed order, disabled entries are
+	 * skipped (not reserved).  The resulting SGPR map depends on which
+	 * flags are enabled:
+	 *
+	 *   enable_sgpr_private_segment_buffer  -> 4 SGPRs  (s[0:3])
+	 *   enable_sgpr_dispatch_ptr            -> 2 SGPRs  (s[4:5])
+	 *   enable_sgpr_queue_ptr               -> 2 SGPRs
+	 *   enable_sgpr_kernarg_segment_ptr     -> 2 SGPRs
+	 *   enable_sgpr_dispatch_id             -> 2 SGPRs
+	 *   enable_sgpr_flat_scratch_init       -> 2 SGPRs
+	 *   enable_sgpr_private_segment_size    -> 1 SGPR
+	 *
+	 * System SGPRs (WorkgroupId etc.) follow immediately after the
+	 * last user SGPR.  user_sgpr_count must match the total above.
+	 */
+	/* 4 SGPRs */
+	kernel_code->code_properties.enable_sgpr_private_segment_buffer = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_dispatch_ptr = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_queue_ptr = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_kernarg_segment_ptr = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_dispatch_id = 1;
+	/* 2 SGPRs */
+	kernel_code->code_properties.enable_sgpr_flat_scratch_init = 1;
+	/* disabled -> s14/s15 free for workgroup_id */
+	kernel_code->code_properties.enable_sgpr_private_segment_size = 0;
+	/* total = 14 SGPRs */
+	kernel_code->code_properties.reserved0 = 0;
+	if (knod_bpf_wave32)
+		kernel_code->code_properties.enable_wavefront_size32 = 1;
+	else
+		kernel_code->code_properties.enable_wavefront_size32 = 0;
+	kernel_code->code_properties.uses_dynamic_stack = 0;
+	kernel_code->code_properties.reserved1 = 0;
+
+	kernel_code->compute_pgm_rsrc3.accum_offset = 0;
+	kernel_code->compute_pgm_rsrc3.reserved0 = 0;
+	kernel_code->compute_pgm_rsrc3.tg_split = 0;
+	kernel_code->compute_pgm_rsrc3.reserved1 = 0;
+
+	if (kernel_code->code_properties.enable_wavefront_size32 == 1)
+		kernel_code->compute_pgm_rsrc1.granulated_workitem_vgpr_count =
+			(256 / 8) - 1;
+	else
+		kernel_code->compute_pgm_rsrc1.granulated_workitem_vgpr_count =
+			(256 / 4) - 1;
+	kernel_code->compute_pgm_rsrc1.granulated_wavefront_sgpr_count = 0;
+	kernel_code->compute_pgm_rsrc1.priority = 0;
+	kernel_code->compute_pgm_rsrc1.float_round_mode_32 = 0;
+	kernel_code->compute_pgm_rsrc1.float_round_mode_16_64 = 0;
+	kernel_code->compute_pgm_rsrc1.float_denorm_mode_32 = 3;
+	kernel_code->compute_pgm_rsrc1.float_denorm_mode_16_64 = 3;
+	kernel_code->compute_pgm_rsrc1.priv = 0;
+	kernel_code->compute_pgm_rsrc1.enable_dx10_clamp = 1;
+	kernel_code->compute_pgm_rsrc1.debug_mode = 0;
+	kernel_code->compute_pgm_rsrc1.enable_ieee_mode = 1;
+	kernel_code->compute_pgm_rsrc1.bulky = 0;
+	kernel_code->compute_pgm_rsrc1.cdbg_user = 0;
+	kernel_code->compute_pgm_rsrc1.fp16_ovfl = 0;
+	kernel_code->compute_pgm_rsrc1.reserved0 = 0;
+	kernel_code->compute_pgm_rsrc1.wgp_mode = 0;
+	kernel_code->compute_pgm_rsrc1.mem_ordered = 1;
+	kernel_code->compute_pgm_rsrc1.fwd_progress = 0;
+
+	kernel_code->compute_pgm_rsrc2.enable_private_segment = 0;
+	kernel_code->compute_pgm_rsrc2.user_sgpr_count = 14; /* 4+2+2+2+2+2 */
+	kernel_code->compute_pgm_rsrc2.enable_trap_handler = 0;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_x = 1;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_y = 1;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_id_z = 0;
+	kernel_code->compute_pgm_rsrc2.enable_sgpr_workgroup_info = 0;
+	kernel_code->compute_pgm_rsrc2.enable_vgpr_workitem_id = 1;
+	kernel_code->compute_pgm_rsrc2.enable_exception_address_watch = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_memory = 0;
+	kernel_code->compute_pgm_rsrc2.granulated_lds_size = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_invalid_operation = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_fp_denormal_source = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_division_by_zero = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_overflow = 0;
+	kernel_code->compute_pgm_rsrc2
+		.enable_exception_ieee_754_fp_underflow = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_ieee_754_fp_inexact = 0;
+	kernel_code->compute_pgm_rsrc2.enable_exception_int_divide_by_zero = 0;
+	kernel_code->compute_pgm_rsrc2.reserved0 = 0;
+
+	debug_kernel_descriptor(kernel_code);
+}
+
+static int kfd_kernel_init(struct knod *knod, struct knod_bpf_priv *priv)
+{
+	struct kernel_descriptor *kd;
+
+	if (!knod->kernels[1])
+		return -ENOMEM;
+
+	/*
+	 * Pass-through starts on slot 0; the first XDP prog attach stages into
+	 * slot 1 and flips the active index there, ping-ponging on each
+	 * install.
+	 */
+	priv->active_idx = 0;
+
+	if (priv->isa_version == 9)
+		kfd_kernel_gfx9_init(knod);
+	else if (priv->isa_version == 10)
+		kfd_kernel_gfx10_init(knod);
+
+	/*
+	 * Slot 1 must carry the same kernel-descriptor as slot 0 -- gfx init
+	 * only touches slot 0, and slot 1's BO is otherwise uninitialised,
+	 * which stalls the compute queue.  Copy the kd + pre-code region.
+	 */
+	kd = knod->kernels[0]->kaddr;
+	memcpy(knod->kernels[1]->kaddr, knod->kernels[0]->kaddr,
+	       kd->kernel_code_entry_byte_offset);
+	knod_bpf_gpu_mem_fence(priv);
+
+	return 0;
+}
+
+static struct knod_bpf_work_sq *
+__knod_get_free_work_sq(struct knod_bpf_priv *priv)
+{
+	return list_first_entry_or_null(&priv->free_list_sqw,
+					struct knod_bpf_work_sq, list);
+}
+
+/* Prepare a dispatch: peek SPSC rings and fill params, but do not submit.
+ * Returns the prepared sqw (with backlogs > 0), or NULL if nothing to do.
+ *
+ * A single in-flight AQL queue means the worker never has to reserve SPSC
+ * ranges ahead of the current dispatch. The SPSC acquired pointer is advanced
+ * only after the GPU finishes the dispatch that consumed those entries.
+ */
+static struct knod_bpf_work_sq *knod_prepare_bpf(struct knod_bpf_priv *priv)
+{
+	int i, cnt, backlogs = 0;
+	struct knod_dev *knodev = priv->knodev;
+	struct knod_bpf_work_sq *sqw;
+	struct knod_bpf_param *param;
+
+	if (READ_ONCE(priv->installing_kernel))
+		return NULL;
+
+	if (!priv->pass_prog_buf && !READ_ONCE(priv->prog))
+		return NULL;
+
+	sqw = __knod_get_free_work_sq(priv);
+	if (!sqw)
+		return NULL;
+
+	param = (struct knod_bpf_param *)sqw->param->kaddr;
+	memset(sqw->queue_idx, 0, sizeof(sqw->queue_idx));
+
+	/* 2D dispatch: queue_id = workgroup_id_y, tid = workitem within WG.
+	 * Per-queue bds live in sqw->bds[i * batch_size + tid] and shader
+	 * indexes sub[] / sqw->bds[] using (queue_id * batch_size + tid).
+	 * No cumulative start_idx -- each queue's slot range is fixed by i.
+	 */
+	for (i = 0; i < priv->nr_works; i++) {
+		int slot = i * priv->batch_size;
+		unsigned int skip = 0, j;
+
+		/* Stage past every in-flight dispatch's claim on this queue so
+		 * the new sqw reads disjoint SPSC slots.  Peek self-limits: if
+		 * the ring holds fewer entries past @skip, cnt shrinks (or 0).
+		 */
+		for (j = 0; j < priv->inflight_cnt; j++)
+			skip += priv->inflight[j]->queue_idx[i];
+
+		param->queues[i].count = 0;
+		spsc_peek_at(&knodev->wpriv[i].spsc_bds, skip,
+			     (void **)&sqw->bds[slot],
+			     priv->batch_size, &cnt);
+		if (!cnt) {
+			sqw->queue_idx[i] = 0;
+			param->queues[i].count = 0;
+			continue;
+		}
+
+		/* Fill queue descriptor for GPU direct SPSC read.
+		 * ring_start is the absolute ring position where this sqw
+		 * begins - shader reads slots[(ring_start + tid) & mask].
+		 * Offset by skip to keep staged sqws disjoint.
+		 */
+		param->queues[i].pool_gaddr = knodev->wpriv[i].spsc_pool_gaddr;
+		param->queues[i].base_gaddr = priv->queue_base_gaddr[i];
+		param->queues[i].count = cnt;
+		param->queues[i].ring_start =
+			knodev->wpriv[i].spsc_bds.acquired + skip;
+		param->queues[i].ring_mask =
+			knodev->wpriv[i].spsc_bds.mask;
+
+		backlogs += cnt;
+		sqw->queue_idx[i] = cnt;
+	}
+	sqw->backlogs = backlogs;
+	param->nr_backlogs = backlogs;
+	param->nr_queues = priv->nr_works;
+	param->spsc_stride = ALIGN(sizeof(struct spsc_bd), SMP_CACHE_BYTES);
+	for (i = 0; i < priv->nr_works; i++) {
+		param->pass_count[i] = 0;
+		param->pass_meta_buf_gaddr[i] = priv->pass_meta_buf ?
+			priv->pass_meta_buf->gaddr +
+			(u64)i * priv->pass_pkts_per_queue *
+			KNOD_PASS_SLOT_SIZE :
+			0;
+	}
+	param->ktime_ns = ktime_get_ns();
+
+	if (!sqw->backlogs)
+		return NULL;
+
+	list_del_init(&sqw->list);
+	return sqw;
+}
+
+/* Submit a prepared sqw: write AQL packet, ring doorbell, record stats. */
+static void knod_submit_bpf(struct knod_bpf_priv *priv,
+			     struct knod_bpf_work_sq *sqw)
+{
+	struct amd_signal *signal =
+		(struct amd_signal *)priv->knod->kaql[0].queue_signal->kaddr;
+	struct knod_bpf_stats *stats = &priv->stats;
+	struct knod_dispatch_params p;
+	int i, bucket = KNOD_BL_BUCKETS - 1;
+
+	/* The @inflight_cnt dispatches already in flight decrement the signal
+	 * before this one, so this sqw completes when the signal drops below
+	 * (current value - inflight_cnt).
+	 */
+	sqw->sigval = signal->value - priv->inflight_cnt;
+	sqw->expire = jiffies + msecs_to_jiffies(knod_bpf_expire);
+	if (static_branch_unlikely(&knod_stats_key)) {
+		sqw->dispatch_time = ktime_get();
+
+		stats->backlogs_total += sqw->backlogs;
+		for (i = 0; i < KNOD_BL_BUCKETS - 1; i++) {
+			if (sqw->backlogs <= bl_bounds[i]) {
+				bucket = i;
+				break;
+			}
+		}
+		stats->backlogs_hist[bucket]++;
+	}
+
+	knod_bpf_fill_dispatch(priv, sqw, &p);
+	/* publish dispatch params before the AQL packet becomes visible */
+	wmb();
+	knod_setup_header(priv->knod, &p, 0);
+}
+
+/* Phase 1: advance SPSC consumer pointers so next dispatch can peek
+ * new entries.
+ */
+static void knod_complete_acquire(struct knod_bpf_priv *priv,
+				  struct knod_bpf_work_sq *sqw)
+{
+	struct knod_dev *knodev = priv->knodev;
+	int i;
+
+	for (i = 0; i < priv->nr_works; i++) {
+		if (sqw->queue_idx[i] >= 1) {
+			spsc_acquire(&knodev->wpriv[i].spsc_bds, NULL,
+				     sqw->queue_idx[i], NULL);
+		}
+	}
+}
+
+/* Phase 2: schedule NAPI and free sqw.  Can run after the next dispatch
+ * has been submitted - napi_schedule overlaps with GPU execution.
+ */
+static void knod_complete_napi(struct knod_bpf_priv *priv,
+			       struct knod_bpf_work_sq *sqw)
+{
+	struct knod_dev *knodev = priv->knodev;
+	struct knod_bpf_stats *stats = &priv->stats;
+	ktime_t start;
+	int i;
+
+	if (static_branch_unlikely(&knod_stats_key))
+		start = ktime_get();
+
+	for (i = 0; i < priv->nr_works; i++) {
+		if (sqw->queue_idx[i] >= 1)
+			knod_napi_kick(&knodev->wpriv[i]);
+	}
+
+	sqw->backlogs = 0;
+	sqw->expire = 0;
+	list_add_tail_rcu(&sqw->list, &priv->free_list_sqw);
+
+	if (static_branch_unlikely(&knod_stats_key)) {
+		u64 ns = ktime_to_ns(ktime_sub(ktime_get(), start));
+
+		stats->decode_act_total_ns += ns;
+		stats->decode_act_count++;
+		if (ns > stats->decode_act_max_ns)
+			stats->decode_act_max_ns = ns;
+	}
+}
+
+static void knod_bpf_update_kernel_descriptor(struct knod_bpf_priv *priv,
+					      struct kernel_descriptor *kd,
+					      const struct knod_prog *knod_prog)
+{
+	unsigned int sgprs_used;
+
+	if (priv->isa_version != 9 || !knod_prog)
+		return;
+
+	sgprs_used = knod_prog->initial_exec_sreg + 2;
+	kd->compute_pgm_rsrc1.granulated_wavefront_sgpr_count =
+		knod_bpf_gfx9_sgpr_granule(sgprs_used);
+}
+
+/*
+ * Install kernel code into the inactive slot and atomically flip the active
+ * index.  The active slot is never modified while the GPU dispatches it, so
+ * the swap never races the live pipeline, and the worker is not touched: new
+ * dispatches pick up the new slot, the in-flight one finishes on the old slot.
+ */
+static void knod_bpf_install_kernel(struct knod_bpf_priv *priv,
+				    const struct knod_prog *knod_prog,
+				    const void *code, u32 size)
+{
+	struct kernel_descriptor *kd;
+	struct knod *knod = priv->knod;
+	struct knod_mem *slot;
+	u32 entry_off;
+	u32 image_len;
+	int idx;
+
+	if (!code || !size || !knod->kernels[1])
+		return;
+
+	/*
+	 * Before the worker runs, install in place; once it is dispatching,
+	 * stage into the inactive slot and flip the active index so the live
+	 * pipeline never reads a half-written slot.
+	 */
+	if (!priv->start || !knod->worker)
+		idx = priv->active_idx;
+	else
+		idx = priv->active_idx ^ 1;
+
+	slot = knod->kernels[idx];
+	kd = slot->kaddr;
+	knod_bpf_update_kernel_descriptor(priv, kd, knod_prog);
+	entry_off = kd->kernel_code_entry_byte_offset;
+	if (WARN_ON(entry_off >= slot->size))
+		return;
+	if (WARN_ON(size > slot->size - entry_off))
+		size = slot->size - entry_off;
+	image_len = entry_off + size;
+
+	memcpy(slot->kaddr + entry_off, code, size);
+	if (image_len < slot->size) {
+		u32 clear_end = min_t(u32, slot->size,
+					  entry_off + KNOD_BPF_PROG_BUF_SIZE);
+
+		if (image_len < clear_end)
+			memset(slot->kaddr + image_len, 0,
+			       clear_end - image_len);
+	}
+	/*
+	 * kernels[] is write-combining VRAM.  smp_wmb() is only a compiler
+	 * barrier on x86 and does NOT drain the WC buffers, so the GPU could
+	 * fetch half-written code and spin.  wmb() (sfence) flushes WC to VRAM
+	 * before we publish the new slot; the dispatch doorbell is ordered
+	 * behind it.
+	 */
+	wmb();
+	knod_bpf_gpu_mem_fence(priv);
+	WRITE_ONCE(priv->kernel_image_len[idx], image_len);
+
+	if (idx != priv->active_idx)
+		WRITE_ONCE(priv->active_idx, idx);
+}
+
+/*
+ * Keep the just-built pass-kernel IR for the debugfs "insn" dump, so it can
+ * show the pass-through kernel when no XDP prog is attached.  The machine code
+ * already lives in the kernel slot; this only retains the meta list.  Rebuilt
+ * on every start (old metas freed first), released in knod_priv_exit().
+ */
+static void knod_bpf_retain_pass_ir(struct knod_bpf_priv *priv,
+				    struct knod_prog *src)
+{
+	struct knod_insn_meta *meta, *tmp;
+	struct knod_prog *dst = priv->pass_knod_prog;
+
+	if (!dst) {
+		dst = kzalloc_obj(*dst, GFP_KERNEL);
+		if (!dst)
+			return;
+		INIT_LIST_HEAD(&dst->pre_insns);
+		INIT_LIST_HEAD(&dst->insns);
+		INIT_LIST_HEAD(&dst->post_insns);
+		priv->pass_knod_prog = dst;
+	} else {
+		list_for_each_entry_safe(meta, tmp, &dst->pre_insns, l) {
+			list_del(&meta->l);
+			kfree(meta);
+		}
+		list_for_each_entry_safe(meta, tmp, &dst->insns, l) {
+			list_del(&meta->l);
+			kfree(meta);
+		}
+		list_for_each_entry_safe(meta, tmp, &dst->post_insns, l) {
+			list_del(&meta->l);
+			kfree(meta);
+		}
+	}
+	list_splice_init(&src->pre_insns, &dst->pre_insns);
+	list_splice_init(&src->insns, &dst->insns);
+	list_splice_init(&src->post_insns, &dst->post_insns);
+}
+
+static void knod_bpf_layout_sregs(struct knod_bpf_priv *priv,
+				  struct knod_prog *knod_prog)
+{
+	if (priv->isa_version != 9)
+		return;
+
+	knod_prog->initial_exec_sreg =
+		knod_prog->exec_save_base + knod_prog->exec_save_pairs_used * 2;
+}
+
+static int knod_bpf_jit_pass_kernel(struct knod_bpf_priv *priv)
+{
+	struct list_head *lists[2];
+	struct knod_insn_meta *meta, *tmp, *epi;
+	struct amdgcn_param32 p[3];
+	struct knod *knod = priv->knod;
+	struct knod_prog pass_prog;
+	int pass_branch_idx;
+	u32 pass_dwords;
+	u8 *buf, *ptr;
+	u32 total = 0;
+	int i, j, li, err;
+
+	memset(&pass_prog, 0, sizeof(pass_prog));
+	INIT_LIST_HEAD(&pass_prog.pre_insns);
+	INIT_LIST_HEAD(&pass_prog.insns);
+	INIT_LIST_HEAD(&pass_prog.post_insns);
+	pass_prog.knod = knod;
+	pass_prog.knodev = priv->knodev;
+	if (priv->isa_version == 10) {
+		pass_prog.done_mask_sreg = 32;
+		pass_prog.exec_save_base = 34;
+		pass_prog.initial_exec_sreg =
+			KNOD_AMDGPU_INITIAL_EXEC_SREG_GFX10;
+	} else {
+		pass_prog.done_mask_sreg = KNOD_AMDGPU_DONE_MASK_SREG;
+		pass_prog.exec_save_base = KNOD_AMDGPU_EXEC_SAVE_SREG_BASE;
+		pass_prog.initial_exec_sreg =
+			KNOD_AMDGPU_INITIAL_EXEC_SREG_GFX9;
+	}
+
+	knod_bpf_layout_sregs(priv, &pass_prog);
+	err = knod_prog_prepare_insns(priv, &pass_prog);
+	if (err)
+		return err;
+
+	epi = kzalloc_obj(*epi, GFP_KERNEL);
+	if (!epi) {
+		err = -ENOMEM;
+		goto free_pro;
+	}
+
+	/* BPF/XDP actions are 32-bit values; mlx5 consumes bd->act as low32. */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG0_LO);
+	knod_iset32(&p[1], XDP_PASS);
+	knod_emit(priv, epi, v_mov_b32_e32, p[0], p[1]);
+
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG0_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_SLOT_VREG_LO);
+	knod_emit(priv, epi, global_store_dword, p[0], p[1],
+		  offsetof(struct spsc_bd, act));
+
+	/* XDP_PASS detection: v_cmp_eq_u32 XDP_PASS, R0 -> VCC */
+	knod_iset32(&p[0], XDP_PASS);
+	knod_vset32(&p[1], KNOD_AMDGPU_VREG0_LO);
+	knod_emit(priv, epi, v_cmp_eq_u32, p[0], p[1]);
+
+	pass_branch_idx = epi->amdgpu_insns;
+	knod_emit(priv, epi, s_cbranch_vccz, 0);
+
+	/* EXEC &= VCC - only PASS lanes proceed */
+	knod_emit(priv, epi, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+
+	/* v_mov param addr to VGPR pair for pass_count atomic */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_sset32(&p[1], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_emit(priv, epi, v_mov_b32_e32, p[0], p[1]);
+
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_HI);
+	knod_sset32(&p[1], KNOD_AMDGPU_PARAM_SREG_HI);
+	knod_emit(priv, epi, v_mov_b32_e32, p[0], p[1]);
+
+	/* v_mov v2, s15 (queue_idx -> VGPR) */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG1_LO);
+	knod_sset32(&p[1], KNOD_AMDGPU_WORKGROUP_ID_Y_SREG);
+	knod_emit(priv, epi, v_mov_b32_e32, p[0], p[1]);
+
+	/* v_lshlrev_b32 v2, 2, v2 (queue_idx * 4) */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG1_LO);
+	knod_iset32(&p[1], 2);
+	knod_vset32(&p[2], KNOD_AMDGPU_VREG1_LO);
+	knod_emit(priv, epi, v_lshlrev_b32, p[0], p[1], p[2]);
+
+	/* v_add_u32 TMP9_LO, v2, TMP9_LO (param_addr += queue_idx * 4) */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_VREG1_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_emit(priv, epi, v_add_u32, p[0], p[1], p[2]);
+
+	/* v_mov TMP10_LO, 1 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_iset32(&p[1], 1);
+	knod_emit(priv, epi, v_mov_b32_e32, p[0], p[1]);
+
+	/* global_atomic_add pass_count[q]++, GLC=1 -> old_val in TMP10_LO */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_emit(priv, epi, global_atomic_add, p[0], p[1], p[2],
+		  offsetof(struct knod_bpf_param, pass_count), 1);
+
+	/* s_waitcnt vmcnt(0) */
+	knod_emit(priv, epi, s_waitcnt_vmcnt);
+
+	/* v_sub_u32 TMP9_LO, TMP9_LO, v2 (restore param_addr_lo) */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_VREG1_LO);
+	knod_emit(priv, epi, v_sub_u32, p[0], p[1], p[2]);
+
+	/* old_val * 2 for pass_indices u16 stride */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_iset32(&p[1], 1);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_emit(priv, epi, v_lshlrev_b32, p[0], p[1], p[2]);
+
+	/* addr_lo += old_val * 2 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_emit(priv, epi, v_add_u32, p[0], p[1], p[2]);
+
+	/* global_store_short pass_indices[old_val], BACKLOG_IDX_VREG */
+	knod_vset32(&p[0], KNOD_AMDGPU_BACKLOG_IDX_VREG);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_emit(priv, epi, global_store_short, p[0], p[1],
+		  offsetof(struct knod_bpf_param, pass_indices));
+
+	/* Store len + src_addr to pass_meta_buf slot header */
+	knod_emit_pass_addr_store(priv, epi);
+
+	/* Patch s_cbranch_vccz offset (skip pass handling) */
+	pass_dwords = 0;
+	for (j = pass_branch_idx + 1; j < epi->amdgpu_insns; j++)
+		pass_dwords += epi->amdgpu_insn[j].size / 4;
+	emit_s_cbranch_vccz(priv->isa_version,
+			    &epi->amdgpu_insn[pass_branch_idx], pass_dwords);
+
+	knod_emit(priv, epi, s_endpgm);
+
+	if (priv->isa_version >= 10) {
+		for (j = 0; j < 16 && epi->amdgpu_insns < KNOD_META_INSNS; j++)
+			knod_emit(priv, epi, s_code_end);
+	}
+	list_add_tail(&epi->l, &pass_prog.post_insns);
+
+	/* Linearize prologue + epilogue into pass_prog_buf */
+	lists[0] = &pass_prog.pre_insns;
+	lists[1] = &pass_prog.post_insns;
+
+	for (li = 0; li < 2; li++) {
+		list_for_each_entry(meta, lists[li], l)
+			for (i = 0; i < meta->amdgpu_insns; i++)
+				total += meta->amdgpu_insn[i].size;
+	}
+
+	kfree(priv->pass_prog_buf);
+	buf = kzalloc(total, GFP_KERNEL);
+	if (!buf) {
+		err = -ENOMEM;
+		goto free_all;
+	}
+
+	ptr = buf;
+	for (li = 0; li < 2; li++) {
+		list_for_each_entry(meta, lists[li], l)
+			for (i = 0; i < meta->amdgpu_insns; i++) {
+				memcpy(ptr, &meta->amdgpu_insn[i],
+				       meta->amdgpu_insn[i].size);
+				ptr += meta->amdgpu_insn[i].size;
+			}
+	}
+
+	priv->pass_prog_buf = buf;
+	priv->pass_prog_size = total;
+
+	knod_bpf_install_kernel(priv, &pass_prog, priv->pass_prog_buf,
+				priv->pass_prog_size);
+	/* Remember which slot now holds pass so detach can flip back to it. */
+	priv->pass_idx = priv->active_idx;
+
+	pr_info("knod_bpf: pass kernel JIT'd %u bytes\n", priv->pass_prog_size);
+	err = 0;
+	/* Retain the IR (moves the lists out) before the cleanup below
+	 * frees.
+	 */
+	knod_bpf_retain_pass_ir(priv, &pass_prog);
+
+free_all:
+	list_for_each_entry_safe(meta, tmp, &pass_prog.post_insns, l) {
+		list_del_init(&meta->l);
+		kfree(meta);
+	}
+free_pro:
+	list_for_each_entry_safe(meta, tmp, &pass_prog.pre_insns, l) {
+		list_del_init(&meta->l);
+		kfree(meta);
+	}
+	return err;
+}
+
+static void knod_bpf_reset_sqw(struct knod_bpf_work_sq *sqw)
+{
+	if (!sqw)
+		return;
+
+	sqw->backlogs = 0;
+	sqw->expire = 0;
+}
+
+static void knod_bpf_wait_sqw(struct knod_bpf_priv *priv,
+			      struct knod_bpf_work_sq *sqw)
+{
+	struct amd_signal *signal;
+	unsigned long deadline;
+
+	if (!sqw)
+		return;
+
+	signal = (struct amd_signal *)
+		priv->knod->kaql[0].queue_signal->kaddr;
+	deadline = jiffies + msecs_to_jiffies(1000);
+
+	while (sqw->sigval <= READ_ONCE(signal->value) &&
+	       time_before(jiffies, deadline))
+		usleep_range(100, 200);
+
+	if (sqw->sigval <= READ_ONCE(signal->value))
+		pr_warn("knod: timed out waiting for GPU dispatch completion\n");
+}
+
+static void knod_bpf_drain_worker(struct knod_bpf_priv *priv)
+{
+	struct knod_bpf_work_sq *sqw;
+
+	/* stop() runs on interface-down AND on every feature switch, both
+	 * with mlx5 RX possibly still producing into knodev->wpriv[].spsc_bds.
+	 * So we only quiesce the GPU here; the NIC-owned RX SPSC rings are
+	 * drained on interface-down by mlx5e_rx_offload_stop().
+	 */
+	while (priv->inflight_cnt) {
+		sqw = priv->inflight[--priv->inflight_cnt];
+		priv->inflight[priv->inflight_cnt] = NULL;
+		knod_bpf_wait_sqw(priv, sqw);
+		knod_bpf_reset_sqw(sqw);
+		list_add_tail_rcu(&sqw->list, &priv->free_list_sqw);
+	}
+}
+
+static void knod_bpf_drain(struct knod_bpf_priv *priv)
+{
+	knod_bpf_drain_worker(priv);
+}
+
+static void knod_bpf_stop_worker(struct knod_bpf_priv *priv)
+{
+	priv->start = 0;
+	if (priv->worker_task) {
+		kthread_stop(priv->worker_task);
+		put_task_struct(priv->worker_task);
+		priv->worker_task = NULL;
+	}
+	synchronize_net();
+}
+
+static void knod_bpf_configure_worker(struct knod_bpf_priv *priv)
+{
+	knod_bpf_stop_worker(priv);
+	knod_bpf_drain(priv);
+
+	priv->inflight_cnt = 0;
+}
+
+static int knod_bpf_start_worker(struct knod_bpf_priv *priv)
+{
+	struct task_struct *p;
+
+	p = kthread_run(knod_bpf_worker, priv, "knod_%d_0",
+			priv->knodev->accel->id);
+	if (IS_ERR(p))
+		return PTR_ERR(p);
+
+	get_task_struct(p);
+	priv->worker_task = p;
+	return 0;
+}
+
+static bool knod_bpf_uses_percpu(struct knod_bpf_priv *priv)
+{
+	struct knod_bpf_map *knod_map;
+
+	list_for_each_entry(knod_map, &priv->knodev->accel->xdp.bound_maps,
+			    list)
+		if (knod_map->knod_map_obj->map_type ==
+		    BPF_MAP_TYPE_PERCPU_ARRAY)
+			return true;
+	return false;
+}
+
+/* Fan out one workgroup per CU (rounded down to a power of two).  PERCPU maps
+ * keep one instance per RX queue, so a queue must stay on a single workgroup;
+ * force xgroups=1 when the program uses them.  Non-PERCPU maps are globally
+ * shared with atomics, so fan-out is safe there.  This replaces the old manual
+ * xgroups knob.
+ */
+static unsigned int knod_bpf_auto_xgroups(struct knod_bpf_priv *priv)
+{
+	struct amdgpu_device *adev = NULL;
+	unsigned int cus, xgroups;
+
+	if (knod_bpf_uses_percpu(priv))
+		return 1;
+
+	if (priv->knod->dev)
+		adev = priv->knod->dev->adev;
+	else if (priv->knod->process && priv->knod->process->pdds[0])
+		adev = priv->knod->process->pdds[0]->dev->adev;
+	if (!adev || !priv->nr_works)
+		return 1;
+
+	cus = adev->gfx.cu_info.number;
+	xgroups = cus / priv->nr_works;
+	if (!xgroups)
+		xgroups = 1;
+	return rounddown_pow_of_two(xgroups);
+}
+
+/* Per-queue dispatch batch = workgroups * xgroups packets, capped by the
+ * static descriptor array and rounded down to a power of two (the shader
+ * derives the flat slot as queue_id << ilog2(batch_size) + local_idx).
+ */
+static unsigned int knod_bpf_batch_size(struct knod_bpf_priv *priv)
+{
+	unsigned int xgroups = knod_bpf_auto_xgroups(priv);
+	unsigned int max_flat = KNOD_BPF_BACKLOGS_MAX / priv->nr_works;
+	unsigned int batch = min_t(unsigned int,
+				   knod_bpf_workgroups * xgroups, max_flat);
+
+	if (!batch)
+		batch = knod_bpf_workgroups;
+	return rounddown_pow_of_two(batch);
+}
+
+static void knod_bpf_start(struct knod_dev *knodev)
+{
+	struct knod_bpf_priv *priv =
+		(struct knod_bpf_priv *)knodev->accel->xdp.priv;
+	struct bpf_prog *prog;
+	unsigned int active_rxq;
+	int err;
+
+	priv->start = 1;
+	active_rxq = knod_bpf_active_rxq_count(knodev->netdev);
+	if (active_rxq && active_rxq != priv->nr_works)
+		pr_warn("knod_bpf: active rx queues changed from %d to %u; using initialized count\n",
+			priv->nr_works, active_rxq);
+
+	priv->batch_size = knod_bpf_batch_size(priv);
+
+	knod_jit_dbg(" batch_size = %d\n", priv->batch_size);
+	knod_bpf_configure_worker(priv);
+	pr_info("knod_bpf: using single AQL queue, rx_works=%d active_rxq=%u batch_size=%d xgroups=%u\n",
+		priv->nr_works, active_rxq, priv->batch_size,
+		priv->batch_size / knod_bpf_workgroups);
+
+	if (knod_bpf_jit_pass_kernel(priv))
+		pr_warn("knod_bpf: pass kernel JIT failed\n");
+
+	prog = READ_ONCE(priv->prog);
+	if (prog)
+		knod_setup_bpf_prog(prog);
+
+	priv->start = 1;
+	err = knod_bpf_start_worker(priv);
+	if (err) {
+		pr_err("knod_bpf: start_worker failed: %d\n", err);
+		priv->start = 0;
+		return;
+	}
+}
+
+static void knod_bpf_stop(struct knod_dev *knodev)
+{
+	struct knod_bpf_priv *priv =
+		(struct knod_bpf_priv *)knodev->accel->xdp.priv;
+
+	knod_bpf_stop_worker(priv);
+	knod_bpf_drain(priv);
+
+	kfree(priv->pass_prog_buf);
+	priv->pass_prog_buf = NULL;
+	priv->pass_prog_size = 0;
+}
+
+/*
+ * Flip the dispatched kernel back to pass-through when the XDP prog is
+ * detached.  The pass slot already holds the pass code, so this is just an
+ * atomic index flip -- no re-copy.
+ */
+static void knod_bpf_reload_pass(struct knod_dev *knodev)
+{
+	struct knod_bpf_priv *priv = knodev->accel->xdp.priv;
+
+	if (priv)
+		WRITE_ONCE(priv->active_idx, priv->pass_idx);
+}
+
+static void knod_setup_bpf_prog(struct bpf_prog *prog)
+{
+	struct knod_prog *knod_prog = prog->aux->offload->dev_priv;
+	struct knod_dev *knodev = knod_prog->knodev;
+	struct knod_insn_meta *meta, *tmp;
+	struct knod_bpf_priv *priv;
+	u8 *kernel_ptr, *ptr;
+	u32 total_bytes;
+	u32 *debug_ptr;
+	int i;
+
+	priv = (struct knod_bpf_priv *)knodev->accel->xdp.priv;
+	WRITE_ONCE(priv->installing_kernel, true);
+
+	if (prog) {
+		WRITE_ONCE(priv->prog, NULL);
+		kernel_ptr = priv->prog_buf;
+		memset(priv->prog_buf, 0, KNOD_BPF_PROG_BUF_SIZE);
+
+		list_for_each_entry(meta, &priv->knod_prog->pre_insns, l) {
+			for (i = 0; i < meta->amdgpu_insns; i++) {
+				ptr = (u8 *)&meta->amdgpu_insn[i];
+				debug_ptr = (u32 *)ptr;
+
+				memcpy(kernel_ptr, ptr,
+				       meta->amdgpu_insn[i].size);
+				kernel_ptr += meta->amdgpu_insn[i].size;
+
+				if (meta->amdgpu_insn[i].size == 4) {
+					knod_jit_dbg(" 0x%.8X\t%.8X\n",
+						meta->amdgpu_insn_idx,
+						debug_ptr[0]);
+				} else if (meta->amdgpu_insn[i].size == 8) {
+					knod_jit_dbg(" 0x%.8X\t%.8X %.8X\n",
+						meta->amdgpu_insn_idx,
+						debug_ptr[0], debug_ptr[1]);
+				} else if (meta->amdgpu_insn[i].size == 12) {
+					knod_jit_dbg(" 0x%.8X\t%.8X %.8X %.8X\n",
+						meta->amdgpu_insn_idx,
+						debug_ptr[0],
+						debug_ptr[1], debug_ptr[2]);
+				} else {
+					WARN_ON_ONCE(1);
+				}
+			}
+		}
+
+		list_for_each_entry(meta, &priv->knod_prog->insns, l) {
+			for (i = 0; i < meta->amdgpu_insns; i++) {
+				ptr = (u8 *)&meta->amdgpu_insn[i];
+				debug_ptr = (u32 *)ptr;
+
+				memcpy(kernel_ptr, ptr,
+				       meta->amdgpu_insn[i].size);
+				kernel_ptr += meta->amdgpu_insn[i].size;
+				if (meta->amdgpu_insn[i].size == 4) {
+					knod_jit_dbg(" 0x%.8X\t%.8X\n",
+						meta->amdgpu_insn_idx,
+						debug_ptr[0]);
+				} else if (meta->amdgpu_insn[i].size == 8) {
+					knod_jit_dbg(" 0x%.8X\t%.8X %.8X\n",
+						meta->amdgpu_insn_idx,
+						debug_ptr[0], debug_ptr[1]);
+				} else if (meta->amdgpu_insn[i].size == 12) {
+					knod_jit_dbg(" 0x%.8X\t%.8X %.8X %.8X\n",
+						meta->amdgpu_insn_idx,
+						debug_ptr[0],
+						debug_ptr[1], debug_ptr[2]);
+				} else {
+					WARN_ON_ONCE(1);
+				}
+			}
+		}
+
+		list_for_each_entry(meta, &priv->knod_prog->post_insns, l) {
+			for (i = 0; i < meta->amdgpu_insns; i++) {
+				ptr = (u8 *)&meta->amdgpu_insn[i];
+				debug_ptr = (u32 *)ptr;
+
+				memcpy(kernel_ptr, ptr,
+				       meta->amdgpu_insn[i].size);
+				kernel_ptr += meta->amdgpu_insn[i].size;
+				if (meta->amdgpu_insn[i].size == 4) {
+					knod_jit_dbg(" %.8X\n", debug_ptr[0]);
+				} else if (meta->amdgpu_insn[i].size == 8) {
+					knod_jit_dbg(" %.8X %.8X\n",
+						debug_ptr[0], debug_ptr[1]);
+				} else if (meta->amdgpu_insn[i].size == 12) {
+					knod_jit_dbg(" %.8X %.8X %.8X\n",
+						debug_ptr[0],
+						debug_ptr[1], debug_ptr[2]);
+				} else {
+					WARN_ON_ONCE(1);
+				}
+			}
+		}
+		total_bytes = kernel_ptr - (u8 *)priv->prog_buf;
+
+		pr_debug("KNOD JIT: total binary size = %u bytes (limit %u)\n",
+			 total_bytes, KNOD_BPF_PROG_BUF_SIZE);
+		if (WARN_ON(total_bytes > KNOD_BPF_PROG_BUF_SIZE))
+			total_bytes = KNOD_BPF_PROG_BUF_SIZE;
+		knod_bpf_install_kernel(priv, knod_prog, priv->prog_buf,
+					total_bytes);
+		WRITE_ONCE(priv->prog, prog);
+	} else {
+		WRITE_ONCE(priv->prog, NULL);
+		list_for_each_entry_safe(meta, tmp, &priv->knod_prog->pre_insns,
+					 l) {
+			list_del_init(&meta->l);
+			kfree(meta);
+		}
+
+		list_for_each_entry_safe(meta, tmp, &priv->knod_prog->insns,
+					 l) {
+			list_del_init(&meta->l);
+			kfree(meta);
+		}
+
+		list_for_each_entry_safe(meta, tmp,
+					 &priv->knod_prog->post_insns, l) {
+			list_del_init(&meta->l);
+			kfree(meta);
+		}
+
+		/* bbs points into the metas just freed */
+		kfree(priv->knod_prog->bbs);
+		priv->knod_prog->bbs = NULL;
+		priv->knod_prog->n_bbs = 0;
+
+		if (priv->pass_prog_buf)
+			knod_bpf_install_kernel(priv, priv->pass_knod_prog,
+						priv->pass_prog_buf,
+						priv->pass_prog_size);
+	}
+	WRITE_ONCE(priv->installing_kernel, false);
+}
+
+static int knod_bpf_map_hash_init_elem(struct knod_bpf_map *knod_map,
+				       struct knod_bpf_map_obj *knod_map_obj)
+{
+	unsigned int *queue = (unsigned int *)knod_map->queue_mem->kaddr;
+	unsigned int *bucket = (unsigned int *)&knod_map_obj->bucket[0];
+	void *elems = knod_map->hash_elems_mem->kaddr;
+	struct knod_bpf_hash_elem_obj *e;
+	int i, elem_size;
+
+	elem_size = sizeof(struct knod_bpf_hash_elem_obj) +
+			   roundup(knod_map_obj->key_size, 4) +
+			   roundup(knod_map_obj->value_size, 4);
+	knod_map_obj->meta.hmeta.elem_size = elem_size;
+
+	for (i = 0; i < knod_map_obj->meta.hmeta.n_buckets; i++)
+		bucket[i] = KNOD_BPF_HASH_NEXT_END;
+
+	for (i = 0; i < knod_map_obj->max_entries; i++) {
+		e = elems + (i * elem_size);
+		e->next = KNOD_BPF_HASH_NEXT_END;
+		queue[i] = i;
+	}
+	knod_map_obj->meta.hmeta.cur = knod_map_obj->max_entries - 1;
+
+	return 0;
+}
+
+static inline unsigned char *
+knod_bpf_hash_elem_kv(struct knod_bpf_hash_elem_obj *e)
+{
+	return (unsigned char *)e + offsetof(struct knod_bpf_hash_elem_obj, kv);
+}
+
+static inline void *
+knod_bpf_array_value_ptr(struct knod_bpf_map_obj *knod_map_obj,
+			 unsigned int idx)
+{
+	return (unsigned char *)knod_map_obj +
+	       offsetof(struct knod_bpf_map_obj, bucket) +
+	       (size_t)idx * knod_map_obj->value_size;
+}
+
+static int __knod_bpf_map_alloc(struct knod_dev *knodev,
+				struct bpf_offloaded_map *offmap)
+{
+	struct knod_bpf_priv *priv =
+		(struct knod_bpf_priv *)knodev->accel->xdp.priv;
+	struct knod_mem *mem, *queue_mem, *hash_elems_mem, *gc_mem;
+	int order, size, queue_size, i, value_size, nents, err;
+	int n_instances = 1;
+	int flags = KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+		    KFD_IOC_ALLOC_MEM_FLAGS_COHERENT |
+		    KFD_IOC_ALLOC_MEM_FLAGS_PUBLIC |
+		    KFD_IOC_ALLOC_MEM_FLAGS_VRAM;
+	struct knod_bpf_map_obj *knod_map_obj;
+	struct knod *knod = priv->knod;
+	struct knod_bpf_map *knod_map;
+	unsigned int gc_size;
+	unsigned int *q;
+
+	if (offmap->map.map_type == BPF_MAP_TYPE_HASH) {
+		value_size = sizeof(unsigned int);
+		nents = roundup_pow_of_two(offmap->map.max_entries);
+	} else {
+		value_size = offmap->map.value_size;
+		nents = offmap->map.max_entries;
+	}
+
+	/* PERCPU_ARRAY keeps one value array per GPU workgroup (percpu
+	 * instance) so each CU updates its own copy - no cross-CU atomic
+	 * contention.  Instances map 1:1 to the per-cpu value buffer, so
+	 * allocate num_possible_cpus of them (workgroup_id_y indexes into it).
+	 */
+	if (offmap->map.map_type == BPF_MAP_TYPE_PERCPU_ARRAY)
+		n_instances = num_possible_cpus();
+
+	size = sizeof(struct knod_bpf_map_obj) +
+	       (value_size * nents * n_instances);
+	if (offmap->map.map_type == BPF_MAP_TYPE_HASH)
+		size += sizeof(unsigned int) * nents;
+	order = get_order(size);
+
+	mem = knod_alloc_mem(knod, PAGE_SIZE << order, flags);
+	if (IS_ERR(mem))
+		return -ENOMEM;
+
+	memset(mem->kaddr, 0, size);
+	knod_map = kzalloc_obj(struct knod_bpf_map, GFP_KERNEL);
+	if (!knod_map) {
+		knod_free_mem(knod, mem);
+		return -ENOMEM;
+	}
+
+	knod_map->mem = mem;
+	knod_map->queue_mem = NULL;
+	knod_map->hash_elems_mem = NULL;
+	knod_map->offmap = offmap;
+	knod_map->priv = priv;
+	if (offmap->dev_priv)
+		WARN_ON_ONCE(1);
+	offmap->dev_priv = knod_map;
+
+	knod_map_obj = (struct knod_bpf_map_obj *)mem->kaddr;
+	knod_map_obj->key_size = offmap->map.key_size;
+	if (knod_map_obj->key_size > MAX_MAP_KEY_SIZE) {
+		pr_warn("request key size is %d, but max key size is %d\n",
+			knod_map_obj->key_size, MAX_MAP_KEY_SIZE);
+		return -ENOMEM;
+	}
+	knod_map_obj->value_size = offmap->map.value_size;
+	knod_map_obj->max_entries = nents;
+	knod_map_obj->id = offmap->map.id;
+	knod_map_obj->map_type = offmap->map.map_type;
+	if (knod_map_obj->map_type == BPF_MAP_TYPE_HASH) {
+		knod_map_obj->meta.hmeta.n_buckets = nents;
+		if (offmap->map.map_flags & BPF_F_ZERO_SEED)
+			knod_map_obj->meta.hmeta.hashrnd = 0;
+		else
+			knod_map_obj->meta.hmeta.hashrnd = get_random_u32();
+	} else {
+		knod_map_obj->meta.ameta.per_instance_size = value_size * nents;
+		knod_map_obj->meta.ameta.n_instances = n_instances;
+	}
+	knod_map->knod_map_obj = knod_map_obj;
+	/* map->flags = ? */
+	knod_jit_dbg(" map_id = %d\n", knod_map_obj->id);
+
+	if (knod_map_obj->map_type == BPF_MAP_TYPE_HASH) {
+		queue_size = sizeof(unsigned int) * nents;
+		queue_size = PAGE_SIZE << get_order(queue_size);
+		queue_mem = knod_alloc_mem(knod, queue_size, flags);
+		if (IS_ERR(queue_mem)) {
+			knod_free_mem(knod, mem);
+			kfree(knod_map);
+			return -ENOMEM;
+		}
+
+		memset(queue_mem->kaddr, 0, queue_mem->size);
+		q = queue_mem->kaddr;
+		for (i = 0; i < knod_map_obj->meta.hmeta.n_buckets; i++)
+			q[i] = i;
+		knod_map->queue_mem = queue_mem;
+		knod_map_obj->meta.hmeta.q = (struct _queue *)queue_mem->gaddr;
+
+		queue_size = (sizeof(struct knod_bpf_hash_elem_obj) +
+			      roundup(knod_map_obj->key_size, 4) +
+			      roundup(knod_map_obj->value_size, 4)) *
+			      knod_map_obj->max_entries;
+		queue_size = PAGE_SIZE << get_order(queue_size);
+
+		hash_elems_mem = knod_alloc_mem(knod, queue_size, flags);
+		if (IS_ERR(hash_elems_mem)) {
+			knod_free_mem(knod, queue_mem);
+			knod_free_mem(knod, mem);
+			kfree(knod_map);
+			return -ENOMEM;
+		}
+
+		memset(hash_elems_mem->kaddr, 0, queue_size);
+		knod_map->hash_elems_mem = hash_elems_mem;
+		knod_map_obj->meta.hmeta.elems = (void *)hash_elems_mem->gaddr;
+		knod_bpf_map_hash_init_elem(knod_map, knod_map_obj);
+
+		/* GC list for GPU-side delete: elem_ids pending unlink */
+		gc_size = sizeof(unsigned int) * nents;
+		gc_size = PAGE_SIZE << get_order(gc_size);
+		gc_mem = knod_alloc_mem(knod, gc_size, flags);
+		if (IS_ERR(gc_mem)) {
+			knod_free_mem(knod, hash_elems_mem);
+			knod_free_mem(knod, queue_mem);
+			knod_free_mem(knod, mem);
+			kfree(knod_map);
+			return -ENOMEM;
+		}
+		memset(gc_mem->kaddr, 0, gc_size);
+		knod_map->gc_mem = gc_mem;
+		knod_map_obj->meta.hmeta.gc_count = 0;
+		knod_map_obj->meta.hmeta.gc_list = (void *)gc_mem->gaddr;
+	}
+
+	err = __knod_map_mem(knod, mem);
+	if (err) {
+		pr_err("knod_bpf: failed to GPU-map map BO\n");
+		goto err_map;
+	}
+	if (knod_map_obj->map_type == BPF_MAP_TYPE_HASH) {
+		err = __knod_map_mem(knod, queue_mem);
+		if (err) {
+			pr_err("knod_bpf: failed to GPU-map queue BO\n");
+			goto err_map;
+		}
+		err = __knod_map_mem(knod, hash_elems_mem);
+		if (err) {
+			pr_err("knod_bpf: failed to GPU-map hash_elems BO\n");
+			goto err_map;
+		}
+		err = __knod_map_mem(knod, knod_map->gc_mem);
+		if (err) {
+			pr_err("knod_bpf: failed to GPU-map gc BO\n");
+			goto err_map;
+		}
+	}
+	knod_bpf_gpu_mem_fence(priv);
+
+	mutex_lock(&knodev->lock);
+	list_add(&knod_map->list, &knodev->accel->xdp.bound_maps);
+	mutex_unlock(&knodev->lock);
+	return 0;
+
+err_map:
+	if (knod_map_obj->map_type == BPF_MAP_TYPE_HASH) {
+		knod_free_mem(knod, knod_map->gc_mem);
+		knod_free_mem(knod, hash_elems_mem);
+		knod_free_mem(knod, queue_mem);
+	}
+	knod_free_mem(knod, mem);
+	kfree(knod_map);
+	return err;
+}
+
+static void knod_bpf_map_setup(struct bpf_prog *prog)
+{
+	struct knod_prog *knod_prog = prog->aux->offload->dev_priv;
+	struct knod_dev *knodev = knod_prog->knodev;
+	struct knod_bpf_map *knod_map;
+	struct knod_bpf_map_obj *map;
+	struct knod_mem *mem;
+
+	mutex_lock(&knodev->lock);
+	list_for_each_entry(knod_map, &knodev->accel->xdp.bound_maps, list) {
+		mem = knod_map->mem;
+		map = mem->kaddr;
+		map->id = knod_map->offmap->map.id;
+		map->map_type = knod_map->offmap->map.map_type;
+		knod_jit_dbg(" id = %d type = %d\n", knod_map->offmap->map.id,
+			knod_map->offmap->map.map_type);
+	}
+	mutex_unlock(&knodev->lock);
+}
+
+static struct knod_bpf_hash_elem_obj *
+knod_bpf_map_hash_pop(struct knod_bpf_map *knod_map,
+		      struct knod_bpf_map_obj *knod_map_obj)
+{
+	void *elems = knod_map->hash_elems_mem->kaddr;
+	unsigned int *queue = (unsigned int *)knod_map->queue_mem->kaddr;
+	struct knod_bpf_hash_elem_obj *e;
+	int elem_id;
+
+	if (knod_map_obj->meta.hmeta.cur < 1)
+		return NULL;
+
+	elem_id = queue[knod_map_obj->meta.hmeta.cur];
+	knod_jit_dbg(" elem_id = 0x%x\n", elem_id);
+	e = elems + (elem_id * knod_map_obj->meta.hmeta.elem_size);
+	knod_map_obj->meta.hmeta.cur--;
+	e->next = KNOD_BPF_HASH_NEXT_END;
+
+	return e;
+}
+
+static struct knod_bpf_hash_elem_obj *
+knod_bpf_map_hash_alloc_elem(struct knod_bpf_map *knod_map,
+			     struct knod_bpf_map_obj *knod_map_obj,
+			     void *key, void *value)
+{
+	struct knod_bpf_hash_elem_obj *e;
+
+	e = knod_bpf_map_hash_pop(knod_map, knod_map_obj);
+	if (!e)
+		return NULL;
+
+	unsafe_memcpy(knod_bpf_hash_elem_kv(e), key, knod_map_obj->key_size,
+		      "knod hash elems are variable-sized GPU map records");
+	unsafe_memcpy(knod_bpf_hash_elem_kv(e) + knod_map_obj->key_size,
+		      value, knod_map_obj->value_size,
+		      "knod hash elems are variable-sized GPU map records");
+	/* VRAM is ioremap_wc - drain new elem's next and kv stores before
+	 * the caller publishes a pointer to this elem.
+	 */
+	wmb();
+	return e;
+}
+
+static int knod_bpf_map_hash_lookup_elem(struct knod_bpf_map *knod_map,
+					 struct knod_bpf_map_obj *knod_map_obj,
+					 void *key,
+					 void *value)
+{
+	void *elems = knod_map->hash_elems_mem->kaddr;
+	unsigned int hash, elem_id, elem_size;
+	struct knod_bpf_hash_elem_obj *e;
+	unsigned int *bucket;
+
+	hash = jhash((const void *)key, knod_map_obj->key_size,
+		     knod_map_obj->meta.hmeta.hashrnd);
+	knod_jit_dbg(" hash = %x\n", hash);
+	hash = hash & (knod_map_obj->meta.hmeta.n_buckets - 1);
+	knod_jit_dbg(" hash = %x\n", hash);
+	bucket = (unsigned int *)&knod_map_obj->bucket[0];
+
+	elem_id = bucket[hash];
+	if (elem_id == KNOD_BPF_HASH_NEXT_END)
+		return -ENOENT;
+
+	elem_size = knod_map_obj->meta.hmeta.elem_size;
+
+	e = elems + (elem_id * elem_size);
+	while (1) {
+		if (!(e->next & KNOD_BPF_HASH_NEXT_DELETED) &&
+		    !memcmp(&e->kv[0], (const unsigned char *)key,
+			    knod_map_obj->key_size)) {
+			memcpy(value,
+			       (unsigned char *)&e->kv[0] +
+			       knod_map_obj->key_size,
+			       knod_map_obj->value_size);
+			return 0;
+		}
+		unsigned int real_next = e->next & KNOD_BPF_HASH_NEXT_MASK;
+
+		if (real_next == KNOD_BPF_HASH_NEXT_END)
+			return -ENOENT;
+		e = elems + (real_next * elem_size);
+	}
+
+	return -ENOENT;
+}
+
+static int knod_bpf_map_hash_update_elem(struct knod_bpf_map *knod_map,
+					 struct knod_bpf_map_obj *knod_map_obj,
+					 void *key,
+					 void *value)
+{
+	void *elems = knod_map->hash_elems_mem->kaddr;
+	unsigned int hash, elem_id, elem_size;
+	struct knod_bpf_hash_elem_obj *e, *ne;
+	unsigned int *bucket;
+
+	hash = jhash((const void *)key, knod_map_obj->key_size,
+		     knod_map_obj->meta.hmeta.hashrnd);
+	hash = hash & (knod_map_obj->meta.hmeta.n_buckets - 1);
+	bucket = (unsigned int *)&knod_map_obj->bucket[0];
+
+	elem_size = knod_map_obj->meta.hmeta.elem_size;
+	elem_id = bucket[hash];
+	if (elem_id == KNOD_BPF_HASH_NEXT_END) {
+		ne = knod_bpf_map_hash_alloc_elem(knod_map, knod_map_obj, key,
+						  value);
+		if (!ne)
+			return -ENOMEM;
+		bucket[hash] = ((void *)ne - (void *)elems) / elem_size;
+		return 0;
+	}
+
+	e = elems + (elem_id * elem_size);
+	while (1) {
+		if (!(e->next & KNOD_BPF_HASH_NEXT_DELETED) &&
+		    !memcmp(&e->kv[0], (const unsigned char *)key,
+			    knod_map_obj->key_size)) {
+			unsafe_memcpy(knod_bpf_hash_elem_kv(e) +
+				      knod_map_obj->key_size,
+				      value, knod_map_obj->value_size,
+				      "knod hash elems are variable-sized GPU map records");
+			return 0;
+		}
+		unsigned int real_next = e->next & KNOD_BPF_HASH_NEXT_MASK;
+
+		if (real_next == KNOD_BPF_HASH_NEXT_END) {
+			ne = knod_bpf_map_hash_alloc_elem(knod_map,
+							  knod_map_obj,
+							  key, value);
+			if (!ne)
+				return -ENOMEM;
+			e->next = (e->next & KNOD_BPF_HASH_NEXT_DELETED) |
+				  (((void *)ne - (void *)elems) / elem_size);
+			return 0;
+		}
+		e = elems + (real_next * elem_size);
+	}
+
+	return -ENOENT;
+}
+
+static int knod_bpf_map_hash_delete_elem(struct knod_bpf_map *knod_map,
+					 struct knod_bpf_map_obj *knod_map_obj,
+					 void *key)
+{
+	void *elems = knod_map->hash_elems_mem->kaddr;
+	unsigned int *queue = knod_map->queue_mem->kaddr;
+	unsigned int hash, elem_id, elem_size, cur;
+	struct knod_bpf_hash_elem_obj *e, *pe;
+	unsigned int *bucket;
+
+	hash = jhash((const void *)key, knod_map_obj->key_size,
+		     knod_map_obj->meta.hmeta.hashrnd);
+	hash = hash & (knod_map_obj->meta.hmeta.n_buckets - 1);
+	bucket = (unsigned int *)&knod_map_obj->bucket[0];
+
+	elem_id = bucket[hash];
+	if (elem_id == KNOD_BPF_HASH_NEXT_END)
+		return -ENOENT;
+
+	elem_size = knod_map_obj->meta.hmeta.elem_size;
+
+	e = elems + (elem_id * elem_size);
+	pe = e;
+	while (1) {
+		if (!(e->next & KNOD_BPF_HASH_NEXT_DELETED) &&
+		    !memcmp(&e->kv[0], (const unsigned char *)key,
+			    knod_map_obj->key_size)) {
+			unsigned int e_next = e->next & KNOD_BPF_HASH_NEXT_MASK;
+			unsigned int del_id = ((void *)e - elems) / elem_size;
+
+			/* Unlink (GPU is paused - safe) */
+			if (pe != e)
+				pe->next = (pe->next &
+					    KNOD_BPF_HASH_NEXT_DELETED) |
+					   e_next;
+			else
+				bucket[hash] = e_next;
+
+			e->next = KNOD_BPF_HASH_NEXT_END;
+
+			/* Return elem to queue */
+			cur = knod_map_obj->meta.hmeta.cur;
+			queue[cur] = del_id;
+			knod_map_obj->meta.hmeta.cur = cur + 1;
+			return 0;
+		}
+		unsigned int real_next = e->next & KNOD_BPF_HASH_NEXT_MASK;
+
+		if (real_next == KNOD_BPF_HASH_NEXT_END)
+			return -ENOENT;
+		pe = e;
+		e = elems + (real_next * elem_size);
+	}
+
+	return -ENOENT;
+}
+
+static int knod_bpf_map_hash_get_first_key(struct bpf_offloaded_map *offmap,
+					   void *nkey)
+{
+	struct knod_bpf_map *knod_map = (struct knod_bpf_map *)offmap->dev_priv;
+	struct knod_bpf_map_obj *knod_map_obj;
+	unsigned int *bucket, elem_size, i;
+	struct knod_bpf_hash_elem_obj *e;
+	void *elems;
+
+	knod_map_obj = knod_map->knod_map_obj;
+	bucket =  (unsigned int *)&knod_map_obj->bucket[0];
+	elems = knod_map->hash_elems_mem->kaddr;
+
+	elem_size = knod_map_obj->meta.hmeta.elem_size;
+
+	for (i = 0; i < knod_map_obj->meta.hmeta.n_buckets; i++) {
+		unsigned int eid;
+
+		if (bucket[i] == KNOD_BPF_HASH_NEXT_END)
+			continue;
+		eid = bucket[i];
+		while (eid != KNOD_BPF_HASH_NEXT_END) {
+			e = elems + (eid * elem_size);
+			if (!(e->next & KNOD_BPF_HASH_NEXT_DELETED)) {
+				unsafe_memcpy(nkey, knod_bpf_hash_elem_kv(e),
+					      knod_map_obj->key_size,
+					      "knod hash elems are variable-sized GPU map records");
+				return 0;
+			}
+			eid = e->next & KNOD_BPF_HASH_NEXT_MASK;
+		}
+	}
+
+	return -ENOENT;
+}
+
+static int knod_bpf_map_hash_get_next_key(struct bpf_offloaded_map *offmap,
+					  void *key, void *nkey)
+{
+	struct knod_bpf_map *knod_map = (struct knod_bpf_map *)offmap->dev_priv;
+	struct knod_bpf_map_obj *knod_map_obj;
+	unsigned int *bucket, elem_size, i;
+	struct knod_bpf_hash_elem_obj *e;
+	bool found = false;
+	unsigned int hash;
+	void *elems;
+
+	knod_map_obj = knod_map->knod_map_obj;
+
+	bucket =  (unsigned int *)&knod_map_obj->bucket[0];
+	elems = knod_map->hash_elems_mem->kaddr;
+
+	hash = jhash((const void *)key, knod_map_obj->key_size,
+		     knod_map_obj->meta.hmeta.hashrnd);
+	hash = hash & (knod_map_obj->meta.hmeta.n_buckets - 1);
+	elem_size = knod_map_obj->meta.hmeta.elem_size;
+
+	for (i = hash; i < knod_map_obj->meta.hmeta.n_buckets; i++) {
+		unsigned int eid;
+
+		if (bucket[i] == KNOD_BPF_HASH_NEXT_END)
+			continue;
+
+		eid = bucket[i];
+		while (eid != KNOD_BPF_HASH_NEXT_END) {
+			e = elems + (eid * elem_size);
+			if (!(e->next & KNOD_BPF_HASH_NEXT_DELETED)) {
+				if (found &&
+				    memcmp(&e->kv[0],
+					   (const unsigned char *)key,
+					   knod_map_obj->key_size)) {
+					unsafe_memcpy(nkey,
+						      knod_bpf_hash_elem_kv(e),
+						      knod_map_obj->key_size,
+						      "knod hash elems are variable-sized GPU map records");
+					return 0;
+				}
+				if (!memcmp(&e->kv[0],
+					    (const unsigned char *)key,
+					    knod_map_obj->key_size))
+					found = true;
+			}
+			eid = e->next & KNOD_BPF_HASH_NEXT_MASK;
+		}
+	}
+
+	return -ENOENT;
+}
+
+static void knod_bpf_map_free(struct knod_dev *knodev,
+			      struct bpf_offloaded_map *offmap)
+{
+	struct knod_bpf_map *knod_map = offmap->dev_priv;
+	struct knod_bpf_priv *priv = knodev->accel->xdp.priv;
+
+	if (!knod_map)
+		return;
+	/*
+	 * Defer the BO free: an in-flight prog dispatch may still reference
+	 * this map's VRAM.  Move it from bound_maps onto dead_maps under
+	 * knodev->lock (the lock that guards the add); the worker reaps it
+	 * from there after its next completion, by which point the in-flight
+	 * dispatch on the old slot has retired (clean atomic flip).
+	 */
+	mutex_lock(&knodev->lock);
+	list_del(&knod_map->list);
+	list_add(&knod_map->list, &priv->dead_maps);
+	mutex_unlock(&knodev->lock);
+	offmap->dev_priv = NULL;
+}
+
+static int __knod_bpf_map_lookup_elem(struct bpf_offloaded_map *offmap,
+				      void *key, void *value)
+{
+	unsigned int idx = *(unsigned int *)key;
+	struct knod_bpf_map_obj *knod_map_obj;
+	struct knod_bpf_map *knod_map;
+	void *bucket;
+	u32 stride;
+	int i;
+
+	knod_map = (struct knod_bpf_map *)offmap->dev_priv;
+	if (!knod_map || !knod_map->mem || !knod_map->mem->kaddr ||
+	    (knod_map->hash_elems_mem && !knod_map->hash_elems_mem->kaddr)) {
+		pr_err("knod_bpf: lookup on freed/invalid map (dev_priv=%p)\n",
+		       offmap->dev_priv);
+		return -ENODEV;
+	}
+	knod_map_obj = knod_map->knod_map_obj;
+
+	if (knod_map_obj->map_type == BPF_MAP_TYPE_ARRAY) {
+		if (*(unsigned int *)key >= knod_map_obj->max_entries)
+			return -ENOENT;
+		bucket = knod_bpf_array_value_ptr(knod_map_obj, idx);
+
+		unsafe_memcpy(value, bucket, knod_map_obj->value_size,
+			      "knod array values live in a variable-sized GPU map tail");
+	} else if (knod_map_obj->map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
+		if (idx >= knod_map_obj->max_entries)
+			return -ENOENT;
+		stride = round_up(knod_map_obj->value_size, 8);
+		bucket = &knod_map_obj->bucket[0];
+		bucket += (idx * knod_map_obj->value_size);
+		for (i = 0; i < knod_map_obj->meta.ameta.n_instances; i++)
+			unsafe_memcpy(value + i * stride,
+				      bucket + i *
+				      knod_map_obj->meta.ameta
+				      .per_instance_size,
+				      knod_map_obj->value_size,
+				      "knod percpu array values live in a variable-sized GPU map tail");
+	} else if (knod_map_obj->map_type == BPF_MAP_TYPE_HASH) {
+		return knod_bpf_map_hash_lookup_elem(knod_map, knod_map_obj,
+						     key, value);
+	}
+
+	return 0;
+}
+
+static void knod_bpf_map_op_begin(struct knod_bpf_priv *priv)
+{
+	/*
+	 * Serialize concurrent map ops but do NOT park the worker: stopping it
+	 * mid-flight strands the in-flight dispatch and stalls the GPU compute
+	 * queue.  A map value updated while the GPU reads it may be seen torn,
+	 * which is a transient inconsistency the BPF prog tolerates.
+	 */
+	mutex_lock(&priv->map_op_lock);
+}
+
+static void knod_bpf_map_op_end(struct knod_bpf_priv *priv)
+{
+	knod_bpf_gpu_mem_fence(priv);
+	mutex_unlock(&priv->map_op_lock);
+}
+
+static int __knod_bpf_map_update_elem(struct bpf_offloaded_map *offmap,
+				      void *key, void *value, u64 flags)
+{
+	struct knod_bpf_map *knod_map = (struct knod_bpf_map *)offmap->dev_priv;
+	struct knod_bpf_map_obj *knod_map_obj;
+	struct knod_bpf_priv *priv;
+	unsigned int idx = *(unsigned int *)key;
+	struct knod_dev *knodev;
+	void *bucket;
+	u32 stride;
+	int i;
+
+	if (!knod_map || !knod_map->mem || !knod_map->mem->kaddr)
+		return -ENODEV;
+	knod_map_obj = knod_map->knod_map_obj;
+	priv = knod_map->priv;
+	knodev = priv->knodev;
+	if (knod_map_obj->map_type == BPF_MAP_TYPE_ARRAY) {
+		if (idx >= knod_map_obj->max_entries)
+			return -ENOENT;
+
+		bucket = knod_bpf_array_value_ptr(knod_map_obj, idx);
+		unsafe_memcpy(bucket, value, knod_map_obj->value_size,
+			      "knod array values live in a variable-sized GPU map tail");
+		knod_bpf_gpu_mem_fence(priv);
+		return 0;
+	} else if (knod_map_obj->map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
+		if (idx >= knod_map_obj->max_entries)
+			return -ENOENT;
+		stride = round_up(knod_map_obj->value_size, 8);
+		bucket = &knod_map_obj->bucket[0];
+		bucket += (idx * knod_map_obj->value_size);
+		for (i = 0; i < knod_map_obj->meta.ameta.n_instances; i++)
+			unsafe_memcpy(bucket + i *
+				      knod_map_obj->meta.ameta
+				      .per_instance_size,
+				      value + i * stride,
+				      knod_map_obj->value_size,
+				      "knod percpu array values live in a variable-sized GPU map tail");
+		knod_bpf_gpu_mem_fence(priv);
+		return 0;
+	} else if (knod_map_obj->map_type == BPF_MAP_TYPE_HASH) {
+		int ret = -ENOENT;
+
+		mutex_lock(&knodev->lock);
+		list_for_each_entry(knod_map, &knodev->accel->xdp.bound_maps,
+				    list) {
+			if (knod_map->knod_map_obj == knod_map_obj) {
+				mutex_unlock(&knodev->lock);
+				knod_bpf_map_op_begin(priv);
+				ret = knod_bpf_map_hash_update_elem(knod_map,
+						knod_map_obj,
+								    key, value);
+				knod_bpf_map_op_end(priv);
+				return ret;
+			}
+		}
+		mutex_unlock(&knodev->lock);
+	}
+
+	return -ENOENT;
+}
+
+static int __knod_bpf_map_delete_elem(struct bpf_offloaded_map *offmap,
+				      void *key)
+{
+	struct knod_bpf_map *knod_map = (struct knod_bpf_map *)offmap->dev_priv;
+	struct knod_bpf_map_obj *knod_map_obj;
+	struct knod_bpf_priv *priv;
+	int ret;
+
+	if (!knod_map || !knod_map->mem || !knod_map->mem->kaddr)
+		return -ENODEV;
+	knod_map_obj = knod_map->knod_map_obj;
+	priv = knod_map->priv;
+
+	if (knod_map_obj->map_type == BPF_MAP_TYPE_ARRAY ||
+	    knod_map_obj->map_type == BPF_MAP_TYPE_PERCPU_ARRAY)
+		return 0;
+	else if (knod_map_obj->map_type == BPF_MAP_TYPE_HASH) {
+		knod_bpf_map_op_begin(priv);
+		ret = knod_bpf_map_hash_delete_elem(knod_map, knod_map_obj,
+						    key);
+		knod_bpf_map_op_end(priv);
+		return ret;
+	}
+
+	return -ENOENT;
+}
+
+static void knod_bpf_map_gc_process(struct knod_bpf_map *knod_map)
+{
+	struct knod_bpf_map_obj *knod_map_obj = knod_map->knod_map_obj;
+	unsigned int *gc_list = knod_map->gc_mem->kaddr;
+	unsigned int *queue = knod_map->queue_mem->kaddr;
+	void *elems = knod_map->hash_elems_mem->kaddr;
+	unsigned int *bucket = (unsigned int *)&knod_map_obj->bucket[0];
+	unsigned int elem_size = knod_map_obj->meta.hmeta.elem_size;
+	unsigned int gc_count, cur, i;
+
+	gc_count = READ_ONCE(knod_map_obj->meta.hmeta.gc_count);
+	if (!gc_count)
+		return;
+
+	for (i = 0; i < gc_count; i++) {
+		unsigned int del_id = gc_list[i];
+		struct knod_bpf_hash_elem_obj *del_elem =
+			elems + (del_id * elem_size);
+		unsigned int hash, eid;
+		struct knod_bpf_hash_elem_obj *e, *pe;
+
+		hash = jhash(&del_elem->kv[0], knod_map_obj->key_size,
+			     knod_map_obj->meta.hmeta.hashrnd);
+		hash = hash & (knod_map_obj->meta.hmeta.n_buckets - 1);
+
+		eid = bucket[hash];
+		pe = NULL;
+		while (eid != KNOD_BPF_HASH_NEXT_END) {
+			e = elems + (eid * elem_size);
+			if (e == del_elem) {
+				unsigned int next = e->next &
+						   KNOD_BPF_HASH_NEXT_MASK;
+				if (pe)
+					pe->next =
+						(pe->next &
+						 KNOD_BPF_HASH_NEXT_DELETED) |
+						next;
+				else
+					bucket[hash] = next;
+
+				e->next = KNOD_BPF_HASH_NEXT_END;
+
+				cur = knod_map_obj->meta.hmeta.cur;
+				queue[cur] = del_id;
+				knod_map_obj->meta.hmeta.cur = cur + 1;
+				break;
+			}
+			pe = e;
+			eid = e->next & KNOD_BPF_HASH_NEXT_MASK;
+		}
+	}
+
+	WRITE_ONCE(knod_map_obj->meta.hmeta.gc_count, 0);
+}
+
+/*
+ * Per-loop map maintenance, run from the worker loop head (outside any
+ * rcu_read_lock_bh, since knod_free_mem() may sleep).  All bound_maps access
+ * is serialized under knodev->lock -- the same lock map_alloc/map_free use:
+ * GC live HASH maps, then reap maps that detach moved onto dead_maps.  The
+ * worker only reaches here after completing the previous dispatch, so the
+ * clean atomic flip guarantees the GPU no longer reads a reaped map's BOs.
+ */
+#define KNOD_BPF_MAPS_TICK_INTERVAL 65536
+
+static void knod_bpf_maps_tick(struct knod_bpf_priv *priv)
+{
+	struct knod_dev *knodev = priv->knodev;
+	struct knod_bpf_map *knod_map, *tmp;
+	LIST_HEAD(reap);
+
+	if (list_empty(&knodev->accel->xdp.bound_maps) &&
+	    list_empty(&priv->dead_maps))
+		return;
+
+	if (list_empty(&priv->dead_maps) &&
+	    (++priv->maps_tick_skip & (KNOD_BPF_MAPS_TICK_INTERVAL - 1)))
+		return;
+
+	mutex_lock(&knodev->lock);
+	list_for_each_entry(knod_map, &knodev->accel->xdp.bound_maps, list) {
+		if (knod_map->knod_map_obj->map_type == BPF_MAP_TYPE_HASH)
+			knod_bpf_map_gc_process(knod_map);
+	}
+	list_splice_init(&priv->dead_maps, &reap);
+	mutex_unlock(&knodev->lock);
+
+	list_for_each_entry_safe(knod_map, tmp, &reap, list) {
+		if (knod_map->gc_mem)
+			knod_free_mem(priv->knod, knod_map->gc_mem);
+		if (knod_map->queue_mem)
+			knod_free_mem(priv->knod, knod_map->queue_mem);
+		if (knod_map->hash_elems_mem)
+			knod_free_mem(priv->knod, knod_map->hash_elems_mem);
+		if (knod_map->mem)
+			knod_free_mem(priv->knod, knod_map->mem);
+		kfree(knod_map);
+	}
+}
+
+/* Completion mode: 0 = event (default, sleep on the AQL signal interrupt),
+ * 1 = poll (busy-spin the signal value).  Selectable via debugfs.
+ */
+static bool knod_bpf_poll_mode;
+
+/* Max spacing (microseconds) between dispatch-ahead submissions.  Once a
+ * dispatch is in flight the worker waits up to this long before submitting
+ * the next so it batches the packets arriving meanwhile, letting inflight
+ * grow >= 2 without degenerating into one-packet dispatches.  This is a
+ * ceiling only: an empty pipe submits at once to keep the GPU fed, and a
+ * completed dispatch is always retired without waiting.  To actually build
+ * depth the value must be below the GPU execution time of a dispatch.
+ * 0 disables spacing (submit as soon as the ring has anything).
+ */
+static u32 knod_bpf_dispatch_delay_us = 20;
+
+static void knod_bpf_wait_event(struct knod_bpf_priv *priv)
+{
+	struct kfd_event_data events = {
+		.event_id = priv->knod->aql_event[0].id,
+	};
+	u32 timeout_ms = knod_bpf_expire;
+	u32 wait_result;
+
+	knod_wait_on_events(priv->knod->process, 1, &events, true,
+			    &timeout_ms, &wait_result);
+}
+
+static bool knod_bpf_submit_work(struct knod_bpf_priv *priv)
+{
+	struct knod_bpf_work_sq *sqw;
+	struct knod_bpf_stats *stats = &priv->stats;
+	ktime_t dispatch_start;
+
+	if (priv->inflight_cnt >= KNOD_BPF_INFLIGHT)
+		return false;
+
+	/* Pace dispatch-ahead so the next dispatch batches the packets that
+	 * arrive during this window instead of firing one-packet dispatches.
+	 * An empty pipe skips the wait so the GPU is never left idle.
+	 */
+	if (priv->inflight_cnt &&
+	    ktime_before(ktime_get(), priv->next_dispatch_time))
+		return false;
+
+	if (static_branch_unlikely(&knod_stats_key))
+		dispatch_start = ktime_get();
+
+	sqw = knod_prepare_bpf(priv);
+	if (!sqw)
+		return false;
+
+	if (static_branch_unlikely(&knod_stats_key)) {
+		u64 dns = ktime_to_ns(ktime_sub(ktime_get(),
+						dispatch_start));
+
+		stats->dispatch_total_ns += dns;
+		stats->dispatch_count++;
+		if (dns > stats->dispatch_max_ns)
+			stats->dispatch_max_ns = dns;
+	}
+
+	knod_submit_bpf(priv, sqw);
+	priv->inflight[priv->inflight_cnt++] = sqw;
+	priv->next_dispatch_time =
+		ktime_add_us(ktime_get(),
+			     READ_ONCE(knod_bpf_dispatch_delay_us));
+	return true;
+}
+
+static void knod_bpf_record_completion(struct knod_bpf_priv *priv,
+				       struct knod_bpf_work_sq *sqw)
+{
+	struct knod_bpf_stats *stats = &priv->stats;
+	u64 ns;
+	int bucket;
+
+	if (!static_branch_unlikely(&knod_stats_key))
+		return;
+
+	ns = ktime_to_ns(ktime_sub(ktime_get(), sqw->dispatch_time));
+	stats->completion_total_ns += ns;
+	stats->completion_count++;
+
+	if (ns > stats->completion_max_ns)
+		stats->completion_max_ns = ns;
+
+	if (ns < 1000)
+		bucket = 0;
+	else
+		bucket = min(ilog2(ns / 1000) + 1,
+			     KNOD_LAT_BUCKETS - 1);
+	stats->completion_hist[bucket]++;
+}
+
+static bool knod_bpf_poll_complete(struct knod_bpf_priv *priv,
+				   struct knod_bpf_work_sq *sqw)
+{
+	struct amd_signal *signal;
+
+	if (!sqw)
+		return false;
+
+	signal = (struct amd_signal *)
+		priv->knod->kaql[0].queue_signal->kaddr;
+
+	if (sqw->sigval > READ_ONCE(signal->value)) {
+		knod_bpf_record_completion(priv, sqw);
+		return true;
+	}
+
+	if (time_after(jiffies, sqw->expire)) {
+		pr_warn_ratelimited("knod_bpf: poll expire (sigval=%lld signal=%lld expire_ms=%u)\n",
+			sqw->sigval, READ_ONCE(signal->value), knod_bpf_expire);
+		knod_bpf_record_completion(priv, sqw);
+		return true;
+	}
+
+	return false;
+}
+
+static void knod_bpf_schedule_pending_napi(struct knod_bpf_priv *priv)
+{
+	struct knod_dev *knodev = priv->knodev;
+	int qi;
+
+	for (qi = 0; qi < priv->nr_works; qi++) {
+		if (spsc_pending(&knodev->wpriv[qi].spsc_bds) &&
+		    knodev->wpriv[qi].napi)
+			napi_schedule(knodev->wpriv[qi].napi);
+	}
+}
+
+static int knod_bpf_worker(void *arg)
+{
+	struct knod_bpf_priv *priv = arg;
+	struct knod_bpf_work_sq *sqw;
+	bool progressed;
+
+	while (!kthread_should_stop()) {
+		if (kthread_should_park()) {
+			knod_bpf_drain_worker(priv);
+			kthread_parkme();
+			continue;
+		}
+
+		knod_bpf_maps_tick(priv);
+
+		progressed = false;
+
+		rcu_read_lock_bh();
+		/* Retire completed dispatches oldest-first: the signal is
+		 * monotonic so inflight[0] finishes before inflight[1..].
+		 */
+		while (priv->inflight_cnt &&
+		       knod_bpf_poll_complete(priv, priv->inflight[0])) {
+			sqw = priv->inflight[0];
+			if (--priv->inflight_cnt)
+				memmove(priv->inflight, priv->inflight + 1,
+					priv->inflight_cnt *
+					sizeof(priv->inflight[0]));
+			priv->inflight[priv->inflight_cnt] = NULL;
+			knod_complete_acquire(priv, sqw);
+			knod_complete_napi(priv, sqw);
+			progressed = true;
+		}
+
+		/* Keep the pipe full: dispatch ahead up to KNOD_BPF_INFLIGHT.
+		 * Staging self-limits, so this stops once the ring is drained.
+		 */
+		while (knod_bpf_submit_work(priv))
+			progressed = true;
+		rcu_read_unlock_bh();
+
+		if (!priv->inflight_cnt) {
+			knod_bpf_schedule_pending_napi(priv);
+			usleep_range(100, 200);
+		} else if (!progressed) {
+			/* Room to dispatch ahead but the pacing window has not
+			 * opened yet: spin so the next submit fires on time and
+			 * a completion is retired the instant it lands.  Block
+			 * on the event only when the pipe is full (nothing to
+			 * submit) or spacing is disabled.
+			 */
+			if (priv->inflight_cnt < KNOD_BPF_INFLIGHT &&
+			    ktime_before(ktime_get(), priv->next_dispatch_time))
+				cpu_relax();
+			else if (knod_bpf_poll_mode)
+				cpu_relax();
+			else
+				knod_bpf_wait_event(priv);
+		}
+	}
+
+	return 0;
+}
+
+static void knod_bpf_sq_init(struct knod_bpf_priv *priv)
+{
+	struct knod_bpf_work_sq *sqw;
+	int i;
+
+	priv->worker_task = NULL;
+	priv->inflight_cnt = 0;
+	INIT_LIST_HEAD(&priv->free_list_sqw);
+
+	for (i = 0; i < 32; i++) {
+		sqw = kvzalloc_obj(struct knod_bpf_work_sq, GFP_KERNEL);
+		if (!sqw)
+			continue;
+
+		sqw->param = knod_alloc_mem(priv->knod,
+					    sizeof(struct knod_bpf_param),
+					    KFD_IOC_ALLOC_MEM_FLAGS_GTT |
+					    KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+					    KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+		if (!sqw->param) {
+			kvfree(sqw);
+			continue;
+		}
+		memset(sqw->param->kaddr, 0, sizeof(struct knod_bpf_param));
+		INIT_LIST_HEAD(&sqw->list);
+		list_add(&sqw->list, &priv->free_list_sqw);
+		sqw->backlogs = 0;
+	}
+}
+
+static void knod_bpf_free_sqw(struct knod_bpf_priv *priv,
+			      struct knod_bpf_work_sq *sqw)
+{
+	if (!sqw)
+		return;
+
+	knod_free_mem(priv->knod, sqw->param);
+	kfree(sqw);
+}
+
+static void knod_bpf_free_sqw_list(struct knod_bpf_priv *priv,
+				   struct list_head *head)
+{
+	struct knod_bpf_work_sq *sqw, *tmp;
+
+	list_for_each_entry_safe(sqw, tmp, head, list) {
+		list_del(&sqw->list);
+		knod_bpf_free_sqw(priv, sqw);
+	}
+}
+
+static void knod_bpf_sq_exit(struct knod_bpf_priv *priv)
+{
+	if (!priv->knod)
+		return;
+
+	knod_bpf_stop_worker(priv);
+	knod_bpf_drain(priv);
+
+	knod_bpf_free_sqw_list(priv, &priv->free_list_sqw);
+	priv->inflight_cnt = 0;
+}
+
+static void knod_priv_exit(struct knod_bpf_priv *priv)
+{
+	struct knod_dev *knodev = priv->knodev;
+	struct knod_bpf_map *knod_map, *tmp;
+	LIST_HEAD(reap);
+
+	knod_bpf_sq_exit(priv);
+
+	/*
+	 * The dispatch worker is not stopped until the next feature registers
+	 * its own worker, so it may still be running knod_bpf_maps_tick() here.
+	 * Serialize under knodev->lock and splice both lists to a local one:
+	 * whichever side splices first frees them, the other sees them empty.
+	 * Free outside the lock since knod_free_mem() may sleep.
+	 */
+	mutex_lock(&knodev->lock);
+	list_splice_init(&knodev->accel->xdp.bound_maps, &reap);
+	list_splice_init(&priv->dead_maps, &reap);
+	mutex_unlock(&knodev->lock);
+
+	list_for_each_entry_safe(knod_map, tmp, &reap, list) {
+		if (knod_map->gc_mem)
+			knod_free_mem(priv->knod, knod_map->gc_mem);
+		if (knod_map->queue_mem)
+			knod_free_mem(priv->knod, knod_map->queue_mem);
+		if (knod_map->hash_elems_mem)
+			knod_free_mem(priv->knod, knod_map->hash_elems_mem);
+		if (knod_map->mem)
+			knod_free_mem(priv->knod, knod_map->mem);
+		kfree(knod_map);
+	}
+
+	kfree(priv->prog_buf);
+	kfree(priv->pass_prog_buf);
+	if (priv->pass_knod_prog) {
+		knod_prog_free(priv->pass_knod_prog);
+		priv->pass_knod_prog = NULL;
+	}
+	/* kernels[] are owned by knod (freed in knod_release_ctx), not here */
+	if (priv->pass_meta_buf)
+		knod_free_mem(priv->knod, priv->pass_meta_buf);
+}
+
+static int knod_priv_init(struct knod_bpf_priv *priv)
+{
+	struct knod_dev *knodev = priv->knodev;
+	int pass_meta_buf_size;
+	int index;
+
+	priv->prog = NULL;
+	mutex_init(&priv->map_op_lock);
+	INIT_LIST_HEAD(&priv->dead_maps);
+	priv->maps_tick_skip = 0;
+
+	priv->nr_works = knod_bpf_active_rxq_count(knodev->netdev);
+	if (!priv->nr_works) {
+		pr_warn("knod_bpf: no active RX queues for %s\n",
+			knodev->netdev ? knodev->netdev->name : "<null>");
+		return -EINVAL;
+	}
+
+	priv->prog_buf = kzalloc(KNOD_BPF_PROG_BUF_SIZE, GFP_KERNEL);
+	if (!priv->prog_buf)
+		return -ENOMEM;
+
+	for (index = 0; index < priv->nr_works; index++)
+		priv->queue_base_gaddr[index] = priv->knod->buf[index]->gaddr;
+
+	/* Per-queue PASS slot count; sizes the shader pass_meta_buf below.
+	 * At most one PASS packet per dispatched slot, i.e. batch_size.
+	 */
+	priv->pass_pkts_per_queue = knod_bpf_batch_size(priv);
+
+	/* Allocate GTT buffer for per-queue shader PASS copy */
+	pass_meta_buf_size = priv->nr_works * priv->pass_pkts_per_queue *
+			KNOD_PASS_SLOT_SIZE;
+	priv->pass_meta_buf = knod_alloc_mem(priv->knod, pass_meta_buf_size,
+					KFD_IOC_ALLOC_MEM_FLAGS_GTT |
+					KFD_IOC_ALLOC_MEM_FLAGS_WRITABLE |
+					KFD_IOC_ALLOC_MEM_FLAGS_COHERENT);
+	if (IS_ERR(priv->pass_meta_buf)) {
+		pr_warn("KNOD: failed to allocate pass_meta_buf\n");
+		priv->pass_meta_buf = NULL;
+		knod_priv_exit(priv);
+		return -ENOMEM;
+	}
+	pr_debug("KNOD: pass_meta_buf gaddr=0x%llx..0x%llx size=%d nr_q=%d pass_pkts_per_queue=%u\n",
+		 priv->pass_meta_buf->gaddr,
+		 priv->pass_meta_buf->gaddr + pass_meta_buf_size,
+		 priv->pass_meta_buf->size, priv->nr_works,
+		 priv->pass_pkts_per_queue);
+
+	/* GPU->host delivery pages come from the framework per-queue page_pool
+	 * (knodev->wpriv[q].pass_pool): the producer allocs from it and the
+	 * NAPI drain recycles, so no per-feature delivery BO is allocated here.
+	 */
+
+	knod_bpf_sq_init(priv);
+
+	return 0;
+}
+
+static struct knod_bpf_priv *__knod_accel_xdp_init(struct knod_accel *accel,
+						   struct knod_dev *knodev)
+{
+	struct knod *knod = (struct knod *)knodev->accel->priv;
+	struct knod_bpf_priv *priv;
+
+	priv = kzalloc_obj(struct knod_bpf_priv, GFP_KERNEL);
+	if (!priv)
+		return ERR_PTR(-ENOMEM);
+
+	INIT_LIST_HEAD(&priv->list);
+	if (knod_bpf_workgroups % 64) {
+		knod_bpf_workgroups /= 64;
+		knod_bpf_workgroups++;
+		knod_bpf_workgroups *= 64;
+	}
+
+	if (knod_bpf_workgroups < KNOD_BPF_WORKGROUPS_MIN ||
+	    knod_bpf_workgroups > KNOD_BPF_WORKGROUPS_MAX)
+		knod_bpf_workgroups = KNOD_BPF_WORKGROUPS_DEFAULT;
+
+	if (knod_bpf_expire < KNOD_BPF_EXPIRE_MIN ||
+	    knod_bpf_expire > KNOD_BPF_EXPIRE_MAX)
+		knod_bpf_expire = KNOD_BPF_EXPIRE_DEFAULT;
+	pr_debug("workgroup size %d\n", knod_bpf_workgroups);
+	pr_debug("expire time = %dms\n", knod_bpf_expire);
+	pr_debug("packet cache = %d", knod_bpf_pkt_cache);
+
+	INIT_LIST_HEAD(&accel->xdp.bound_maps);
+	accel->flags |= KNOD_FLAGS_XDP;
+	accel->xdp.priv = priv;
+	list_add(&priv->list, &priv_list);
+
+	priv->knod = knod;
+	priv->accel = accel;
+	priv->knodev = knodev;
+	priv->dev = knodev->netdev;
+
+	priv->isa_version = knod->isa_version;
+
+	/*
+	 * Only permanent per-attach state is set up here; the GPU compute
+	 * buffers (knod_priv_init/kfd_kernel_init) are allocated by
+	 * ->activate() when the BPF feature is selected.
+	 */
+
+	return priv;
+}
+
+/* Feature select: allocate the BPF GPU compute resources. */
+static int knod_bpf_activate(struct knod_dev *knodev)
+{
+	struct knod_accel *accel = knodev->accel;
+	struct knod_bpf_priv *priv = accel->xdp.priv;
+	struct knod *knod = accel->priv;
+
+	/*
+	 * Pin the module while BPF is the selected feature: the core calls
+	 * into these ops, so it must not be unloaded until feature->none.
+	 * (No-op when built in - THIS_MODULE is NULL.)
+	 */
+	if (!try_module_get(THIS_MODULE))
+		return -ENODEV;
+
+	if (knod_priv_init(priv)) {
+		WARN_ON_ONCE(1);
+		module_put(THIS_MODULE);
+		return -EINVAL;
+	}
+	if (kfd_kernel_init(knod, priv)) {
+		knod_priv_exit(priv);
+		module_put(THIS_MODULE);
+		return -ENOMEM;
+	}
+
+	priv->start = 0;
+	return 0;
+}
+
+/* Feature deselect: free the BPF GPU compute resources. */
+static void knod_bpf_deactivate(struct knod_dev *knodev)
+{
+	struct knod_bpf_priv *priv = knodev->accel->xdp.priv;
+
+	knod_priv_exit(priv);
+	module_put(THIS_MODULE);
+}
+
+/* True while a user XDP prog or offloaded map is still bound to this accel. */
+static bool knod_bpf_busy(struct knod_dev *knodev)
+{
+	struct knod_accel *accel = knodev->accel;
+	struct knod_bpf_priv *priv = accel->xdp.priv;
+
+	if (!priv)
+		return false;
+	return READ_ONCE(priv->prog) || !list_empty(&accel->xdp.bound_maps);
+}
+
+static void __knod_accel_xdp_exit(struct knod_accel *accel,
+				  struct knod_bpf_priv *priv)
+{
+	/* GPU compute buffers are freed by ->deactivate(); free the rest. */
+	memset(&accel->xdp, 0, sizeof(struct knod_accel_xdp));
+	accel->flags &= ~KNOD_FLAGS_XDP;
+	list_del(&priv->list);
+	kfree(priv);
+}
+
+static struct knod_insn_meta *knod_bpf_goto_meta(struct knod_prog *knod_prog,
+						 struct knod_insn_meta *meta,
+						 unsigned int insn_idx)
+{
+	unsigned int forward, backward, i;
+
+	backward = meta->bpf_insn_idx - insn_idx;
+	forward = insn_idx - meta->bpf_insn_idx;
+
+	if (min(forward, backward) > knod_prog->n_insns - insn_idx - 1) {
+		backward = knod_prog->n_insns - insn_idx - 1;
+		meta = knod_prog_last_meta(knod_prog);
+	}
+	if (min(forward, backward) > insn_idx && backward > insn_idx) {
+		forward = insn_idx;
+		meta = knod_prog_first_meta(knod_prog);
+	}
+
+	if (forward < backward)
+		for (i = 0; i < forward; i++)
+			meta = knod_meta_next(meta);
+	else
+		for (i = 0; i < backward; i++)
+			meta = knod_meta_prev(meta);
+
+	return meta;
+}
+
+static int knod_bpf_check_stack_access(struct knod_prog *knod_prog,
+				       struct knod_insn_meta *meta,
+				       const struct bpf_reg_state *reg,
+				       struct bpf_verifier_env *env)
+{
+	s32 old_off, new_off;
+
+	if (reg->frameno != env->cur_state->curframe)
+		meta->flags |= FLAG_INSN_PTR_CALLER_STACK_FRAME;
+
+	if (!tnum_is_const(reg->var_off)) {
+		knod_jit_dbg(" variable ptr stack access\n");
+		return -EINVAL;
+	}
+
+	if (meta->ptr.type == NOT_INIT)
+		return 0;
+
+	old_off = meta->ptr.var_off.value;
+	new_off = reg->var_off.value;
+
+	meta->ptr_not_const |= old_off != new_off;
+
+	if (!meta->ptr_not_const)
+		return 0;
+
+	if (old_off % 4 == new_off % 4)
+		return 0;
+
+	knod_jit_dbg(" stack access changed location was:%d is:%d\n",
+		old_off, new_off);
+	return -EINVAL;
+}
+
+static struct knod_insn_meta *
+knod_bpf_lookup_prev_meta_by_dreg(struct knod_prog *knod_prog,
+				  struct knod_insn_meta *meta,
+				  int dreg_id)
+{
+	list_for_each_entry_continue_reverse(meta, &knod_prog->insns, l) {
+		if (!is_mbpf_alu(meta) &&
+		    !is_mbpf_load(meta) &&
+		    !is_mbpf_store(meta))
+			continue;
+		if (meta->insn.dst_reg == dreg_id)
+			return meta;
+	}
+
+	return NULL;
+}
+
+static int knod_bpf_check_ptr(struct knod_prog *knod_prog,
+			      struct knod_insn_meta *meta,
+			      struct bpf_verifier_env *env, u8 reg_no)
+{
+	const struct bpf_reg_state *reg = cur_regs(env) + reg_no;
+	int err;
+
+	if (reg->type != PTR_TO_CTX &&
+	    reg->type != PTR_TO_STACK &&
+	    reg->type != PTR_TO_MAP_VALUE &&
+	    reg->type != PTR_TO_PACKET) {
+		knod_jit_dbg(" unsupported ptr type: %d\n", reg->type);
+		return -EINVAL;
+	}
+
+	if (reg->type == PTR_TO_STACK) {
+		err = knod_bpf_check_stack_access(knod_prog, meta, reg, env);
+		if (err)
+			return err;
+	}
+
+	if (meta->ptr.type != NOT_INIT && meta->ptr.type != reg->type) {
+		knod_jit_dbg(" ptr type changed for instruction %d -> %d\n",
+			meta->ptr.type,
+			reg->type);
+		return -EINVAL;
+	}
+
+	meta->ptr = *reg;
+
+	return 0;
+}
+
+static int knod_bpf_update_ptr_off(struct knod_prog *knod_prog,
+				   struct knod_insn_meta *meta,
+				   struct bpf_verifier_env *env)
+{
+	struct knod_bpf_reg_state *sreg = &meta->sreg;
+	struct knod_bpf_reg_state *dreg = &meta->dreg;
+	struct knod_insn_meta *prev_meta;
+
+	if (is_mbpf_load(meta)) {
+		if (sreg->reg.type == PTR_TO_PACKET ||
+		    sreg->reg.type == PTR_TO_STACK) {
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.src_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			if (sreg->reg.type == PTR_TO_PACKET)
+				sreg->packet_off = prev_meta->dreg.packet_off;
+			else
+				sreg->stack_off = prev_meta->dreg.stack_off;
+		}
+	} else if (is_mbpf_store(meta)) {
+		if (dreg->reg.type == PTR_TO_PACKET ||
+		    dreg->reg.type == PTR_TO_PACKET) {
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			if (dreg->reg.type == PTR_TO_PACKET)
+				dreg->packet_off = prev_meta->dreg.packet_off;
+			else
+				dreg->stack_off = prev_meta->dreg.stack_off;
+		}
+	}
+
+	return 0;
+}
+
+static int knod_bpf_check_store(struct knod_prog *knod_prog,
+				struct knod_insn_meta *meta,
+				struct bpf_verifier_env *env)
+{
+	const struct bpf_reg_state *reg = cur_regs(env) + meta->insn.dst_reg;
+
+	if (reg->type == PTR_TO_CTX) {
+		if (knod_prog->type == BPF_PROG_TYPE_XDP) {
+			/* XDP ctx accesses must be 4B in size */
+			switch (meta->insn.off) {
+			case offsetof(struct xdp_md, rx_queue_index):
+				knod_jit_dbg(" queue selection not supported by FW\n");
+				return -EOPNOTSUPP;
+			}
+		}
+		knod_jit_dbg(" unsupported store to context field\n");
+		return -EOPNOTSUPP;
+	}
+
+	return knod_bpf_check_ptr(knod_prog, meta, env, meta->insn.dst_reg);
+}
+
+/* NOTE:
+ * knod_bpf_lookup_prev_meta_by_dreg(), src_reg vs dst_reg ???????/
+ */
+static int knod_bpf_check_alu(struct knod_prog *knod_prog,
+			      struct knod_insn_meta *meta,
+			      struct bpf_verifier_env *env)
+{
+	const struct bpf_reg_state *sreg = cur_regs(env) + meta->insn.src_reg;
+	const struct bpf_reg_state *dreg = cur_regs(env) + meta->insn.dst_reg;
+	struct knod_bpf_reg_state *ksreg = &meta->sreg;
+	struct knod_bpf_reg_state *kdreg = &meta->dreg;
+	struct knod_insn_meta *prev_meta;
+	int imm;
+
+	meta->umin_src = min(meta->umin_src, reg_umin(sreg));
+	meta->umax_src = max(meta->umax_src, reg_umax(sreg));
+	meta->umin_dst = min(meta->umin_dst, reg_umin(dreg));
+	meta->umax_dst = max(meta->umax_dst, reg_umax(dreg));
+
+	/* AMDGPU doesn't have divide instructions, we support divide by
+	 * constant through reciprocal multiplication. Given NFP support
+	 * multiplication no bigger than u32, we'd require divisor and dividend
+	 * no bigger than that as well.
+	 *
+	 * Also eBPF doesn't support signed divide and has enforced this on C
+	 * language level by failing compilation. However LLVM assembler hasn't
+	 * enforced this, so it is possible for negative constant to leak in as
+	 * a BPF_K operand through assembly code, we reject such cases as well.
+	 */
+	if (is_mbpf_div(meta)) {
+		if (meta->umax_dst > U32_MAX) {
+			knod_jit_dbg(" dividend is not within u32 value range\n");
+			return -EINVAL;
+		}
+		if (mbpf_src(meta) == BPF_X) {
+			if (meta->umin_src != meta->umax_src) {
+				knod_jit_dbg(" divisor is not constant\n");
+				return -EINVAL;
+			}
+			if (meta->umax_src > U32_MAX) {
+				knod_jit_dbg(" divisor is not within u32 value range\n");
+				return -EINVAL;
+			}
+		}
+		if (mbpf_src(meta) == BPF_K && meta->insn.imm < 0) {
+			knod_jit_dbg(" divide by negative constant is not supported\n");
+			return -EINVAL;
+		}
+	}
+
+	if (dreg->type == PTR_TO_STACK) {
+		imm = meta->insn.imm;
+
+		switch (meta->insn.code) {
+		/* ALU
+		 * If a destination register contains a pointer of STACK,
+		 * offset should not be minus.
+		 */
+		case BPF_ALU | BPF_MOV | BPF_X:
+		case BPF_ALU64 | BPF_MOV | BPF_X:
+			//r[d] = r[s];
+			kdreg->stack_off = ksreg->stack_off;
+			break;
+		case BPF_ALU | BPF_MOV | BPF_K:
+		case BPF_ALU64 | BPF_MOV | BPF_K:
+			//r[d] = imm;
+			kdreg->stack_off = ksreg->stack_off;
+			break;
+		case BPF_ALU | BPF_XOR | BPF_X:
+		case BPF_ALU64 | BPF_XOR | BPF_X:
+			//r[d] ^= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_XOR | BPF_K:
+		case BPF_ALU64 | BPF_XOR | BPF_K:
+			//r[d] ^= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off ^ imm;
+			break;
+		case BPF_ALU | BPF_MOD | BPF_X:
+		case BPF_ALU64 | BPF_MOD | BPF_X:
+			//r[d] %= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_MOD | BPF_K:
+		case BPF_ALU64 | BPF_MOD | BPF_K:
+			//r[d] %= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off % imm;
+			break;
+		case BPF_ALU | BPF_AND | BPF_X:
+		case BPF_ALU64 | BPF_AND | BPF_X:
+			//r[d] &= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_AND | BPF_K:
+		case BPF_ALU64 | BPF_AND | BPF_K:
+			//r[d] &= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off & imm;
+			break;
+		case BPF_ALU | BPF_OR | BPF_X:
+		case BPF_ALU64 | BPF_OR | BPF_X:
+			//r[d] |= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_OR | BPF_K:
+		case BPF_ALU64 | BPF_OR | BPF_K:
+			//r[d] |= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off | imm;
+			break;
+		case BPF_ALU | BPF_ADD | BPF_X:
+		case BPF_ALU64 | BPF_ADD | BPF_X:
+			//r[d] += r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_ADD | BPF_K:
+		case BPF_ALU64 | BPF_ADD | BPF_K:
+			//r[d] += imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off + imm;
+			break;
+		case BPF_ALU | BPF_SUB | BPF_X:
+		case BPF_ALU64 | BPF_SUB | BPF_X:
+			//r[d] -= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_SUB | BPF_K:
+		case BPF_ALU64 | BPF_SUB | BPF_K:
+			//r[d] -= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off - imm;
+			break;
+		case BPF_ALU | BPF_MUL | BPF_X:
+		case BPF_ALU64 | BPF_MUL | BPF_X:
+			//r[d] *= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_MUL | BPF_K:
+		case BPF_ALU64 | BPF_MUL | BPF_K:
+			//r[d] *= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off * imm;
+			break;
+		case BPF_ALU | BPF_DIV | BPF_X:
+		case BPF_ALU64 | BPF_DIV | BPF_X:
+			//r[d] /= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_DIV | BPF_K:
+		case BPF_ALU64 | BPF_DIV | BPF_K:
+			//r[d] /= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off / imm;
+			break;
+		case BPF_ALU | BPF_NEG:
+		case BPF_ALU64 | BPF_NEG:
+			//r[d] = -r[d];
+			break;
+		case BPF_ALU | BPF_LSH | BPF_X:
+		case BPF_ALU64 | BPF_LSH | BPF_X:
+			//r[d] <<= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_LSH | BPF_K:
+		case BPF_ALU64 | BPF_LSH | BPF_K:
+			//r[d] <<= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off << imm;
+			break;
+		case BPF_ALU | BPF_RSH | BPF_X:
+		case BPF_ALU64 | BPF_RSH | BPF_X:
+			//r[d] >>= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_RSH | BPF_K:
+		case BPF_ALU64 | BPF_RSH | BPF_K:
+			//r[d] >>= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off >> imm;
+			break;
+		case BPF_ALU | BPF_ARSH | BPF_X:
+		case BPF_ALU64 | BPF_ARSH | BPF_X:
+			//r[d] >>= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_ARSH | BPF_K:
+		case BPF_ALU64 | BPF_ARSH | BPF_K:
+			//r[d] >>= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->stack_off = prev_meta->dreg.stack_off >> imm;
+			break;
+		}
+		knod_jit_dbg(" %d: dreg->stack_off = %d\n", meta->bpf_insn_idx,
+			kdreg->stack_off);
+	}
+
+	if (dreg->type == PTR_TO_PACKET) {
+		imm = meta->insn.imm;
+
+		switch (meta->insn.code) {
+		/* ALU
+		 * If a destination register contains a pointer of STACK,
+		 * offset should not be minus.
+		 */
+		case BPF_ALU | BPF_MOV | BPF_X:
+		case BPF_ALU64 | BPF_MOV | BPF_X:
+			//r[d] = r[s];
+			kdreg->packet_off = ksreg->packet_off;
+			break;
+		case BPF_ALU | BPF_MOV | BPF_K:
+		case BPF_ALU64 | BPF_MOV | BPF_K:
+			//r[d] = imm;
+			kdreg->packet_off = ksreg->packet_off;
+			break;
+		case BPF_ALU | BPF_XOR | BPF_X:
+		case BPF_ALU64 | BPF_XOR | BPF_X:
+			//r[d] ^= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_XOR | BPF_K:
+		case BPF_ALU64 | BPF_XOR | BPF_K:
+			//r[d] ^= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off ^ imm;
+			break;
+		case BPF_ALU | BPF_MOD | BPF_X:
+		case BPF_ALU64 | BPF_MOD | BPF_X:
+			//r[d] %= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_MOD | BPF_K:
+		case BPF_ALU64 | BPF_MOD | BPF_K:
+			//r[d] %= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off % imm;
+			break;
+		case BPF_ALU | BPF_AND | BPF_X:
+		case BPF_ALU64 | BPF_AND | BPF_X:
+			//r[d] &= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_AND | BPF_K:
+		case BPF_ALU64 | BPF_AND | BPF_K:
+			//r[d] &= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off & imm;
+			break;
+		case BPF_ALU | BPF_OR | BPF_X:
+		case BPF_ALU64 | BPF_OR | BPF_X:
+			//r[d] |= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_OR | BPF_K:
+		case BPF_ALU64 | BPF_OR | BPF_K:
+			//r[d] |= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off | imm;
+			break;
+		case BPF_ALU | BPF_ADD | BPF_X:
+		case BPF_ALU64 | BPF_ADD | BPF_X:
+			//r[d] += r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_ADD | BPF_K:
+		case BPF_ALU64 | BPF_ADD | BPF_K:
+			//r[d] += imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off + imm;
+			break;
+		case BPF_ALU | BPF_SUB | BPF_X:
+		case BPF_ALU64 | BPF_SUB | BPF_X:
+			//r[d] -= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_SUB | BPF_K:
+		case BPF_ALU64 | BPF_SUB | BPF_K:
+			//r[d] -= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off - imm;
+			break;
+		case BPF_ALU | BPF_MUL | BPF_X:
+		case BPF_ALU64 | BPF_MUL | BPF_X:
+			//r[d] *= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_MUL | BPF_K:
+		case BPF_ALU64 | BPF_MUL | BPF_K:
+			//r[d] *= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off * imm;
+			break;
+		case BPF_ALU | BPF_DIV | BPF_X:
+		case BPF_ALU64 | BPF_DIV | BPF_X:
+			//r[d] /= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_DIV | BPF_K:
+		case BPF_ALU64 | BPF_DIV | BPF_K:
+			//r[d] /= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off / imm;
+			break;
+		case BPF_ALU | BPF_NEG:
+		case BPF_ALU64 | BPF_NEG:
+			//r[d] = -r[d];
+			break;
+		case BPF_ALU | BPF_LSH | BPF_X:
+		case BPF_ALU64 | BPF_LSH | BPF_X:
+			//r[d] <<= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_LSH | BPF_K:
+		case BPF_ALU64 | BPF_LSH | BPF_K:
+			//r[d] <<= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off << imm;
+			break;
+		case BPF_ALU | BPF_RSH | BPF_X:
+		case BPF_ALU64 | BPF_RSH | BPF_X:
+			//r[d] >>= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_RSH | BPF_K:
+		case BPF_ALU64 | BPF_RSH | BPF_K:
+			//r[d] >>= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off >> imm;
+			break;
+		case BPF_ALU | BPF_ARSH | BPF_X:
+		case BPF_ALU64 | BPF_ARSH | BPF_X:
+			//r[d] >>= r[s];
+			knod_jit_dbg(" PTR_TO_STACK with BPF_X is not supported\n");
+			return -EINVAL;
+		case BPF_ALU | BPF_ARSH | BPF_K:
+		case BPF_ALU64 | BPF_ARSH | BPF_K:
+			//r[d] >>= imm;
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(
+				knod_prog, meta, meta->insn.dst_reg);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid\n");
+				return -EINVAL;
+			}
+			kdreg->packet_off = prev_meta->dreg.packet_off >> imm;
+			break;
+		}
+		knod_jit_dbg(" %d: dreg->packet_off = %d\n", meta->bpf_insn_idx,
+			kdreg->packet_off);
+	}
+	return 0;
+}
+
+static int knod_bpf_verify_insn(struct bpf_verifier_env *env,
+				int insn_idx, int prev_insn)
+{
+	struct knod_prog *knod_prog = env->prog->aux->offload->dev_priv;
+	const struct bpf_reg_state *sreg, *dreg, *kreg, *vreg;
+	struct knod_insn_meta *meta = knod_prog->meta;
+	struct knod_insn_meta *prev_meta;
+	int err = 0;
+
+	meta = knod_bpf_goto_meta(knod_prog, meta, insn_idx);
+	sreg = cur_regs(env) + meta->insn.src_reg;
+	dreg = cur_regs(env) + meta->insn.dst_reg;
+	knod_prog->meta = meta;
+	meta->sreg.reg = *sreg;
+	meta->dreg.reg = *dreg;
+
+	knod_bpf_update_ptr_off(knod_prog, meta, env);
+
+	if (meta->insn.src_reg >= MAX_BPF_REG ||
+			meta->insn.dst_reg >= MAX_BPF_REG) {
+		knod_jit_dbg(" program uses extended registers - jit hardening?\n");
+		err = -EINVAL;
+		goto out;
+	}
+
+	if (is_mbpf_load(meta)) {
+		err = knod_bpf_check_ptr(knod_prog, meta, env,
+					 meta->insn.src_reg);
+		goto out;
+	}
+	if (is_mbpf_store(meta)) {
+		err = knod_bpf_check_store(knod_prog, meta, env);
+		goto out;
+	}
+
+	if (is_mbpf_map_call(meta)) {
+		kreg = cur_regs(env) + 2;
+		meta->kreg.reg = *kreg;
+
+		prev_meta = knod_bpf_lookup_prev_meta_by_dreg(knod_prog,
+							      meta,
+							      2);
+		if (!prev_meta) {
+			knod_jit_dbg(" Invalid\n");
+			err = -EINVAL;
+			goto out;
+		}
+		if (kreg->type == PTR_TO_PACKET)
+			meta->kreg.packet_off = prev_meta->dreg.packet_off;
+		else
+			meta->kreg.stack_off = prev_meta->dreg.stack_off;
+		if (knod_prog->max_stack_off > meta->kreg.stack_off)
+			knod_prog->max_stack_off = meta->kreg.stack_off;
+		if (knod_prog->max_packet_off < meta->kreg.packet_off)
+			knod_prog->max_packet_off = meta->kreg.packet_off;
+
+		/* bpf_map_update_elem: track r3 (value pointer) */
+		if (meta->insn.imm == 2) {
+			vreg = cur_regs(env) + 3;
+			meta->vreg.reg = *vreg;
+
+			prev_meta = knod_bpf_lookup_prev_meta_by_dreg(knod_prog,
+								      meta,
+								      3);
+			if (!prev_meta) {
+				knod_jit_dbg(" Invalid vreg\n");
+				err = -EINVAL;
+				goto out;
+			}
+			if (vreg->type == PTR_TO_PACKET)
+				meta->vreg.packet_off =
+					prev_meta->dreg.packet_off;
+			else
+				meta->vreg.stack_off =
+					prev_meta->dreg.stack_off;
+			if (knod_prog->max_stack_off > meta->vreg.stack_off)
+				knod_prog->max_stack_off = meta->vreg.stack_off;
+			if (knod_prog->max_packet_off < meta->vreg.packet_off)
+				knod_prog->max_packet_off =
+					meta->vreg.packet_off;
+		}
+	}
+
+	if (is_mbpf_alu(meta))
+		err = knod_bpf_check_alu(knod_prog, meta, env);
+
+	/* less stack offset is bigger */
+	if (knod_prog->max_stack_off > meta->sreg.stack_off)
+		knod_prog->max_stack_off = meta->sreg.stack_off;
+	if (knod_prog->max_stack_off > meta->dreg.stack_off)
+		knod_prog->max_stack_off = meta->dreg.stack_off;
+	if (knod_prog->max_packet_off < meta->sreg.packet_off)
+		knod_prog->max_packet_off = meta->sreg.packet_off;
+	if (knod_prog->max_packet_off < meta->dreg.packet_off)
+		knod_prog->max_packet_off = meta->dreg.packet_off;
+
+out:
+	if (err)
+		pr_warn("knod_bpf: verifier rejected bpf insn %d (code 0x%02x off %d imm %d): %d\n",
+			insn_idx, meta->insn.code, meta->insn.off,
+			meta->insn.imm, err);
+	return err;
+}
+
+static int knod_bpf_finalize(struct bpf_verifier_env *env)
+{
+	return 0;
+}
+
+static int knod_bpf_offload(struct knod_dev *knodev,
+			    struct bpf_prog *prog, bool oldprog)
+{
+	struct knod_bpf_priv *priv = knodev->accel->xdp.priv;
+
+	WARN(!!knod_dev_offloaded(knodev) != oldprog,
+	     "bad offload state, expected offload %sto be active",
+	     oldprog ? "" : "not ");
+
+	WRITE_ONCE(priv->prog, prog);
+	knod_dev_offload(knodev, prog);
+
+	/*
+	 * Uninstalling the prog: reload the pass kernel now, while the prog's
+	 * maps are still valid, so the worker stops dispatching prog code that
+	 * is about to reference freed maps.
+	 */
+	if (!prog)
+		knod_bpf_reload_pass(knodev);
+
+	return 0;
+}
+
+static int knod_bpf_xdp_offload_prog(struct knod_dev *knodev,
+				     struct netdev_bpf *bpf)
+{
+	if (!knod_dev_active(knodev) && !bpf->prog)
+		return 0;
+
+	if (!knod_dev_active(knodev) && bpf->prog &&
+	    knodev->accel->xdp.bpf_offloaded) {
+		return -EBUSY;
+	}
+
+	return knod_bpf_offload(knodev, bpf->prog, knod_dev_active(knodev));
+}
+
+static int knod_bpf_xdp_set_prog(struct knod_dev *knodev,
+				 struct netdev_bpf *bpf)
+{
+	int err;
+
+	if (bpf->command == XDP_SETUP_PROG_HW) {
+		err = knod_bpf_xdp_offload_prog(knodev, bpf);
+		if (err)
+			return err;
+	}
+
+	xdp_attachment_setup(&knodev->accel->xdp.xdp_hw, bpf);
+
+	return 0;
+}
+
+static void knod_wait_vmcnt(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta)
+{
+	knod_emit(priv, meta, s_waitcnt_vmcnt);
+}
+
+static void knod_global_load_size_cache(struct knod_bpf_priv *priv,
+					struct knod_insn_meta *meta,
+					struct amdgcn_param32 *d,
+					struct amdgcn_param32 s,
+					int dst_idx, int start_off, int length)
+{
+	int off = start_off;
+
+	/* length is 4B aligned */
+	while (length) {
+		if (length >= 16) {
+			knod_emit(priv, meta, global_load_dwordx4, d[dst_idx],
+				  s, off);
+			length -= 16;
+			off += 16;
+			dst_idx += 4;
+		} else if (length >= 8) {
+			knod_emit(priv, meta, global_load_dwordx2, d[dst_idx],
+				  s, off);
+			length -= 8;
+			off += 8;
+			dst_idx += 2;
+		} else if (length >= 4) {
+			knod_emit(priv, meta, global_load_dword, d[dst_idx],
+				  s, off);
+			length -= 4;
+			off += 4;
+			dst_idx += 1;
+		}
+	}
+
+	knod_wait_vmcnt(priv, meta);
+}
+
+static void knod_global_store_size_cache(struct knod_bpf_priv *priv,
+					 struct knod_insn_meta *meta,
+					 struct amdgcn_param32 *d,
+					 struct amdgcn_param32 s,
+					 int dst_idx, int start_off, int length)
+{
+	int off = start_off;
+
+	/* length is 4B aligned */
+	while (length) {
+		if (length >= 16) {
+			knod_emit(priv, meta, global_store_dwordx4, d[dst_idx],
+				  s, off);
+			length -= 16;
+			off += 16;
+			dst_idx += 4;
+		} else if (length >= 8) {
+			knod_emit(priv, meta, global_store_dwordx2, d[dst_idx],
+				  s, off);
+			length -= 8;
+			off += 8;
+			dst_idx += 2;
+		} else if (length >= 4) {
+			knod_emit(priv, meta, global_store_dword, d[dst_idx],
+				  s, off);
+			length -= 4;
+			off += 4;
+			dst_idx += 1;
+		}
+	}
+
+	knod_wait_vmcnt(priv, meta);
+}
+
+static int knod_prog_prepare_insns(struct knod_bpf_priv *priv,
+				   struct knod_prog *knod_prog)
+{
+	struct amdgcn_param64 param64[3];
+	struct amdgcn_param32 param[10];
+	struct knod_insn_meta *meta;
+	int bs_shift;
+
+	meta = kzalloc_obj(*meta, GFP_KERNEL);
+	if (!meta)
+		return -ENOMEM;
+
+	meta->amdgpu_insn_idx = 0;
+
+	/* Invalidate SQC instruction cache so that a re-uploaded shader
+	 * at the same VRAM address is fetched from memory, not from the
+	 * stale I-cache.  Must be the very first instruction at the entry
+	 * point so that every shader version has s_icache_inv at the same
+	 * offset - the cached old version executes s_icache_inv too,
+	 * which flushes the cache before divergent code is reached.
+	 */
+	knod_emit(priv, meta, s_icache_inv);
+	knod_emit(priv, meta, s_waitcnt_vmcnt_lgkmcnt);
+
+	knod_sset32(&param[0], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_sset32(&param[1], KNOD_AMDGPU_ARG_SREG);
+	/* param = (__global struct _knod_bpf_param *)pkt.kernarg_address; */
+	knod_emit(priv, meta, s_load_dwordx2, param[0], param[1],
+		  offsetof(struct hsa_kernel_dispatch_packet, kernarg_address));
+
+	knod_vset32(&param[0], KNOD_AMDGPU_IDX_VREG);
+	knod_vset32(&param[1], KNOD_AMDGPU_VREG0_LO);
+	knod_iset32(&param[2], 0);
+	/* 10bits, lidx can up to 1024, Do not edit */
+	knod_iset32(&param[3], 10);
+	/* extract workitem ID to reserved vgpr register.
+	 * In the 2D-dispatch layout, IDX_VREG holds the per-workgroup tid
+	 * (0..workgroup_size_x-1).  queue_id = workgroup_id_y (s15).  The
+	 * flat index (queue_id * batch_size + local_idx) is computed later,
+	 * after queue_desc has been loaded and the v_cmpx bounds check has
+	 * narrowed EXEC to lanes with local_idx < count.
+	 */
+	knod_emit(priv, meta, v_bfe_i32, param[0], param[1], param[2],
+		  param[3]);
+	if (priv->batch_size > knod_bpf_workgroups) {
+		/* local_idx = workgroup_id_x * workgroup_size_x
+		 * + workitem_id.
+		 */
+		knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG5_LO);
+		knod_sset32(&param[1], KNOD_AMDGPU_WORKGROUP_ID_X_SREG);
+		knod_iset32(&param[2], knod_bpf_workgroups);
+		knod_emit(priv, meta,
+			v_mul_lo_u32, param[0], param[1], param[2]);
+		knod_vset32(&param[0], KNOD_AMDGPU_IDX_VREG);
+		knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG5_LO);
+		knod_vset32(&param[2], KNOD_AMDGPU_IDX_VREG);
+		knod_emit(priv, meta, v_add_u32, param[0], param[1], param[2]);
+	}
+	/* set frame pointer to 0 */
+	knod_sset32(&param[0], KNOD_AMDGPU_FRAME_POINTER_SREG);
+	knod_iset32(&param[1], 0);
+	knod_emit(priv, meta, s_mov_b32, param[0], param[1]);
+	/* wait for s_load_dwordx2 (kernarg_address) */
+	knod_emit(priv, meta, s_waitcnt_vmcnt_lgkmcnt);
+
+	/* load {nr_backlogs, _pad} from param (offset 0, 8-byte aligned) */
+	knod_sset32(&param[0], KNOD_AMDGPU_TMP_SREG1_LO);
+	knod_sset32(&param[1], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_emit(priv, meta, s_load_dwordx2, param[0], param[1], 0);
+	knod_emit(priv, meta, s_waitcnt_vmcnt_lgkmcnt);
+
+	/* NOTE: the bounds check `EXEC &= (tid < count)` is deferred until
+	 * after the queue descriptor load (queue<->workgroup binding).
+	 * nr_backlogs is no longer the right upper bound because lanes with
+	 * tid > this queue's count must be masked, not just the ones past
+	 * the aggregate backlog total.
+	 */
+
+	/* Initialize done_mask to 0 for structurized CFG */
+	knod_emit(priv, meta, s_mov_b64, knod_prog->done_mask_sreg,
+		  AMDGCN_SREG_INTEGER_0);
+
+	/* =========================================================
+	 * Queue-descriptor prep (2D dispatch: queue_id = workgroup_id_y)
+	 *
+	 * Loads this WG's queue descriptor from VRAM, performs the
+	 * per-lane bounds check (local_idx < queues[queue_id].count), and
+	 * converts IDX_VREG from local_idx to flat_IDX (queue_id *
+	 * batch_size + local_idx) which the rest of the prologue/BPF body
+	 * expects.  TMP_VREG9_LO is repurposed to hold the saved local
+	 * tid for use as local_idx in the slot-address step.
+	 * =========================================================
+	 */
+	bs_shift = ilog2(priv->batch_size);
+
+	/* a. queue_id = workgroup_id_y (broadcast scalar to VGPR LO). */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_sset32(&param[1], KNOD_AMDGPU_WORKGROUP_ID_Y_SREG);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+	knod_iset32(&param[1], 0);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+
+	/* b. Copy queue_id into TMP_VREG5_LO - separate scratch used as
+	 *    the v_mad src-multiplicand.  Avoids dst/src1 overlap on the
+	 *    following v_mad_u64_u32 (dst=TMP_VREG0_LO:HI).
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG5_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+
+	/* c. TMP_VREG0 = PARAM + queue_id * sizeof(queue_desc). */
+	knod_vset64(&param64[0], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_sset32(&param[0], KNOD_AMDGPU_TMP_SREG0_LO);
+	knod_iset32(&param[1],
+				 sizeof(struct knod_bpf_queue_desc));
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG5_LO);
+	knod_sset64(&param64[1], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_emit(priv, meta, v_mad_u64_u32, param64[0], param[0],
+		  param[1], param[2], param64[1]);
+
+	/* d. TMP_VREG0 += offsetof(queues). */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_iset32(&param[1],
+				 offsetof(struct knod_bpf_param, queues));
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_emit(priv, meta, v_add_co_u32, param[0], param[1], param[2]);
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+	knod_iset32(&param[1], 0);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, param[0], param[1],
+		  param[0]);
+
+	/* e. Load pool_gaddr + base_gaddr (offset 0, 16 bytes) into
+	 *    TMP_VREG1_LO..TMP_VREG2_HI (v24..v27 - must be consecutive).
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_emit(priv, meta, global_load_dwordx4, param[0], param[1], 0);
+
+	/* f. Load count + _pad + ring_start + ring_mask (offset 16, 16
+	 *    bytes) into TMP_VREG3_LO..TMP_VREG4_HI.
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG3_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_emit(priv, meta, global_load_dwordx4, param[0], param[1],
+		  offsetof(struct knod_bpf_queue_desc, count));
+	knod_emit(priv, meta, s_waitcnt_vmcnt);
+
+	/* g. Bounds check: EXEC &= (workitem_id < count). */
+	knod_vset32(&param[0], KNOD_AMDGPU_IDX_VREG);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG3_LO);
+	knod_emit(priv, meta, v_cmpx_lt_u32, param[0], param[1]);
+
+	/* Snapshot the in-bounds lane mask.  The unified epilogue uses this
+	 * to publish one verdict for every lane the dispatch claimed, even if
+	 * a malformed or newly added CFG path fails to join done_mask.
+	 */
+	knod_emit(priv, meta, s_mov_b64, knod_prog->initial_exec_sreg,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* h. Save per-queue local_idx to TMP_VREG9_LO.
+	 *    The slot-address step consumes this value; keeping it in a
+	 *    dedicated VGPR lets us overwrite IDX_VREG with flat_IDX for
+	 *    the CTX address computation that immediately follows.
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_IDX_VREG);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+
+	/* i. IDX_VREG = (queue_id << ilog2(batch_size)) + local_idx
+	 *    -> flat_IDX into the sub[] / sqw->bds[] arrays, matching the
+	 *    CPU-side layout `sqw->bds[queue_id * batch_size + local_idx]`.
+	 *    batch_size is rounded down to a power of two at start so the
+	 *    shift is exact.
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_IDX_VREG);
+	knod_sset32(&param[1], KNOD_AMDGPU_WORKGROUP_ID_Y_SREG);
+	knod_iset32(&param[2], bs_shift);
+	knod_vset32(&param[3], KNOD_AMDGPU_IDX_VREG);
+	knod_emit(priv, meta, v_lshl_add_u32, param[0], param[1],
+		  param[2], param[3]);
+
+	/* ctx = &param->sub[flat_IDX].ctx;
+	 * v_mad: VREG1 = sizeof(sub_obj) * flat_IDX + PARAM_SREG
+	 * then add offsetof(sub) = 8 to account for nr_backlogs/_pad
+	 */
+	knod_vset64(&param64[0], KNOD_AMDGPU_VREG1_LO);
+	knod_sset32(&param[0], KNOD_AMDGPU_TMP_SREG0_LO);
+	knod_iset32(&param[1], sizeof(struct knod_bpf_subparam_obj));
+	knod_vset32(&param[2], KNOD_AMDGPU_IDX_VREG);
+	knod_sset64(&param64[1], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_emit(priv, meta, v_mad_u64_u32, param64[0], param[0],
+		  param[1], param[2], param64[1]);
+	/* + offsetof(struct knod_bpf_param, sub) */
+	knod_vset32(&param[0], KNOD_AMDGPU_VREG1_LO);
+	knod_iset32(&param[1], offsetof(struct knod_bpf_param, sub));
+	knod_vset32(&param[2], KNOD_AMDGPU_VREG1_LO);
+	knod_emit(priv, meta, v_add_co_u32, param[0], param[1], param[2]);
+	knod_vset32(&param[0], KNOD_AMDGPU_VREG1_HI);
+	knod_iset32(&param[1], 0);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, param[0], param[1],
+		  param[0]);
+	knod_vset32(&param[0], KNOD_AMDGPU_CTX_VREG_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_VREG1_LO);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+	knod_vset32(&param[0], KNOD_AMDGPU_CTX_VREG_HI);
+	knod_vset32(&param[1], KNOD_AMDGPU_VREG1_HI);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+	knod_vset32(&param[0], KNOD_AMDGPU_FRAME_POINTER_VREG_LO);
+	knod_iset32(&param[1], 0x200);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+	knod_vset32(&param[0], KNOD_AMDGPU_FRAME_POINTER_VREG_HI);
+	knod_iset32(&param[1], 0);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+
+	/* 5. slot = (ring_start + local_idx) & ring_mask.
+	 *    local_idx = saved per-queue local_idx in TMP_VREG9_LO.
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG5_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG4_LO);
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_emit(priv, meta, v_add_u32, param[0], param[1], param[2]);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG4_HI);
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG5_LO);
+	knod_emit(priv, meta, v_and_b32_e32, param[0], param[1], param[2]);
+
+	/* Save backlog index before step 6 overwrites IDX_VREG -> SLOT_VREG.
+	 * v_mov_b32 BACKLOG_IDX_VREG(v58), IDX_VREG(v62)
+	 * Used in epilogue for XDP_PASS pass_indices[] write.
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_BACKLOG_IDX_VREG);
+	knod_vset32(&param[1], KNOD_AMDGPU_IDX_VREG);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+
+	/* 6. slot_addr = pool_gaddr + slot * spsc_stride, where
+	 *    spsc_stride = ALIGN(sizeof(spsc_bd), SMP_CACHE_BYTES)
+	 *    Compute directly into SLOT_VREG (v62:v63).
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_SLOT_VREG_LO);
+	knod_iset32(&param[1],
+		ilog2(ALIGN(sizeof(struct spsc_bd), SMP_CACHE_BYTES)));
+	knod_emit(priv, meta, v_lshlrev_b32, param[0], param[1], param[2]);
+	/* slot_addr = pool_gaddr + slot_offset */
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_emit(priv, meta, v_add_co_u32, param[0], param[0], param[1]);
+	knod_vset32(&param[0], KNOD_AMDGPU_SLOT_VREG_HI);
+	knod_iset32(&param[1], 0);
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG1_HI);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, param[0], param[1],
+		  param[2]);
+
+	/* 7. Load spsc_bd: {off(u16)|len(u16), page_idx} via single dwordx2
+	 *    TMP_VREG6_LO (v34) = off|len, TMP_VREG6_HI (v35) = page_idx
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG6_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_SLOT_VREG_LO);
+	knod_emit(priv, meta, global_load_dwordx2, param[0], param[1],
+		  offsetof(struct spsc_bd, off));
+	knod_emit(priv, meta, s_waitcnt_vmcnt);
+
+	/* 8. data = base_gaddr + (page_idx << PAGE_SHIFT) + off
+	 *    Compute directly into DATA_VREG (v64:v65).
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_iset32(&param[1], PAGE_SHIFT);
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG6_HI);
+	knod_emit(priv, meta, v_lshlrev_b32, param[0], param[1], param[2]);
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_VREG_HI);
+	knod_iset32(&param[1], 32 - PAGE_SHIFT);
+	knod_emit(priv, meta, v_lshrrev_b32, param[0], param[1], param[2]);
+
+	/* data = base_gaddr + page_gaddr */
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG2_LO);
+	knod_vset32(&param[2], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_emit(priv, meta, v_add_co_u32, param[0], param[1], param[2]);
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_VREG_HI);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG2_HI);
+	knod_vset32(&param[2], KNOD_AMDGPU_DATA_VREG_HI);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, param[0], param[1],
+		  param[2]);
+
+	if (knod_prog->uses_adjust) {
+		/* Save page_base to PAGE_BASE_VREG before adding off */
+		knod_vset32(&param[0], KNOD_AMDGPU_PAGE_BASE_VREG_LO);
+		knod_vset32(&param[1], KNOD_AMDGPU_DATA_VREG_LO);
+		knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+		knod_vset32(&param[0], KNOD_AMDGPU_PAGE_BASE_VREG_HI);
+		knod_vset32(&param[1], KNOD_AMDGPU_DATA_VREG_HI);
+		knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+	}
+
+	/* extract off (lower 16 bits of TMP_VREG6_LO) */
+	knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG8_LO);
+	knod_iset32(&param[1], 0xffff);
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG6_LO);
+	knod_emit(priv, meta, v_and_b32_e32, param[0], param[1], param[2]);
+
+	/* data += off */
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG8_LO);
+	knod_vset32(&param[2], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_emit(priv, meta, v_add_co_u32, param[0], param[1], param[2]);
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_VREG_HI);
+	knod_iset32(&param[1], 0);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, param[0], param[1],
+		  param[0]);
+
+	/* 9. data_end = data + len (upper 16 bits of TMP_VREG6_LO)
+	 *    Compute directly into DATA_END_VREG (v66:v67).
+	 */
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_END_VREG_LO);
+	knod_iset32(&param[1], 16);
+	knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG6_LO);
+	knod_emit(priv, meta, v_lshrrev_b32, param[0], param[1], param[2]);
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_END_VREG_LO);
+	knod_vset32(&param[1], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_vset32(&param[2], KNOD_AMDGPU_DATA_END_VREG_LO);
+	knod_emit(priv, meta, v_add_co_u32, param[0], param[1], param[2]);
+	knod_vset32(&param[0], KNOD_AMDGPU_DATA_END_VREG_HI);
+	knod_iset32(&param[1], 0);
+	knod_vset32(&param[2], KNOD_AMDGPU_DATA_VREG_HI);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, param[0], param[1],
+		  param[2]);
+
+	/* Step 10 eliminated: data->DATA_VREG, data_end->DATA_END_VREG,
+	 * slot_addr->SLOT_VREG computed directly in steps 6/8/9 above.
+	 */
+
+	pr_debug("knod_bpf DEBUG: prologue emitted idx=%u (KNOD_META_INSNS=%d)\n",
+		 meta->amdgpu_insns, KNOD_META_INSNS);
+	if (WARN_ON(meta->amdgpu_insns > KNOD_META_INSNS))
+		return -ENOSPC;
+	list_add_tail(&meta->l, &knod_prog->pre_insns);
+
+	return 0;
+}
+
+static int knod_prog_prepare(struct knod_bpf_priv *priv,
+			     struct knod_prog *knod_prog,
+			     const struct bpf_insn *prog,
+			     unsigned int cnt)
+{
+	struct knod_insn_meta *meta;
+	unsigned int i;
+
+	/* Pre-scan: detect helper 44/65 to set uses_adjust early */
+	for (i = 0; i < cnt; i++) {
+		if (prog[i].code == (BPF_JMP | BPF_CALL) &&
+		    (prog[i].imm == 44 || prog[i].imm == 65)) {
+			knod_prog->uses_adjust = true;
+			break;
+		}
+	}
+
+	knod_vset64(&r64[0], KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_vset64(&r64[1], KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_vset64(&r64[2], KNOD_AMDGPU_TMP_VREG2_LO);
+	knod_vset64(&r64[3], KNOD_AMDGPU_TMP_VREG3_LO);
+	knod_vset64(&r64[4], KNOD_AMDGPU_TMP_VREG4_LO);
+	knod_vset64(&r64[5], KNOD_AMDGPU_TMP_VREG5_LO);
+	knod_vset64(&r64[6], KNOD_AMDGPU_TMP_VREG6_LO);
+	knod_vset64(&r64[7], KNOD_AMDGPU_TMP_VREG7_LO);
+	knod_vset64(&r64[8], KNOD_AMDGPU_TMP_VREG8_LO);
+	knod_vset64(&r64[9], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset64(&r64[10], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_vset64(&r64[11], KNOD_AMDGPU_TMP_VREG11_LO);
+	knod_vset64(&r64[12], KNOD_AMDGPU_TMP_VREG12_LO);
+	knod_vset64(&r64[13], KNOD_AMDGPU_TMP_VREG13_LO);
+	knod_vset64(&r64[14], KNOD_AMDGPU_TMP_VREG14_LO);
+	knod_vset64(&r64[15], KNOD_AMDGPU_TMP_VREG15_LO);
+	knod_vset64(&r64[16], KNOD_AMDGPU_TMP_VREG16_LO);
+	knod_vset64(&r64[17], KNOD_AMDGPU_TMP_VREG17_LO);
+	knod_vset64(&r64[18], KNOD_AMDGPU_TMP_VREG18_LO);
+	knod_vset64(&r64[19], KNOD_AMDGPU_CTX_VREG_LO);
+
+	knod_sset64(&sr64[0], KNOD_AMDGPU_TMP_SREG0_LO);
+	knod_sset64(&sr64[1], KNOD_AMDGPU_TMP_SREG1_LO);
+	knod_sset64(&sr64[2], KNOD_AMDGPU_TMP_SREG2_LO);
+	knod_sset64(&sr64[3], KNOD_AMDGPU_TMP_SREG3_LO);
+	knod_sset64(&sr64[4], KNOD_AMDGPU_TMP_SREG4_LO);
+	knod_sset64(&sr64[5], KNOD_AMDGPU_TMP_SREG5_LO);
+
+	knod_vset64(&bpf_reg64[0], KNOD_AMDGPU_VREG0_LO);
+	knod_vset64(&bpf_reg64[1], KNOD_AMDGPU_VREG1_LO);
+	knod_vset64(&bpf_reg64[2], KNOD_AMDGPU_VREG2_LO);
+	knod_vset64(&bpf_reg64[3], KNOD_AMDGPU_VREG3_LO);
+	knod_vset64(&bpf_reg64[4], KNOD_AMDGPU_VREG4_LO);
+	knod_vset64(&bpf_reg64[5], KNOD_AMDGPU_VREG5_LO);
+	knod_vset64(&bpf_reg64[6], KNOD_AMDGPU_VREG6_LO);
+	knod_vset64(&bpf_reg64[7], KNOD_AMDGPU_VREG7_LO);
+	knod_vset64(&bpf_reg64[8], KNOD_AMDGPU_VREG8_LO);
+	knod_vset64(&bpf_reg64[9], KNOD_AMDGPU_VREG9_LO);
+	knod_vset64(&bpf_reg64[10], KNOD_AMDGPU_FRAME_POINTER_VREG_LO);
+
+	knod_vset32(&r32[0], KNOD_AMDGPU_TMP_VREG0_LO);
+	for (i = 1; i < 40; i++)
+		knod_vset32(&r32[i], r32[i - 1].v + 1);
+
+	if (knod_bpf_pkt_cache) {
+		int pkt_cache_start = knod_prog->uses_adjust ?
+			KNOD_AMDGPU_PKT_CACHE_VREG0 :
+			KNOD_AMDGPU_PAGE_BASE_VREG_LO;
+
+		knod_vset32(&pkt_cache[0], pkt_cache_start);
+		for (i = 1; i < 64; i++)
+			knod_vset32(&pkt_cache[i],
+						 pkt_cache[i - 1].v + 1);
+	}
+
+	knod_vset32(&stack[0], KNOD_AMDGPU_STACK_VREG0);
+	for (i = 1; i < 128; i++)
+		knod_vset32(&stack[i], stack[i - 1].v + 1);
+
+	for (i = 0; i < cnt; i++) {
+		meta = kzalloc_obj(*meta, GFP_KERNEL);
+		if (!meta)
+			return -ENOMEM;
+
+		meta->insn = prog[i];
+		meta->bpf_insn_idx = i;
+
+		list_add_tail(&meta->l, &knod_prog->insns);
+	}
+	knod_prog->n_insns = cnt;
+
+	return 0;
+}
+
+static void knod_prog_free(struct knod_prog *knod_prog)
+{
+	struct knod_insn_meta *meta, *tmp;
+
+	//kfree(knod_prog->subprog);
+
+	list_for_each_entry_safe(meta, tmp, &knod_prog->pre_insns, l) {
+		list_del(&meta->l);
+		kfree(meta);
+	}
+	list_for_each_entry_safe(meta, tmp, &knod_prog->insns, l) {
+		list_del(&meta->l);
+		kfree(meta);
+	}
+	list_for_each_entry_safe(meta, tmp, &knod_prog->post_insns, l) {
+		list_del(&meta->l);
+		kfree(meta);
+	}
+	kfree(knod_prog);
+}
+
+static int knod_bpf_verifier_prep(struct bpf_prog *prog)
+{
+	struct knod_prog *knod_prog;
+	struct knod_bpf_priv *priv;
+	int err;
+
+	knod_prog = kzalloc_obj(struct knod_prog, GFP_KERNEL);
+	if (!knod_prog)
+		return -ENOMEM;
+
+	INIT_LIST_HEAD(&knod_prog->insns);
+	INIT_LIST_HEAD(&knod_prog->pre_insns);
+	INIT_LIST_HEAD(&knod_prog->post_insns);
+	prog->aux->offload->dev_priv = knod_prog;
+	priv = bpf_offload_dev_priv(prog->aux->offload->offdev);
+	knod_prog->knodev = priv->knodev;
+	WRITE_ONCE(priv->knod_prog, knod_prog);
+	knod_prog->knod = priv->knod;
+	knod_prog->insn_idx = 0;
+
+	if (priv->isa_version == 10) {
+		knod_prog->done_mask_sreg = 32;
+		knod_prog->exec_save_base = 34;
+		knod_prog->initial_exec_sreg =
+			KNOD_AMDGPU_INITIAL_EXEC_SREG_GFX10;
+	} else {
+		knod_prog->done_mask_sreg = KNOD_AMDGPU_DONE_MASK_SREG;
+		knod_prog->exec_save_base = KNOD_AMDGPU_EXEC_SAVE_SREG_BASE;
+		knod_prog->initial_exec_sreg =
+			KNOD_AMDGPU_INITIAL_EXEC_SREG_GFX9;
+	}
+
+	err = knod_prog_prepare(priv, knod_prog, prog->insnsi, prog->len);
+	if (err)
+		goto err_free;
+
+	knod_prog->meta = knod_prog_first_meta(knod_prog);
+
+	return 0;
+
+err_free:
+	knod_prog_free(knod_prog);
+
+	return err;
+}
+
+static struct knod_insn_meta *knod_bpf_lookup_meta(struct knod_prog *knod_prog,
+						   short idx)
+{
+	struct knod_insn_meta *meta;
+
+	list_for_each_entry(meta, &knod_prog->insns, l) {
+		if (meta->amdgpu_insn_idx == AMDGPU_INSN_SKIP)
+			continue;
+		if (meta->bpf_insn_idx == idx)
+			return meta;
+	}
+
+	return NULL;
+}
+
+static void knod_mov64_imm(struct knod_bpf_priv *priv,
+			  struct knod_insn_meta *meta,
+			  int d, u64 imm64)
+{
+	struct amdgcn_param32 param[2];
+
+	knod_vset32(&param[0], d);
+	knod_iset32(&param[1], imm64 & ~0U);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+	knod_vset32(&param[0], d + 1);
+	knod_iset32(&param[1], imm64 >> 32);
+	knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+}
+
+static void knod_mov32(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param32 dst,
+		      struct amdgcn_param32 src)
+{
+	knod_emit(priv, meta, v_mov_b32_e32, dst, src);
+}
+
+static void knod_mov64(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param64 dst,
+		      struct amdgcn_param64 src)
+{
+	knod_mov32(priv, meta, dst.lo, src.lo);
+	knod_mov32(priv, meta, dst.hi, src.hi);
+}
+
+static void knod_add64(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param64 dst,
+		      struct amdgcn_param64 src0,
+		      struct amdgcn_param64 src1)
+{
+	knod_emit(priv, meta, v_add_co_u32, dst.lo, src0.lo, src1.lo);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, dst.hi, src0.hi,
+		  src1.hi);
+}
+
+/* No carry out/in */
+static void knod_add32(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param32 dst,
+		      struct amdgcn_param32 src0,
+		      struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_add_u32, dst, src0, src1);
+}
+
+static void knod_xor32(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param32 dst,
+		      struct amdgcn_param32 src0,
+		      struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_xor_b32_e32, dst, src0, src1);
+}
+
+static void knod_alignbit32(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param32 dst,
+			   struct amdgcn_param32 src0,
+			   struct amdgcn_param32 src1,
+			   struct amdgcn_param32 src2)
+{
+	knod_emit(priv, meta, v_alignbit_b32, dst, src0, src1, src2);
+}
+
+static void knod_bfe32(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param32 dst,
+			   struct amdgcn_param32 src0,
+			   struct amdgcn_param32 src1,
+			   struct amdgcn_param32 src2)
+{
+	knod_emit(priv, meta, v_bfe_u32, dst, src0, src1, src2);
+}
+
+static void knod_bfi32(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param32 dst,
+			   struct amdgcn_param32 src0,
+			   struct amdgcn_param32 src1,
+			   struct amdgcn_param32 src2)
+{
+	knod_emit(priv, meta, v_bfi_b32, dst, src0, src1, src2);
+}
+
+static void knod_lshrrev32(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param32 dst,
+			   struct amdgcn_param32 src0,
+			   struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_lshrrev_b32, dst, src0, src1);
+}
+
+static void knod_lshrrev64(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param64 dst,
+			   struct amdgcn_param64 src0,
+			   struct amdgcn_param64 src1)
+{
+	knod_emit(priv, meta, v_lshrrev_b64, dst, src0, src1);
+}
+
+static void knod_ashrrev32(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param32 dst,
+			   struct amdgcn_param32 src0,
+			   struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_ashrrev_i32, dst, src0, src1);
+}
+
+static void knod_ashrrev64(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param64 dst,
+			   struct amdgcn_param64 src0,
+			   struct amdgcn_param64 src1)
+{
+	knod_emit(priv, meta, v_ashrrev_i64, dst, src0, src1);
+}
+
+static void knod_lshlrev32(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param32 dst,
+			   struct amdgcn_param32 src0,
+			   struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_lshlrev_b32, dst, src0, src1);
+}
+
+static void knod_lshlrev64(struct knod_bpf_priv *priv,
+			   struct knod_insn_meta *meta,
+			   struct amdgcn_param64 dst,
+			   struct amdgcn_param64 src0,
+			   struct amdgcn_param64 src1)
+{
+	knod_emit(priv, meta, v_lshlrev_b64, dst, src0, src1);
+}
+
+/* No carry out/in */
+static void knod_sub32(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param32 dst,
+		      struct amdgcn_param32 src0,
+		      struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_sub_u32, dst, src0, src1);
+}
+
+static void knod_and32(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param32 dst,
+		      struct amdgcn_param32 src0,
+		      struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_and_b32_e32, dst, src0, src1);
+}
+
+static void knod_and64(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param64 dst,
+		      struct amdgcn_param64 src0,
+		      struct amdgcn_param64 src1)
+{
+	knod_and32(priv, meta, dst.lo, src0.lo, src1.lo);
+	knod_and32(priv, meta, dst.hi, src0.hi, src1.hi);
+}
+
+static void knod_or32(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param32 dst,
+		      struct amdgcn_param32 src0,
+		      struct amdgcn_param32 src1)
+{
+	knod_emit(priv, meta, v_or_b32_e32, dst, src0, src1);
+}
+
+static void knod_sub64(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param64 dst,
+		      struct amdgcn_param64 src0,
+		      struct amdgcn_param64 src1)
+{
+	knod_emit(priv, meta, v_sub_co_u32, dst.lo, src0.lo, src1.lo);
+	knod_emit(priv, meta, v_sub_co_ci_u32_e32, dst.hi, src0.hi,
+		  src1.hi);
+}
+
+static void knod_subrev64(struct knod_bpf_priv *priv,
+			 struct knod_insn_meta *meta,
+			 struct amdgcn_param64 dst,
+			 struct amdgcn_param64 src0,
+			 struct amdgcn_param64 src1)
+{
+	knod_emit(priv, meta, v_subrev_co_u32, dst.lo, src0.lo, src1.lo);
+	knod_emit(priv, meta, v_subrev_co_ci_u32_e32, dst.hi, src0.hi,
+		  src1.hi);
+}
+
+static void knod_mul_lo32(struct knod_bpf_priv *priv,
+			 struct knod_insn_meta *meta,
+			 struct amdgcn_param32 dst,
+			 struct amdgcn_param32 src1,
+			 struct amdgcn_param32 src2)
+{
+	knod_emit(priv, meta, v_mul_lo_u32, dst, src1, src2);
+}
+
+static void knod_mul_hi32(struct knod_bpf_priv *priv,
+			 struct knod_insn_meta *meta,
+			 struct amdgcn_param32 dst,
+			 struct amdgcn_param32 src1,
+			 struct amdgcn_param32 src2)
+{
+	knod_emit(priv, meta, v_mul_hi_u32, dst, src1, src2);
+}
+
+static void knod_mul64(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      struct amdgcn_param64 dst,
+		      struct amdgcn_param64 src1,
+		      struct amdgcn_param64 src2,
+		      struct amdgcn_param64 tmp)
+{
+	/*
+	 * v_mul_lo_u32 v1, v2, v1
+	 * v_mul_hi_u32 v5, v2, v0
+	 * v_mul_lo_u32 v3, v3, v0
+	 * v_mul_lo_u32 v0, v2, v0
+	 * v_add_u32_e32 v1, v5, v1
+	 * v_add_u32_e32 v1, v1, v3
+	 *
+	 * v[0:1] = src1, dst
+	 * v[2:3] = src2
+	 * v5 = tmp
+	 */
+
+	/* v_mul_lo_u32 v1, v2, v1 */
+	knod_mul_lo32(priv, meta, src2.hi, src1.lo, src2.hi);
+	/* v_mul_hi_u32 v5, v2, v0 */
+	knod_mul_hi32(priv, meta, tmp.lo, src1.lo, src2.lo);
+	/* v_mul_lo_u32 v3, v3, v0 */
+	knod_mul_lo32(priv, meta, src1.hi, src1.hi, src2.lo);
+	/* v_mul_lo_u32 v0, v2, v0 */
+	knod_mul_lo32(priv, meta, src1.lo, src1.lo, src2.lo);
+	/* v_add_u32_e32 v1, v5, v1 */
+	knod_add32(priv, meta, src2.lo, tmp.lo, src2.hi);
+	/* v_add_u32_e32 v1, v1, v3 */
+	knod_add32(priv, meta, src1.hi, src2.lo, src1.hi);
+	knod_mov32(priv, meta, dst.lo, src1.lo);
+	knod_mov32(priv, meta, dst.hi, src1.hi);
+}
+
+static void knod_div(struct knod_bpf_priv *priv,
+		    struct knod_insn_meta *meta,
+		    struct amdgcn_param64 dst,
+		    struct amdgcn_param64 imm,
+		    struct amdgcn_param64 tmp_reg0,
+		    struct amdgcn_param64 tmp_reg1,
+		    struct amdgcn_param64 tmp_reg2,
+		    struct amdgcn_param64 tmp_reg3)
+{
+	struct reciprocal_value_adv rvalue;
+	struct amdgcn_param64 p64[4];
+	u8 pre_shift, exp;
+
+	WARN_ON((imm.lo.type != AMDGCN_PARAM_TYPE_INTEGER_0) &&
+		     (imm.lo.type != AMDGCN_PARAM_TYPE_LITERAL_CONST));
+	WARN_ON((imm.hi.type != AMDGCN_PARAM_TYPE_INTEGER_0) &&
+		     (imm.hi.type != AMDGCN_PARAM_TYPE_LITERAL_CONST));
+	knod_iset64(&p64[0], 0);
+	knod_iset64(&p64[1], 0);
+	knod_iset64(&p64[2], 0);
+	knod_iset64(&p64[3], 0);
+	/*
+	 * dst := imm
+	 * n := dst_reg
+	 */
+	if (imm.imm > U32_MAX) {
+		knod_mov64(priv, meta, dst, p64[0]);
+		return;
+	}
+
+	if (imm.imm >= 1U << 31) {
+		/* result = n >= dst; */
+		knod_mov64(priv, meta, tmp_reg0, imm);
+		knod_emit(priv, meta, v_cmp_ge_u64, dst, tmp_reg0);
+		return;
+	}
+
+	rvalue = reciprocal_value_adv(imm.lo.v, 32);
+	exp = rvalue.exp;
+	if (rvalue.is_wide_m && !(imm.lo.v & 1)) {
+		pre_shift = fls(imm.lo.v & -imm.lo.v) - 1;
+		rvalue = reciprocal_value_adv(imm.lo.v >> pre_shift,
+					      32 - pre_shift);
+	} else {
+		pre_shift = 0;
+	}
+
+	if (imm.lo.v == 1U << exp) {
+		knod_iset64(&p64[0], exp);
+		/* n = n >> exp */
+		knod_lshrrev64(priv, meta, dst, p64[0], dst);
+		return;
+	} else if (rvalue.is_wide_m) {
+		/*
+		 * pre_shift must be zero when reached here.
+		 * t = (n * rvalue.m) >> 32;
+		 * result = n - t;
+		 * result >>= 1;
+		 * result += t;
+		 * result >>= rvalue.sh - 1;
+		 */
+
+		/*
+		 * n := VREG0
+		 * t := VREG1
+		 * rvalue.m := VREG2
+		 * tmp := VREG3
+		 */
+
+		/* n := TMP_VREG0 */
+		knod_mov64(priv, meta, tmp_reg0, dst);
+
+		knod_iset64(&p64[0], rvalue.m);
+		/* rvalue.m := TMP_VREG2 */
+		knod_mov64(priv, meta, tmp_reg2, p64[0]);
+
+		/* t = n * rvalue.m; */
+		knod_mul64(priv, meta,
+			   tmp_reg1, /* t */
+			   tmp_reg0, /* n */
+			   tmp_reg2, /* rvalue.m */
+			   tmp_reg3); /* tmp */
+
+		/* t >>= 32; */
+		knod_iset64(&p64[0], 0);
+		knod_mov32(priv, meta, tmp_reg1.lo, tmp_reg1.hi);
+		knod_mov32(priv, meta, tmp_reg1.hi, p64[0].lo);
+
+		/* result = n - t */
+		knod_sub64(priv, meta, dst, dst, tmp_reg1);
+
+		/* result >>= 1 */
+		knod_iset64(&p64[0], 1);
+		knod_lshrrev64(priv, meta, dst, p64[0], dst);
+
+		/* result += t; */
+		knod_add64(priv, meta,
+			   dst,
+			   dst, /* result */
+			   tmp_reg1); /* t */
+
+		/* result >>= rvalue.sh - 1; */
+		knod_iset64(&p64[0], rvalue.sh - 1);
+		WARN_ON(rvalue.sh - 1 > 31);
+		knod_lshrrev64(priv, meta, dst, p64[0], dst);
+		return;
+	}
+
+	/*
+	 * if (pre_shift)
+	 *   result = n >> pre_shift;
+	 * result = ((u64)result * rvalue.m) >> 32;
+	 * result >>= rvalue.sh;
+	 */
+
+	/*
+	 * n := VREG0
+	 * <NONE> := VREG1
+	 * rvalue.m := VREG2
+	 * tmp := VREG3
+	 * result := dst * 2
+	 */
+
+	/* n := TMP_VREG0 */
+	knod_mov64(priv, meta, tmp_reg0, dst);
+
+	/* rvalue.m := TMP_VREG2 */
+	knod_iset64(&p64[0], rvalue.m);
+	knod_mov64(priv, meta, tmp_reg2, p64[0]);
+
+	if (pre_shift) {
+		/* result = n >> pre_shift; */
+		knod_iset64(&p64[0], pre_shift);
+		knod_lshrrev64(priv, meta, dst, p64[0],
+			       tmp_reg0); /* n */
+	} else {
+		/* tmp = 0 */
+		knod_iset64(&p64[0], 0);
+		knod_mov64(priv, meta, tmp_reg0, p64[0]);
+	}
+
+	/* result = result * rvalue.m; */
+	knod_mul64(priv, meta,
+		   dst, /* result */
+		   dst, /* result */
+		   tmp_reg2, /* rvalue.m */
+		   tmp_reg3); /* tmp */
+
+	/* result >>= (32 + rvalue.sh); */
+	knod_iset64(&p64[0], 32 + rvalue.sh);
+	knod_lshrrev64(priv, meta, dst, p64[0], dst);
+}
+
+static void knod_mod(struct knod_bpf_priv *priv,
+		    struct knod_insn_meta *meta,
+		    struct amdgcn_param64 dst,
+		    struct amdgcn_param64 imm,
+		    struct amdgcn_param64 tmp_reg0,
+		    struct amdgcn_param64 tmp_reg1,
+		    struct amdgcn_param64 tmp_reg2,
+		    struct amdgcn_param64 tmp_reg3,
+		    struct amdgcn_param64 tmp_reg4)
+{
+	WARN_ON((imm.lo.type != AMDGCN_PARAM_TYPE_INTEGER_0) &&
+		     (imm.lo.type != AMDGCN_PARAM_TYPE_LITERAL_CONST));
+	WARN_ON((imm.hi.type != AMDGCN_PARAM_TYPE_INTEGER_0) &&
+		     (imm.hi.type != AMDGCN_PARAM_TYPE_LITERAL_CONST));
+	/* q := tmp_reg0 */
+	knod_mov64(priv, meta, tmp_reg0, dst);
+	/* q = n / imm */
+	knod_div(priv, meta, tmp_reg0, imm,
+		     tmp_reg1, tmp_reg2, tmp_reg3, tmp_reg4);
+
+	/* tmp_reg1 := imm_reg */
+	knod_mov64(priv, meta, tmp_reg1, imm);
+
+	/* imm * q := tmp_reg3 */
+	knod_mul64(priv, meta,
+		   tmp_reg3, /* imm * q */
+		   tmp_reg0, /* q */
+		   tmp_reg1, /* imm_reg */
+		   tmp_reg2); /* tmp */
+
+	knod_sub64(priv, meta, dst, dst, tmp_reg3);
+}
+
+/*
+ * Fast constant modulo on the 32-bit value in @dst.lo for divisors of a
+ * special form, avoiding knod_mod's reciprocal divide + 64-bit multiply:
+ *   2^k     -> dst & (2^k-1)                     (mask)
+ *   2^k + 1 -> lo - hi (+C if lo<hi)             (Fermat: 2^k = -1 mod C)
+ *   2^k - 1 -> lo + hi (-C while >=C)            (Mersenne: 2^k = 1 mod C)
+ * lo/hi are the low/high k-bit halves.  One fold is exact for a 32-bit
+ * dividend when 2^k covers the high half (true for e.g. 65537 = 2^16+1,
+ * kondor's per-packet `hash % RING_SIZE`).  Returns false for other
+ * divisors (caller falls back to knod_mod).  Scratch: r64[0], r64[1].
+ */
+static bool knod_mod_k32(struct knod_bpf_priv *priv,
+			 struct knod_insn_meta *meta,
+			 struct amdgcn_param64 dst, u32 imm)
+{
+	struct amdgcn_param32 p;
+	int i;
+
+	if (is_power_of_2(imm)) {
+		knod_iset32(&p, imm - 1);
+		knod_and32(priv, meta, dst.lo, p, dst.lo);
+	} else if (is_power_of_2(imm - 1) && (imm - 1) >= (1u << 16)) {
+		knod_iset32(&p, imm - 2);			/* mask 2^k-1 */
+		knod_and32(priv, meta, r64[0].lo, p, dst.lo);	/* lo */
+		knod_iset32(&p, ilog2(imm - 1));		/* k */
+		knod_emit(priv, meta, v_lshrrev_b32, r64[1].lo, p, dst.lo);
+		/* lo-hi */
+		knod_sub32(priv, meta, dst.lo, r64[0].lo, r64[1].lo);
+		knod_emit(priv, meta, v_cmp_lt_u32, r64[0].lo, r64[1].lo);
+		knod_iset32(&p, imm);
+		knod_add32(priv, meta, r64[1].lo, p, dst.lo);	/* +C */
+		knod_emit(priv, meta, v_cndmask_b32_e32, dst.lo, dst.lo,
+			  r64[1].lo);
+	} else if (is_power_of_2(imm + 1) && (imm + 1) >= (1u << 16)) {
+		knod_iset32(&p, imm);				/* mask 2^k-1 */
+		knod_and32(priv, meta, r64[0].lo, p, dst.lo);	/* lo */
+		knod_iset32(&p, ilog2(imm + 1));		/* k */
+		knod_emit(priv, meta, v_lshrrev_b32, r64[1].lo, p, dst.lo);
+		/* lo+hi */
+		knod_add32(priv, meta, dst.lo, r64[0].lo, r64[1].lo);
+		knod_iset32(&p, imm);
+		knod_mov32(priv, meta, r64[0].lo, p);		/* C in VGPR */
+		for (i = 0; i < 2; i++) {			/* r < 2C */
+			knod_emit(priv, meta, v_cmp_le_u32, r64[0].lo, dst.lo);
+			knod_sub32(priv, meta, r64[1].lo, dst.lo, r64[0].lo);
+			knod_emit(priv, meta, v_cndmask_b32_e32, dst.lo,
+				  dst.lo, r64[1].lo);
+		}
+	} else {
+		return false;
+	}
+
+	knod_iset32(&p, 0);
+	knod_mov32(priv, meta, dst.hi, p);
+	return true;
+}
+
+/* Considered to be able to use all temporary vregisters
+ * Also, key is stack pointer, not global
+ */
+static void knod_jhash(struct knod_bpf_priv *priv,
+		      struct knod_insn_meta *meta,
+		      u32 dst_idx, u32 length, u32 initval)
+{
+	u32 a_reg = TREG32_MAX - 3, b_reg = TREG32_MAX - 2;
+	u32 c_reg = TREG32_MAX - 1, d_reg = TREG32_MAX;
+	u32 key_in_pkt = KEY_IN_PKT_32;
+	struct amdgcn_param32 p32;
+
+	knod_iset32(&p32, JHASH_INITVAL + length + initval);
+	knod_mov32(priv, meta, r32[a_reg], p32);
+	knod_mov32(priv, meta, r32[b_reg], p32);
+	knod_mov32(priv, meta, r32[c_reg], p32);
+	knod_iset32(&p32, 0);
+
+	while (length > 12) {
+		/* a += *key; */
+		knod_add32(priv, meta, r32[a_reg], r32[a_reg],
+			       r32[key_in_pkt]);
+		/* b += *(key + 4); */
+		knod_add32(priv, meta, r32[b_reg], r32[b_reg],
+			       r32[key_in_pkt + 1]);
+		/* c += *(key + 8); */
+		knod_add32(priv, meta, r32[c_reg], r32[c_reg],
+			       r32[key_in_pkt + 2]);
+		/* a -= c; */
+		knod_sub32(priv, meta, r32[a_reg], r32[a_reg], r32[c_reg]);
+		/* a ^= rol32(c, 4); */
+		knod_iset32(&p32, 32 - 4);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[c_reg],
+				    r32[c_reg], p32);
+		knod_xor32(priv, meta,
+			       r32[a_reg], r32[a_reg], r32[d_reg]);
+		/* c += b; */
+		knod_add32(priv, meta, r32[c_reg], r32[c_reg], r32[b_reg]);
+		/* b -= a; */
+		knod_sub32(priv, meta, r32[b_reg], r32[b_reg], r32[a_reg]);
+		/* b ^= rol32(a, 6); */
+		knod_iset32(&p32, 32 - 6);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[a_reg],
+				    r32[a_reg], p32);
+		knod_xor32(priv, meta,
+			       r32[b_reg], r32[b_reg], r32[d_reg]);
+		/*a += c; */
+		knod_add32(priv, meta, r32[a_reg], r32[a_reg], r32[c_reg]);
+		/* c -= b; */
+		knod_sub32(priv, meta, r32[c_reg], r32[c_reg], r32[b_reg]);
+		/* c ^= rol32(b, 8); */
+		knod_iset32(&p32, 32 - 8);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[b_reg],
+				    r32[b_reg], p32);
+		knod_xor32(priv, meta,
+			       r32[c_reg], r32[c_reg], r32[d_reg]);
+		/* b += a; */
+		knod_add32(priv, meta, r32[b_reg], r32[b_reg], r32[a_reg]);
+		/* a -= c; */
+		knod_sub32(priv, meta, r32[a_reg], r32[a_reg], r32[c_reg]);
+		/* a ^= rol32(c, 16); */
+		knod_iset32(&p32, 32 - 16);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[c_reg],
+				    r32[c_reg], p32);
+		knod_xor32(priv, meta, r32[a_reg], r32[a_reg], r32[d_reg]);
+		/* c += b; */
+		knod_add32(priv, meta, r32[c_reg], r32[c_reg], r32[b_reg]);
+		/* b -= a; */
+		knod_sub32(priv, meta, r32[b_reg], r32[b_reg], r32[a_reg]);
+		/* b ^= rol32(a, 19); */
+		knod_iset32(&p32, 32 - 19);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[a_reg],
+				    r32[a_reg], p32);
+		knod_xor32(priv, meta, r32[b_reg], r32[b_reg], r32[d_reg]);
+		/* a += c; */
+		knod_add32(priv, meta, r32[a_reg], r32[a_reg], r32[c_reg]);
+		/* c -= b; */
+		knod_sub32(priv, meta, r32[c_reg], r32[c_reg], r32[b_reg]);
+		/* c ^= rol32(b, 4); */
+		knod_iset32(&p32, 32 - 4);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[b_reg],
+				    r32[b_reg], p32);
+		knod_xor32(priv, meta, r32[c_reg], r32[c_reg], r32[d_reg]);
+		/* b += a; */
+		knod_add32(priv, meta, r32[b_reg], r32[b_reg], r32[a_reg]);
+		length -= 12;
+		key_in_pkt += 3;
+	}
+
+	switch (length) {
+	case 12:
+		/* c += (unsigned int)k[11]<<24; */
+		fallthrough;
+	case 11:
+		/* c += (unsigned int)k[10]<<16; */
+		fallthrough;
+	case 10:
+		/* c += (unsigned int)k[9]<<8; */
+		fallthrough;
+	case 9:
+		/* c += k[8]; */
+		knod_add32(priv, meta, r32[c_reg], r32[c_reg],
+			       r32[key_in_pkt + 2]);
+		fallthrough;
+	case 8:
+		/* b += (unsigned int)k[7]<<24; */
+		fallthrough;
+	case 7:
+		/* b += (unsigned int)k[6]<<16; */
+		fallthrough;
+	case 6:
+		/* b += (unsigned int)k[5]<<8; */
+		fallthrough;
+	case 5:
+		/* b += k[4]; */
+		knod_add32(priv, meta, r32[b_reg], r32[b_reg],
+			       r32[key_in_pkt + 1]);
+		fallthrough;
+	case 4:
+		/* a += (unsigned int)k[3]<<24; */
+		fallthrough;
+	case 3:
+		/* a += (unsigned int)k[2]<<16; */
+		fallthrough;
+	case 2:
+		/* a += (unsigned int)k[1]<<8; */
+		fallthrough;
+	case 1:
+		/* a += k[0]; */
+		knod_add32(priv, meta, r32[a_reg], r32[a_reg],
+			       r32[key_in_pkt]);
+		/* c ^= b; */
+		knod_xor32(priv, meta, r32[c_reg], r32[c_reg], r32[b_reg]);
+		/* c -= rol32(b, 14); */
+		knod_iset32(&p32, 32 - 14);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[b_reg],
+				    r32[b_reg], p32);
+		knod_sub32(priv, meta, r32[c_reg], r32[c_reg], r32[d_reg]);
+		/* a ^= c; */
+		knod_xor32(priv, meta, r32[a_reg], r32[a_reg], r32[c_reg]);
+		/* a -= rol32(c, 11); */
+		knod_iset32(&p32, 32 - 11);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[c_reg],
+				    r32[c_reg], p32);
+		knod_sub32(priv, meta, r32[a_reg], r32[a_reg], r32[d_reg]);
+		/* b ^= a; */
+		knod_xor32(priv, meta, r32[b_reg], r32[b_reg], r32[a_reg]);
+		/* b -= rol32(a, 25); */
+		knod_iset32(&p32, 32 - 25);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[a_reg],
+				    r32[a_reg], p32);
+		knod_sub32(priv, meta, r32[b_reg], r32[b_reg], r32[d_reg]);
+		/* c ^= b; */
+		knod_xor32(priv, meta, r32[c_reg], r32[c_reg], r32[b_reg]);
+		/* c -= rol32(b, 16); */
+		knod_iset32(&p32, 32 - 16);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[b_reg],
+				    r32[b_reg], p32);
+		knod_sub32(priv, meta, r32[c_reg], r32[c_reg], r32[d_reg]);
+		/* a ^= c; */
+		knod_xor32(priv, meta, r32[a_reg], r32[a_reg], r32[c_reg]);
+		/* a -= rol32(c, 4); */
+		knod_iset32(&p32, 32 - 4);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[c_reg],
+				    r32[c_reg], p32);
+		knod_sub32(priv, meta, r32[a_reg], r32[a_reg], r32[d_reg]);
+		/* b ^= a; */
+		knod_xor32(priv, meta, r32[b_reg], r32[b_reg], r32[a_reg]);
+		/* b -= rol32(a, 14); */
+		knod_iset32(&p32, 32 - 14);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[a_reg],
+				    r32[a_reg], p32);
+		knod_sub32(priv, meta, r32[b_reg], r32[b_reg], r32[d_reg]);
+		/* c ^= b; */
+		knod_xor32(priv, meta, r32[c_reg], r32[c_reg], r32[b_reg]);
+		/* c -= rol32(b, 24); */
+		knod_iset32(&p32, 32 - 24);
+		knod_alignbit32(priv, meta, r32[d_reg], r32[b_reg],
+				    r32[b_reg], p32);
+		knod_sub32(priv, meta, r32[c_reg], r32[c_reg], r32[d_reg]);
+		break;
+	case 0: /* Nothing left to add */
+		break;
+	}
+
+	knod_mov32(priv, meta, r64[dst_idx].lo, r32[c_reg]);
+}
+
+static u64 knod_bpf_map_gaddr(struct knod_bpf_priv *priv, int id)
+{
+	struct knod_dev *knodev = priv->knodev;
+	struct knod_bpf_map *knod_map;
+	struct knod_mem *mem;
+
+	mutex_lock(&knodev->lock);
+	list_for_each_entry(knod_map, &knodev->accel->xdp.bound_maps, list) {
+		if (knod_map->offmap->map.id == id) {
+			mem = knod_map->mem;
+			mutex_unlock(&knodev->lock);
+			return (u64)mem->gaddr;
+		}
+	}
+	mutex_unlock(&knodev->lock);
+
+	return 0;
+}
+
+static void *knod_bpf_map_kaddr(struct knod_bpf_priv *priv, int id)
+{
+	struct knod_dev *knodev = priv->knodev;
+	struct knod_bpf_map *knod_map;
+	struct knod_mem *mem;
+
+	mutex_lock(&knodev->lock);
+	list_for_each_entry(knod_map, &knodev->accel->xdp.bound_maps, list) {
+		if (knod_map->offmap->map.id == id) {
+			mem = knod_map->mem;
+
+			/* GPUVM */
+			mutex_unlock(&knodev->lock);
+			return (void *)mem->kaddr;
+		}
+	}
+	mutex_unlock(&knodev->lock);
+
+	return NULL;
+}
+
+static u64 knod_bpf_get_map_gaddr(struct knod_bpf_priv *priv,
+				  struct knod_insn_meta *meta1,
+				  struct knod_insn_meta *meta2)
+{
+	struct bpf_map *map;
+
+	map = (void *)(unsigned long)((u32)meta1->insn.imm |
+			(u64)meta2->insn.imm << 32);
+
+	return knod_bpf_map_gaddr(priv, map->id);
+}
+
+static int knod_bpf_get_map_id(struct knod_bpf_priv *priv,
+			       struct knod_insn_meta *meta1,
+			       struct knod_insn_meta *meta2)
+{
+	struct bpf_map *map;
+
+	map = (void *)(unsigned long)((u32)meta1->insn.imm |
+			(u64)meta2->insn.imm << 32);
+
+	return map->id;
+}
+
+static int knod_bpf_get_amdgpu_insn_idx(struct knod_bpf_priv *priv,
+					struct knod_insn_meta *meta,
+					int t)
+{
+	int i, insn_idx = meta->amdgpu_insn_idx;
+
+	for (i = 0; i < t; i++)
+		insn_idx += meta->amdgpu_insn[i].size / 4;
+
+	return insn_idx;
+}
+
+static void knod_bpf_fixup_branch(struct knod_bpf_priv *priv,
+				  struct amdgcn_branch_fixup *fixup)
+{
+	int target_off = knod_bpf_get_amdgpu_insn_idx(priv,
+						      fixup->target_label->meta,
+			fixup->target_label->insn_idx);
+	int cur_off = knod_bpf_get_amdgpu_insn_idx(priv,
+						   fixup->meta,
+						   fixup->insn_idx);
+	cur_off++;
+
+	target_off -= cur_off;
+	emit_branch_fixup(priv->isa_version,
+			  &fixup->meta->amdgpu_insn[fixup->insn_idx],
+			  target_off);
+	knod_jit_dbg(" target_off was updated to %d\n", target_off);
+}
+
+static void knod_bpf_set_fixup(struct knod_insn_meta *meta,
+			       struct amdgcn_branch_fixup *fixup,
+			       struct amdgcn_label *target_label,
+			       int insn_idx)
+{
+	fixup->meta = meta;
+	fixup->insn_idx = insn_idx;
+	fixup->target_label = target_label;
+}
+
+static void knod_bpf_set_label(struct knod_insn_meta *meta,
+			       struct amdgcn_label *label,
+			       int insn_idx)
+{
+	label->meta = meta;
+	label->insn_idx = insn_idx;
+}
+
+/*
+ * knod_bpf_emit_offlen_writeback - Write updated off/len to spsc_bd.
+ *
+ * Uses PAGE_BASE_VREG (set once in prologue), computes
+ * new_off = DATA_VREG_LO - PAGE_BASE_VREG_LO and
+ * new_len = DATA_END_VREG_LO - DATA_VREG_LO, packs them as (len<<16)|off,
+ * and stores the result at spsc_bd.off via SLOT_VREG.
+ *
+ * Clobbers: TMP_VREG2 (v26:v27).
+ */
+static void knod_bpf_emit_offlen_writeback(struct knod_bpf_priv *priv,
+					  struct knod_insn_meta *meta)
+{
+	struct amdgcn_param32 s0, s1, data_lo, data_end_lo, pbase_lo, slot_lo;
+	struct amdgcn_param32 imm;
+
+	knod_vset32(&s0, KNOD_AMDGPU_TMP_VREG2_LO);
+	knod_vset32(&s1, KNOD_AMDGPU_TMP_VREG2_HI);
+	knod_vset32(&data_lo, KNOD_AMDGPU_DATA_VREG_LO);
+	knod_vset32(&data_end_lo, KNOD_AMDGPU_DATA_END_VREG_LO);
+	knod_vset32(&pbase_lo, KNOD_AMDGPU_PAGE_BASE_VREG_LO);
+	knod_vset32(&slot_lo, KNOD_AMDGPU_SLOT_VREG_LO);
+
+	/* s0 = len = DATA_END_LO - DATA_LO */
+	knod_sub32(priv, meta, s0, data_end_lo, data_lo);
+
+	/* s0 = len << 16 */
+	knod_iset32(&imm, 16);
+	knod_lshlrev32(priv, meta, s0, imm, s0);
+
+	/* s1 = off = DATA_LO - page_base_lo */
+	knod_sub32(priv, meta, s1, data_lo, pbase_lo);
+
+	/* s0 = (len << 16) | off */
+	knod_or32(priv, meta, s0, s0, s1);
+
+	/* Store packed {off, len} to spsc_bd */
+	knod_emit(priv, meta, global_store_dword, s0, slot_lo,
+		  offsetof(struct spsc_bd, off));
+}
+
+/*
+ * knod_bpf_xdp_adjust_head - JIT bpf_xdp_adjust_head (helper 44).
+ *
+ * R2 = delta (signed 32-bit).  Adjusts DATA_VREG by delta.
+ * Bounds: page_base <= DATA_VREG <= DATA_END_VREG - ETH_HLEN.
+ * Each bound is checked with its own VOPC, but VCC is captured into
+ * VGPRs via v_cndmask (VALU) rather than SGPRs via s_mov_b64 (SALU).
+ * VALU reads VCC correctly after VOPC; only SALU suffers the GFX10
+ * dual-VOPC stale-read hazard.
+ * page_base is reloaded on demand from param + spsc_bd.
+ * On failure, DATA_VREG is restored and R0 = -EINVAL.
+ * On success, R0 = 0.
+ *
+ * Clobbers: TMP_VREG0 (v22:v23), TMP_VREG1 (v24:v25), TMP_VREG2 (v26:v27),
+ *           TMP_SREG0 (s16), TMP_SREG2 (s20:s21).
+ */
+static void knod_bpf_xdp_adjust_head(struct knod_bpf_priv *priv,
+				    struct knod_insn_meta *meta)
+{
+	struct amdgcn_param32 ub_lo, ub_hi, dend_lo, dend_hi, sext_dst;
+	struct amdgcn_param32 shift_amt;
+	struct amdgcn_param32 tmp0_lo, tmp0_hi, data_lo, data_hi, fail_lo;
+	struct amdgcn_param32 fail_hi;
+	struct amdgcn_param64 data_vreg, pbase_vreg, ub;
+	struct amdgcn_param32 r0_lo, r0_hi, imm, delta;
+
+	knod_vset64(&data_vreg, KNOD_AMDGPU_DATA_VREG_LO);
+
+	knod_vset32(&tmp0_lo, KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_vset32(&tmp0_hi, KNOD_AMDGPU_TMP_VREG0_HI);
+	knod_vset32(&data_lo, KNOD_AMDGPU_DATA_VREG_LO);
+	knod_vset32(&data_hi, KNOD_AMDGPU_DATA_VREG_HI);
+	knod_vset32(&r0_lo, KNOD_AMDGPU_VREG0_LO);
+	knod_vset32(&r0_hi, KNOD_AMDGPU_VREG0_HI);
+	knod_vset32(&delta, bpf_reg64[2].lo.v);
+	knod_vset32(&fail_lo, KNOD_AMDGPU_TMP_VREG2_LO);
+	knod_vset32(&fail_hi, KNOD_AMDGPU_TMP_VREG2_HI);
+
+	/* 1. Save original DATA_VREG -> TMP_VREG0 */
+	knod_mov32(priv, meta, tmp0_lo, data_lo);
+	knod_mov32(priv, meta, tmp0_hi, data_hi);
+
+	/* 2. DATA_VREG += delta (R2.lo, sign-extended to 64-bit) */
+	knod_emit(priv, meta, v_add_co_u32, data_lo, delta, data_lo);
+
+	knod_vset32(&sext_dst, KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_iset32(&shift_amt, 31);
+	knod_emit(priv, meta, v_ashrrev_i32, sext_dst, shift_amt, delta);
+
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, data_hi, sext_dst,
+		  data_hi);
+
+	/* 3. Lower bound: DATA_VREG < page_base -> VCC = fail */
+	knod_vset64(&pbase_vreg, KNOD_AMDGPU_PAGE_BASE_VREG_LO);
+	knod_emit(priv, meta, v_cmp_lt_u64, data_vreg, pbase_vreg);
+
+	/*
+	 * Capture VCC -> VGPR via v_cndmask (VALU reads VCC correctly,
+	 * unlike SALU which suffers the dual-VOPC stale-read hazard).
+	 */
+	knod_iset32(&imm, 1);
+	knod_mov32(priv, meta, fail_hi, imm);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, fail_lo, imm, fail_hi);
+
+	/* 5. Upper bound: DATA_VREG > DATA_END_VREG - ETH_HLEN */
+	knod_vset32(&ub_lo, KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_vset32(&ub_hi, KNOD_AMDGPU_TMP_VREG1_HI);
+	knod_vset32(&dend_lo, KNOD_AMDGPU_DATA_END_VREG_LO);
+	knod_vset32(&dend_hi, KNOD_AMDGPU_DATA_END_VREG_HI);
+
+	knod_iset32(&imm, ETH_HLEN);
+	/*
+	 * v_sub_co_u32 is VOP2 on GFX9, whose vsrc1 must be a VGPR (a literal
+	 * there reads v0). Subtraction is not commutative, so materialise
+	 * ETH_HLEN into a scratch VGPR (ub_hi, overwritten by the high half
+	 * below) and use it as src1 instead of an immediate.
+	 */
+	knod_mov32(priv, meta, ub_hi, imm);
+	knod_emit(priv, meta, v_sub_co_u32, ub_lo, dend_lo, ub_hi);
+	knod_iset32(&imm, 0);
+	knod_mov32(priv, meta, delta, imm);
+	knod_emit(priv, meta, v_sub_co_ci_u32_e32, ub_hi, dend_hi, delta);
+
+	knod_vset64(&ub, KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_emit(priv, meta, v_cmp_gt_u64, data_vreg, ub);
+
+	/* Capture upper_fail via v_cndmask, combine, convert to VCC */
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, fail_hi, imm, fail_hi);
+
+	knod_emit(priv, meta, v_or_b32_e32, fail_lo, fail_lo, fail_hi);
+
+	knod_emit(priv, meta, v_cmp_lt_u32, imm, fail_lo);
+
+	/* 6. Conditional restore: VCC=1(fail) -> original,
+	 *    VCC=0(pass) -> adjusted
+	 */
+	knod_emit(priv, meta, v_cndmask_b32_e32, data_lo, data_lo,
+		  tmp0_lo);
+	knod_emit(priv, meta, v_cndmask_b32_e32, data_hi, data_hi,
+		  tmp0_hi);
+
+	/* 7. R0 = VCC ? -EINVAL : 0 */
+	knod_iset32(&imm, -EINVAL);
+	knod_mov32(priv, meta, tmp0_lo, imm);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, r0_lo, imm, tmp0_lo);
+
+	knod_iset32(&imm, -1);
+	knod_mov32(priv, meta, tmp0_hi, imm);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, r0_hi, imm, tmp0_hi);
+}
+
+/*
+ * knod_bpf_xdp_adjust_tail - JIT bpf_xdp_adjust_tail (helper 65).
+ *
+ * R2 = delta (signed 32-bit).  Adjusts DATA_END_VREG by delta.
+ * Bounds: DATA_VREG + ETH_HLEN <= DATA_END_VREG <= page_base + PAGE_SIZE.
+ * Each bound is checked with its own VOPC, but VCC is captured into
+ * VGPRs via v_cndmask (VALU) rather than SGPRs via s_mov_b64 (SALU).
+ * VALU reads VCC correctly after VOPC; only SALU suffers the GFX10
+ * dual-VOPC stale-read hazard.
+ * page_base is reloaded on demand from param + spsc_bd.
+ * On failure, DATA_END_VREG is restored and R0 = -EINVAL.
+ * On success, R0 = 0.
+ *
+ * Clobbers: TMP_VREG0 (v22:v23), TMP_VREG1 (v24:v25), TMP_VREG2 (v26:v27),
+ *           TMP_SREG0 (s16), TMP_SREG2 (s20:s21).
+ */
+static void knod_bpf_xdp_adjust_tail(struct knod_bpf_priv *priv,
+				    struct knod_insn_meta *meta)
+{
+	struct amdgcn_param32 tmp0_lo, tmp0_hi, dend_lo, dend_hi, fail_lo;
+	struct amdgcn_param32 fail_hi;
+	struct amdgcn_param32 lb_lo, lb_hi, d_lo, d_hi, sext_dst, shift_amt;
+	struct amdgcn_param32 pb_src_lo, pb_src_hi;
+	struct amdgcn_param32 r0_lo, r0_hi;
+	struct amdgcn_param32 imm, delta;
+	struct amdgcn_param64 dend_vreg, lb;
+
+	knod_vset32(&tmp0_lo, KNOD_AMDGPU_TMP_VREG0_LO);
+	knod_vset32(&tmp0_hi, KNOD_AMDGPU_TMP_VREG0_HI);
+	knod_vset32(&dend_lo, KNOD_AMDGPU_DATA_END_VREG_LO);
+	knod_vset32(&dend_hi, KNOD_AMDGPU_DATA_END_VREG_HI);
+	knod_vset32(&r0_lo, KNOD_AMDGPU_VREG0_LO);
+	knod_vset32(&r0_hi, KNOD_AMDGPU_VREG0_HI);
+	knod_vset32(&delta, bpf_reg64[2].lo.v);
+	knod_vset32(&fail_lo, KNOD_AMDGPU_TMP_VREG2_LO);
+	knod_vset32(&fail_hi, KNOD_AMDGPU_TMP_VREG2_HI);
+	knod_vset64(&dend_vreg, KNOD_AMDGPU_DATA_END_VREG_LO);
+
+	/* 1. Save original DATA_END_VREG -> TMP_VREG0 */
+	knod_mov32(priv, meta, tmp0_lo, dend_lo);
+	knod_mov32(priv, meta, tmp0_hi, dend_hi);
+
+	/* 2. DATA_END_VREG += delta (R2.lo, sign-extended) */
+	knod_emit(priv, meta, v_add_co_u32, dend_lo, delta, dend_lo);
+
+	knod_vset32(&sext_dst, KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_iset32(&shift_amt, 31);
+	knod_emit(priv, meta, v_ashrrev_i32, sext_dst, shift_amt, delta);
+
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, dend_hi, sext_dst,
+		  dend_hi);
+
+	/* 3. Lower bound: lb = DATA + ETH_HLEN -> TMP_VREG1 */
+	knod_vset32(&lb_lo, KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_vset32(&lb_hi, KNOD_AMDGPU_TMP_VREG1_HI);
+	knod_vset32(&d_lo, KNOD_AMDGPU_DATA_VREG_LO);
+	knod_vset32(&d_hi, KNOD_AMDGPU_DATA_VREG_HI);
+
+	knod_iset32(&imm, ETH_HLEN);
+	knod_emit(priv, meta, v_add_co_u32, lb_lo, imm, d_lo);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, lb_hi, imm, d_hi);
+
+	/* VOPC#1: DATA_END < lb -> VCC = lower_fail */
+	knod_vset64(&lb, KNOD_AMDGPU_TMP_VREG1_LO);
+	knod_emit(priv, meta, v_cmp_lt_u64, dend_vreg, lb);
+
+	/*
+	 * Capture VCC -> VGPR via v_cndmask (VALU reads VCC correctly,
+	 * unlike SALU which suffers the GFX10 dual-VOPC stale-read hazard).
+	 */
+	knod_iset32(&imm, 1);
+	knod_mov32(priv, meta, fail_hi, imm);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, fail_lo, imm, fail_hi);
+
+	/* 4. Upper bound: ub = page_base + PAGE_SIZE -> TMP_VREG1 */
+	knod_vset32(&pb_src_lo, KNOD_AMDGPU_PAGE_BASE_VREG_LO);
+	knod_vset32(&pb_src_hi, KNOD_AMDGPU_PAGE_BASE_VREG_HI);
+	knod_mov32(priv, meta, lb_lo, pb_src_lo);
+	knod_mov32(priv, meta, lb_hi, pb_src_hi);
+
+	knod_iset32(&imm, PAGE_SIZE);
+	knod_emit(priv, meta, v_add_co_u32, lb_lo, imm, lb_lo);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_add_co_ci_u32_e32, lb_hi, imm, lb_hi);
+
+	/* VOPC#2: DATA_END > ub -> VCC = upper_fail */
+	knod_emit(priv, meta, v_cmp_gt_u64, dend_vreg, lb);
+
+	/* Capture upper_fail via v_cndmask, combine, convert to VCC */
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, fail_hi, imm, fail_hi);
+
+	knod_emit(priv, meta, v_or_b32_e32, fail_lo, fail_lo, fail_hi);
+
+	knod_emit(priv, meta, v_cmp_lt_u32, imm, fail_lo);
+
+	/* 5. Conditional restore: VCC=1(fail) -> original,
+	 *    VCC=0(pass) -> adjusted
+	 */
+	knod_emit(priv, meta, v_cndmask_b32_e32, dend_lo, dend_lo,
+		  tmp0_lo);
+	knod_emit(priv, meta, v_cndmask_b32_e32, dend_hi, dend_hi,
+		  tmp0_hi);
+
+	/* 6. R0 = VCC ? -EINVAL : 0 */
+	knod_iset32(&imm, -EINVAL);
+	knod_mov32(priv, meta, tmp0_lo, imm);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, r0_lo, imm, tmp0_lo);
+
+	knod_iset32(&imm, -1);
+	knod_mov32(priv, meta, tmp0_hi, imm);
+	knod_iset32(&imm, 0);
+	knod_emit(priv, meta, v_cndmask_b32_e32, r0_hi, imm, tmp0_hi);
+}
+
+static void knod_bpf_load_size(struct knod_bpf_priv *priv,
+			      struct knod_insn_meta *meta,
+			      struct amdgcn_param64 *dst,
+			      /* packet or stack */
+			      struct amdgcn_param32 *cache,
+			      int size, int off)
+{
+	struct amdgcn_param32 p32[2];
+
+	knod_jit_dbg(" %d: off = %d off_4 = %d size = %d\n", meta->bpf_insn_idx,
+		off, off%4, size);
+	switch (size) {
+	case sizeof(unsigned long):
+		if ((off % 4) == 0) {
+			knod_mov32(priv, meta, dst->lo, cache[off / 4]);
+			knod_mov32(priv, meta, dst->hi,
+				       cache[(off / 4) + 1]);
+		} else if ((off % 4) == 1) {
+			WARN_ON_ONCE(1);
+		} else if ((off % 4) == 2) {
+			WARN_ON_ONCE(1);
+		} else {
+			WARN_ON_ONCE(1);
+		}
+		break;
+	case sizeof(unsigned int):
+		if ((off % 4) == 0) {
+			knod_mov32(priv, meta, dst->lo, cache[off / 4]);
+		} else if ((off % 4) == 1) {
+			knod_iset32(&p32[0], 8);
+			knod_lshrrev32(priv, meta, r32[0], p32[0],
+					   cache[off / 4]);
+			knod_iset32(&p32[0], 24);
+			knod_lshlrev32(priv, meta, dst->lo, p32[0],
+					   cache[(off / 4) + 1]);
+			knod_or32(priv, meta, dst->lo, dst->lo, r32[0]);
+		} else if ((off % 4) == 2) {
+			knod_iset32(&p32[0], 16);
+			knod_lshrrev32(priv, meta, r32[0], p32[0],
+					   cache[off / 4]);
+			knod_lshlrev32(priv, meta, dst->lo, p32[0],
+					   cache[(off / 4) + 1]);
+			knod_or32(priv, meta, dst->lo, dst->lo, r32[0]);
+		} else {
+			knod_iset32(&p32[0], 24);
+			knod_lshrrev32(priv, meta, r32[0], p32[0],
+					   cache[off / 4]);
+			knod_iset32(&p32[0], 8);
+			knod_lshlrev32(priv, meta, dst->lo, p32[0],
+					   cache[(off / 4) + 1]);
+			knod_or32(priv, meta, dst->lo, dst->lo, r32[0]);
+		}
+		break;
+	case sizeof(unsigned short):
+		if ((off % 4) == 3) {
+			knod_iset32(&p32[0], 24);
+			knod_iset32(&p32[1], 8);
+			knod_bfe32(priv, meta, r64[0].lo, cache[off / 4],
+				       p32[0], p32[1]);
+			knod_iset32(&p32[0], 0);
+			knod_bfe32(priv, meta, r64[0].hi,
+				       cache[(off / 4) + 1], p32[0], p32[1]);
+			/* bpf_reg64[d].lo = (r64[0].hi << 8) | r64[0].lo. */
+			knod_emit(priv, meta, v_lshl_or_b32, dst->lo,
+				  r64[0].hi, p32[1], r64[0].lo);
+		} else {
+			if (!(off % 4))
+				knod_iset32(&p32[0], 0);
+			else if ((off % 4) == 1)
+				knod_iset32(&p32[0], 8);
+			else if ((off % 4) == 2)
+				knod_iset32(&p32[0], 16);
+			knod_iset32(&p32[1], 16);
+			knod_bfe32(priv, meta, dst->lo, cache[off / 4],
+				       p32[0], p32[1]);
+		}
+		break;
+	case sizeof(unsigned char):
+		if ((off % 4) == 0)
+			knod_iset32(&p32[0], 0);
+		else if ((off % 4) == 1)
+			knod_iset32(&p32[0], 8);
+		else if ((off % 4) == 2)
+			knod_iset32(&p32[0], 16);
+		else
+			knod_iset32(&p32[0], 24);
+		knod_iset32(&p32[1], 8);
+		knod_bfe32(priv, meta, dst->lo, cache[off / 4], p32[0],
+			       p32[1]);
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		break;
+	}
+
+	if (size != sizeof(unsigned long)) {
+		knod_iset32(&p32[0], 0);
+		knod_mov32(priv, meta, dst->hi, p32[0]);
+	}
+}
+
+/*
+ * GFX10 (RDNA2) quirk: global_load_{dword,dwordx2,dwordx4} silently
+ * clear the low 2 bits of the effective address, forcing Dword
+ * alignment. For PTR_TO_PACKET loads at a byte offset that is not
+ * Dword-aligned, round the offset down to the nearest 4-byte boundary,
+ * load enough contiguous dwords to cover the requested range, then use
+ * v_alignbit_b32 to extract the byte-aligned result. For size < 4 a
+ * final v_and_b32 masks the result to the correct width.
+ *
+ * Caller is responsible for zeroing dst.hi for size < 8; this helper
+ * only writes dst.lo (and dst.hi when size == 8).
+ *
+ * Scratch: up to 4 contiguous VGPRs at v32..v35
+ * (TMP_VREG5_LO..TMP_VREG6_HI).
+ */
+static void knod_bpf_emit_gfx10_unaligned_load(struct knod_bpf_priv *priv,
+					      struct knod_insn_meta *meta,
+					      int size,
+					      struct amdgcn_param64 dst,
+					      struct amdgcn_param32 src_lo,
+					      int off)
+{
+	int off_a = off & ~3;
+	int shift_bits = (off - off_a) * 8;
+	int needed = DIV_ROUND_UP((off & 3) + size, 4);
+	struct amdgcn_param32 tmp[4];
+	struct amdgcn_param32 shift_imm, mask_imm;
+
+	knod_vset32(&tmp[0], KNOD_AMDGPU_TMP_VREG5_LO);
+	knod_vset32(&tmp[1], KNOD_AMDGPU_TMP_VREG5_HI);
+	knod_vset32(&tmp[2], KNOD_AMDGPU_TMP_VREG6_LO);
+	knod_vset32(&tmp[3], KNOD_AMDGPU_TMP_VREG6_HI);
+	knod_iset32(&shift_imm, shift_bits);
+
+	if (needed <= 1) {
+		knod_emit(priv, meta, global_load_dword, tmp[0], src_lo,
+			  off_a);
+	} else if (needed == 2) {
+		knod_emit(priv, meta, global_load_dwordx2, tmp[0], src_lo,
+			  off_a);
+	} else {
+		/* needed == 3: no dwordx3, widen to dwordx4. */
+		knod_emit(priv, meta, global_load_dwordx4, tmp[0], src_lo,
+			  off_a);
+	}
+	knod_wait_vmcnt(priv, meta);
+
+	if (size <= 4) {
+		/* v_alignbit_b32 D, S0, S1, S2:
+		 *   D = ({S0, S1} >> S2)[31:0]
+		 * S0 is HIGH, S1 is LOW. tmp[0] holds
+		 * bytes[off_a..+4) (memory-low) and tmp[1] holds
+		 * bytes[off_a+4..+8) (memory-high), so
+		 * src0=tmp[1], src1=tmp[0].
+		 */
+		if (shift_bits == 0)
+			knod_mov32(priv, meta, dst.lo, tmp[0]);
+		else
+			knod_alignbit32(priv, meta, dst.lo,
+					    tmp[1], tmp[0], shift_imm);
+
+		if (size == 1) {
+			knod_iset32(&mask_imm, 0xff);
+			knod_and32(priv, meta, dst.lo, dst.lo,
+				       mask_imm);
+		} else if (size == 2) {
+			knod_iset32(&mask_imm, 0xffff);
+			knod_and32(priv, meta, dst.lo, dst.lo,
+				       mask_imm);
+		}
+	} else {
+		/* size == 8: two alignbits for low / high output dwords. */
+		if (shift_bits == 0) {
+			knod_mov32(priv, meta, dst.lo, tmp[0]);
+			knod_mov32(priv, meta, dst.hi, tmp[1]);
+		} else {
+			knod_alignbit32(priv, meta, dst.lo,
+					    tmp[1], tmp[0], shift_imm);
+			knod_alignbit32(priv, meta, dst.hi,
+					    tmp[2], tmp[1], shift_imm);
+		}
+	}
+}
+
+#define LABEL_NEXT	8
+#define LABEL_OUT	9
+static void knod_bpf_ktime_get_ns(struct knod_bpf_priv *priv,
+				 struct knod_insn_meta *meta)
+{
+	struct amdgcn_param32 p[2];
+
+	knod_sset32(&p[0], KNOD_AMDGPU_TMP_SREG0_LO);
+	knod_sset32(&p[1], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_emit(priv, meta, s_load_dwordx2, p[0], p[1],
+		  offsetof(struct knod_bpf_param, ktime_ns));
+
+	knod_emit(priv, meta, s_waitcnt_lgkmcnt);
+
+	knod_sset32(&p[0], KNOD_AMDGPU_TMP_SREG0_LO);
+	knod_mov32(priv, meta, bpf_reg64[0].lo, p[0]);
+	knod_sset32(&p[0], KNOD_AMDGPU_TMP_SREG0_HI);
+	knod_mov32(priv, meta, bpf_reg64[0].hi, p[0]);
+}
+
+static void knod_bpf_map_lookup(struct knod_bpf_priv *priv,
+			       struct knod_insn_meta *meta,
+			       int map_id)
+{
+	struct knod_bpf_map_obj *knod_map_obj_k, *knod_map_obj_g;
+	int off, len, _len, idx, key_in_pkt, key_in_map;
+	bool first_cmp;
+	struct amdgcn_branch_fixup fixups[12] = {0,};
+	struct amdgcn_label labels[10] = {0,};
+	u32 stack_off = meta->kreg.stack_off;
+	unsigned long bucket_gaddr;
+	struct amdgcn_param32 p32;
+	int fixup_idx = 0;
+
+	knod_map_obj_k =
+		(struct knod_bpf_map_obj *)knod_bpf_map_kaddr(priv, map_id);
+	knod_map_obj_g =
+		(struct knod_bpf_map_obj *)knod_bpf_map_gaddr(priv, map_id);
+	bucket_gaddr = (unsigned long)knod_map_obj_g +
+		       offsetof(struct knod_bpf_map_obj, bucket);
+
+	knod_jit_dbg(" stack_off = %d map_id = %d\n", stack_off, map_id);
+	if (!knod_map_obj_g || !knod_map_obj_k)
+		WARN_ON_ONCE(1);
+
+	knod_bpf_load_size(priv, meta,
+			       &r64[2],
+			       &stack[0],
+			       sizeof(unsigned int),
+			       512 + stack_off);
+	/* reg1 := bucket
+	 * NOTE: bucket_gaddr is greater than X
+	 */
+	knod_iset64(&p64[0], bucket_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+	knod_iset64(&p64[1], 0);
+	knod_mov32(priv, meta, r64[2].hi, p64[1].lo);
+
+	knod_iset64(&p64[1], 0);
+	knod_mov64(priv, meta, bpf_reg64[0], p64[1]);
+
+	/* BPF_REG0 = 0
+	 * TMP_REG1 = bucket_gaddr
+	 * TMP_REG2 = key
+	 */
+
+	if (knod_map_obj_k->map_type == BPF_MAP_TYPE_ARRAY ||
+	    knod_map_obj_k->map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
+		/* if (key > knod_map_obj_k.max_entries)
+		 * NOTE: integer
+		 */
+		knod_iset64(&p64[1], knod_map_obj_k->max_entries);
+		knod_mov64(priv, meta, r64[3], p64[1]);
+		knod_emit(priv, meta, v_cmp_ge_u64, r64[2], r64[3]);
+		/* structurized CFG: save OOB lanes, narrow exec */
+		knod_emit(priv, meta, s_and_b64, KNOD_AMDGPU_TMP_SREG3_LO,
+			  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+		knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+			  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+			emit_s_cbranch_execz(priv->isa_version,
+				     &meta->amdgpu_insn[meta->amdgpu_insns],
+				     0); /* update required */
+		knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+				   &labels[LABEL_OUT], meta->amdgpu_insns);
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+		fixup_idx++;
+		/* PERCPU_ARRAY: bucket += workgroup_id_y * per_instance_size so
+		 * each RX queue addresses its own instance and the atomic
+		 * update after the lookup has no cross-CU contention.  Auto
+		 * xgroups keeps PERCPU programs at one workgroup per queue, so
+		 * the queue id (workgroup_id_y) is the instance index.
+		 */
+		if (knod_map_obj_k->map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
+			/* r64[3] is scratch after the bounds check: .lo =
+			 * workgroup_id_y, .hi = per_instance_size (too large
+			 * for an inline constant, so stage both in VGPRs
+			 * first).
+			 */
+			knod_sset32(&p32, KNOD_AMDGPU_WORKGROUP_ID_Y_SREG);
+			knod_emit(priv, meta, v_mov_b32_e32, r64[3].lo, p32);
+			knod_iset64(&p64[1],
+				    knod_map_obj_k->meta.ameta
+				    .per_instance_size);
+			knod_mov32(priv, meta, r64[3].hi, p64[1].lo);
+			emit_v_mad_u64_u32(priv->isa_version,
+					&meta->amdgpu_insn[meta->amdgpu_insns],
+					   r64[1],
+					   sr64[0].lo,
+					   r64[3].hi, /* per_instance_size */
+					   r64[3].lo, /* workgroup_id_y */
+					   r64[1]); /* bucket */
+			debug_insn(priv->isa_version,
+				   &meta->amdgpu_insn[meta->amdgpu_insns]);
+			meta->amdgpu_insns++;
+		}
+		/* elem_id = &bucket[key]; */
+		knod_iset64(&p64[1], knod_map_obj_k->value_size);
+		emit_v_mad_u64_u32(priv->isa_version,
+				   &meta->amdgpu_insn[meta->amdgpu_insns],
+				   bpf_reg64[0],
+				   sr64[0].lo,
+				   p64[1].lo, /* value_size */
+				   r64[2].lo, /* key */
+				   r64[1]); /* bucket */
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+		/* structurized CFG: restore OOB lanes */
+		knod_bpf_set_label(meta, &labels[LABEL_OUT],
+				   meta->amdgpu_insns);
+		knod_emit(priv, meta, s_or_b64, AMDGCN_SREG_EXEC_LO,
+			  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG3_LO);
+		for (idx = 0; idx < fixup_idx; idx++)
+			knod_bpf_fixup_branch(priv, &fixups[idx]);
+	} else if (knod_map_obj_k->map_type == BPF_MAP_TYPE_HASH) {
+		key_in_pkt = KEY_IN_PKT_64;
+		len = knod_map_obj_k->key_size;
+		off = stack_off;
+
+		/* TMP_VREGs(vgpr-pair)
+		 * |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|
+		 * | | | |K|K|K|K|K|K|K|K |K |K |K |K |K |K |K |K |
+		 */
+		while (len) {
+			if (len >= sizeof(unsigned long))
+				_len = sizeof(unsigned long);
+			else
+				_len = len;
+			knod_bpf_load_size(priv, meta,
+					       &r64[key_in_pkt],
+					       &stack[0],
+					       _len,
+					       512 + off);
+			key_in_pkt++;
+			len -= _len;
+			off += _len;
+		}
+
+		knod_jhash(priv, meta,
+			       2,
+			       knod_map_obj_k->key_size,
+			       knod_map_obj_k->meta.hmeta.hashrnd);
+		/* clear hi register of r64[2] because hash is 32bit */
+		knod_iset64(&p64[0], 0);
+		knod_mov32(priv, meta, r64[2].hi, p64[0].lo);
+
+		/* hash = hash & (n_buckets - 1) before indexing the bucket
+		 * array -- jhash returns the full 32-bit hash and the update
+		 * and delete emitters mask it too; without this
+		 * bucket_gaddr[hash] runs off the end of the bucket array.
+		 */
+		knod_iset32(&p64[0].lo,
+			    knod_map_obj_k->meta.hmeta.n_buckets - 1);
+		knod_and32(priv, meta, r64[2].lo, p64[0].lo, r64[2].lo);
+
+		/* elem_id = bucket_gaddr[hash]; */
+		knod_iset64(&p64[1], sizeof(int));
+		emit_v_mad_u64_u32(priv->isa_version,
+				   &meta->amdgpu_insn[meta->amdgpu_insns],
+				   r64[2],
+				   sr64[0].lo,
+				   p64[1].lo, /* sizeof(int) */
+				   r64[2].lo, /* hash */
+				   r64[1]); /* bucket */
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+
+		/* bpf_reg64[0] = 0 (default return for not-found lanes) */
+		knod_iset64(&p64[0], 0);
+		knod_mov64(priv, meta, bpf_reg64[0], p64[0]);
+
+		/* structurized CFG: save initial exec for restoring at end */
+		knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG3_LO,
+			  AMDGCN_SREG_EXEC_LO);
+
+		knod_bpf_set_label(meta, &labels[LABEL_NEXT],
+				   meta->amdgpu_insns);
+		/* load elem_id from elem structure */
+		knod_emit(priv, meta, global_load_dword, r64[2].lo,
+			  r64[2].lo, 0);
+		knod_wait_vmcnt(priv, meta);
+
+		/* mask out DELETED bit from next field */
+		knod_iset32(&p32, KNOD_BPF_HASH_NEXT_MASK);
+		knod_emit(priv, meta, v_and_b32_e32, r64[2].lo, p32,
+			  r64[2].lo);
+
+		/* if (r64[2].lo == KNOD_BPF_HASH_NEXT_END)
+		 *	goto out;
+		 * VOPC cannot encode literal constants - move to VGPR first.
+		 * NEXT_MASK == NEXT_END (0x7FFFFFFF), reuse p32 from v_and
+		 * above.
+		 */
+		knod_emit(priv, meta, v_mov_b32_e32, r64[0].hi, p32);
+		knod_emit(priv, meta, v_cmp_eq_u32, r64[0].hi, r64[2].lo);
+		/* structurized CFG: remove end-of-chain lanes */
+		knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+			  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+
+		emit_s_cbranch_execz(priv->isa_version,
+				     &meta->amdgpu_insn[meta->amdgpu_insns],
+				     0); /* update required */
+		knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+				   &labels[LABEL_OUT], meta->amdgpu_insns);
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+		fixup_idx++;
+
+		knod_iset64(&p64[0],
+			    (unsigned long)knod_map_obj_k->meta.hmeta.elems);
+		knod_mov64(priv, meta, r64[1], p64[0]);
+		knod_iset64(&p64[0], knod_map_obj_k->meta.hmeta.elem_size);
+		knod_mov64(priv, meta, r64[0], p64[0]);
+
+		key_in_map = KEY_IN_MAP_32;
+		len = knod_map_obj_k->key_size;
+		off = offsetof(struct knod_bpf_hash_elem_obj, kv);
+
+		/* elem = &elems[elem_id]; */
+		emit_v_mad_u64_u32(priv->isa_version,
+				   &meta->amdgpu_insn[meta->amdgpu_insns],
+				   r64[2], /* elem */
+				   sr64[0].lo,
+				   r64[0].lo, /* elem_size */
+				   r64[2].lo, /* elem_id */
+				   r64[1]); /* elem_gaddr */
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+
+		/* load elem.next for DELETED check (parallel with key loads) */
+		knod_emit(priv, meta, global_load_dword, r64[0].hi,
+			  r64[2].lo, 0);
+		while (len >= 16) {
+			knod_emit(priv, meta, global_load_dwordx4,
+				  r32[key_in_map],
+				  r64[2].lo, /* elem ptr */ off);
+			off += 16;
+			len -= 16;
+			key_in_map += 4;
+		}
+
+		if (len >= 8) {
+			knod_emit(priv, meta, global_load_dwordx2,
+				  r32[key_in_map],
+				  /* elem_id */ r64[2].lo, /* elem ptr */ off);
+			off += 8;
+			len -= 8;
+			key_in_map += 2;
+		}
+
+		if (len >= 4) {
+			knod_emit(priv, meta, global_load_dword,
+				  r32[key_in_map],
+				  /* elem_id */ r64[2].lo, /* elem ptr */ off);
+			off += 4;
+			len -= 4;
+			key_in_map += 1;
+		}
+
+		/* map key padding was inited to zero, no AND is required */
+		if (len) {
+			knod_emit(priv, meta, global_load_dword,
+				  r32[key_in_map],
+				  /* elem_id */ r64[2].lo, /* elem ptr */ off);
+		}
+
+		knod_wait_vmcnt(priv, meta);
+
+		/* structurized CFG: accumulate key match into TMP_SREG4
+		 * instead of early-exit branching per key part
+		 */
+		key_in_map = KEY_IN_MAP_32;
+		key_in_pkt = KEY_IN_PKT_32;
+		len = knod_map_obj_k->key_size;
+		first_cmp = true;
+
+		while (len >= 8) {
+			knod_emit(priv, meta, v_cmp_eq_u64, r32[key_in_map],
+				  r32[key_in_pkt]);
+
+			if (first_cmp) {
+				knod_emit(priv, meta, s_and_b64,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  AMDGCN_SREG_EXEC_LO,
+					  AMDGCN_SREG_VCC_LO);
+				first_cmp = false;
+			} else {
+				knod_emit(priv, meta, s_and_b64,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  AMDGCN_SREG_VCC_LO);
+			}
+
+			key_in_map += 2;
+			key_in_pkt += 2;
+			len -= 8;
+		}
+
+		if (len >= 4) {
+			knod_emit(priv, meta, v_cmp_eq_u32, r32[key_in_map],
+				  r32[key_in_pkt]);
+
+			if (first_cmp) {
+				knod_emit(priv, meta, s_and_b64,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  AMDGCN_SREG_EXEC_LO,
+					  AMDGCN_SREG_VCC_LO);
+				first_cmp = false;
+			} else {
+				knod_emit(priv, meta, s_and_b64,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  AMDGCN_SREG_VCC_LO);
+			}
+
+			key_in_map += 1;
+			key_in_pkt += 1;
+			len -= 4;
+		}
+
+		if (len) {
+			knod_emit(priv, meta, v_cmp_eq_u32, r32[key_in_map],
+				  r32[key_in_pkt]);
+
+			if (first_cmp) {
+				knod_emit(priv, meta, s_and_b64,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  AMDGCN_SREG_EXEC_LO,
+					  AMDGCN_SREG_VCC_LO);
+				first_cmp = false;
+			} else {
+				knod_emit(priv, meta, s_and_b64,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  KNOD_AMDGPU_TMP_SREG4_LO,
+					  AMDGCN_SREG_VCC_LO);
+			}
+		}
+
+		/* DELETED check: remove deleted lanes from match result.
+		 * r64[0].hi = elem.next (loaded in parallel with key).
+		 * Deleted elems have bit 31 set - exclude them from SREG4.
+		 */
+		knod_iset32(&p32, KNOD_BPF_HASH_NEXT_DELETED);
+		knod_emit(priv, meta, v_and_b32_e32, r64[0].hi, p32,
+			  r64[0].hi);
+		knod_iset32(&p32, 0);
+		knod_emit(priv, meta, v_cmp_eq_u32, p32, r64[0].hi);
+		knod_emit(priv, meta, s_and_b64, KNOD_AMDGPU_TMP_SREG4_LO,
+			  KNOD_AMDGPU_TMP_SREG4_LO, AMDGCN_SREG_VCC_LO);
+
+		/* TMP_SREG4 = lanes where key matched AND not deleted.
+		 * Save current exec, narrow to matched lanes for value
+		 * computation.
+		 */
+		knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG5_LO,
+			  AMDGCN_SREG_EXEC_LO);
+		knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+			  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG4_LO);
+
+		/* bpf_reg64[0] = value address (only for matched lanes) */
+		knod_iset64(&p64[1],
+				offsetof(struct knod_bpf_hash_elem_obj, kv) +
+					 knod_map_obj_k->key_size);
+		knod_add64(priv, meta, bpf_reg64[0], p64[1], r64[2]);
+
+		/* set exec to unmatched lanes for next loop iteration */
+		knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+			  KNOD_AMDGPU_TMP_SREG5_LO, KNOD_AMDGPU_TMP_SREG4_LO);
+			/* loop back if any unmatched lanes remain */
+		emit_s_cbranch_execnz(priv->isa_version,
+				      &meta->amdgpu_insn[meta->amdgpu_insns],
+				      0); /* update required */
+		knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+				   &labels[LABEL_NEXT], meta->amdgpu_insns);
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+		fixup_idx++;
+		/* structurized CFG: restore all original lanes */
+		knod_bpf_set_label(meta, &labels[LABEL_OUT],
+				   meta->amdgpu_insns);
+		knod_emit(priv, meta, s_or_b64, AMDGCN_SREG_EXEC_LO,
+			  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG3_LO);
+		for (idx = 0; idx < fixup_idx; idx++)
+			knod_bpf_fixup_branch(priv, &fixups[idx]);
+
+	} else {
+		WARN_ON_ONCE(1);
+	}
+}
+
+static void knod_bpf_map_update_array(struct knod_bpf_priv *priv,
+				     struct knod_insn_meta *meta,
+				     int map_id)
+{
+	struct knod_bpf_map_obj *knod_map_obj_k, *knod_map_obj_g;
+	struct amdgcn_branch_fixup fixups[4] = {0,};
+	u32 key_stack_off = meta->kreg.stack_off;
+	u32 val_stack_off = meta->vreg.stack_off;
+	struct amdgcn_label labels[10] = {0,};
+	int idx, val_off, val_len;
+	unsigned long bucket_gaddr;
+	int fixup_idx = 0;
+
+	knod_map_obj_k =
+		(struct knod_bpf_map_obj *)knod_bpf_map_kaddr(priv, map_id);
+	knod_map_obj_g =
+		(struct knod_bpf_map_obj *)knod_bpf_map_gaddr(priv, map_id);
+	bucket_gaddr = (unsigned long)knod_map_obj_g +
+		       offsetof(struct knod_bpf_map_obj, bucket);
+
+	if (!knod_map_obj_g || !knod_map_obj_k)
+		WARN_ON_ONCE(1);
+
+	/* load key from stack -> r64[2] */
+	knod_bpf_load_size(priv, meta,
+			       &r64[2],
+			       &stack[0],
+			       sizeof(unsigned int),
+			       512 + key_stack_off);
+
+	/* r64[1] = bucket_gaddr */
+	knod_iset64(&p64[0], bucket_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+
+	/* clear r64[2].hi (key is 32-bit) */
+	knod_iset64(&p64[1], 0);
+	knod_mov32(priv, meta, r64[2].hi, p64[1].lo);
+
+	/* bpf_reg64[0] = 0 (return value) */
+	knod_mov64(priv, meta, bpf_reg64[0], p64[1]);
+
+	/* bounds check: if (key >= max_entries) -> skip */
+	knod_iset64(&p64[1], knod_map_obj_k->max_entries);
+	knod_mov64(priv, meta, r64[3], p64[1]);
+	knod_emit(priv, meta, v_cmp_ge_u64, r64[2], r64[3]);
+
+	/* structurized CFG: save OOB lanes, narrow exec */
+	knod_emit(priv, meta, s_and_b64, KNOD_AMDGPU_TMP_SREG3_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+	emit_s_cbranch_execz(priv->isa_version,
+			     &meta->amdgpu_insn[meta->amdgpu_insns],
+			     0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_OUT], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* dest = bucket_gaddr + key * value_size -> r64[0] */
+	knod_iset64(&p64[1], knod_map_obj_k->value_size);
+	knod_emit(priv, meta, v_mad_u64_u32, r64[0], sr64[0].lo,
+		  p64[1].lo, r64[2].lo, r64[1]);
+
+	/* load value from stack and store to dest */
+	val_off = 0;
+	val_len = knod_map_obj_k->value_size;
+
+	while (val_len >= 16) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[3],
+				       &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off);
+		knod_bpf_load_size(priv, meta,
+				       &r64[4],
+				       &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off + 8);
+		knod_emit(priv, meta, global_store_dwordx4, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 16;
+		val_len -= 16;
+	}
+
+	if (val_len >= 8) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[3],
+				       &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_dwordx2, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 8;
+		val_len -= 8;
+	}
+
+	if (val_len >= 4) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[3],
+				       &stack[0],
+				       sizeof(unsigned int),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_dword, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 4;
+		val_len -= 4;
+	}
+
+	if (val_len >= 2) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[3],
+				       &stack[0],
+				       sizeof(unsigned short),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_short, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 2;
+		val_len -= 2;
+	}
+
+	if (val_len >= 1) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[3],
+				       &stack[0],
+				       sizeof(unsigned char),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_byte, r64[3].lo,
+			  r64[0].lo, val_off);
+	}
+
+	/* structurized CFG: restore OOB lanes */
+	knod_bpf_set_label(meta, &labels[LABEL_OUT], meta->amdgpu_insns);
+	knod_emit(priv, meta, s_or_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG3_LO);
+
+	for (idx = 0; idx < fixup_idx; idx++)
+		knod_bpf_fixup_branch(priv, &fixups[idx]);
+}
+
+static void knod_bpf_map_update_hash(struct knod_bpf_priv *priv,
+				     struct knod_insn_meta *meta,
+				     int map_id)
+{
+#define LABEL_BUCKET_LOOP	0
+#define LABEL_LOCK_RETRY	1
+#define LABEL_INSERT_LANE	2
+#define LABEL_CHAIN_NEXT	3
+#define LABEL_ALLOC_INSERT	4
+#define LABEL_LANE_DONE		5
+#define LABEL_UNLOCK		6
+	struct knod_bpf_map_obj *knod_map_obj_k, *knod_map_obj_g;
+	struct amdgcn_param32 s_bucket_lo, v_tmp, v_zero, v_one;
+	int off, len, _len, idx, key_in_pkt, key_in_map;
+	struct amdgcn_param32 s_exec_lo, s_exec_hi, s_elem_id;
+	struct amdgcn_branch_fixup fixups[12] = {0,};
+	u32 key_stack_off = meta->kreg.stack_off;
+	u32 val_stack_off = meta->vreg.stack_off;
+	unsigned long queue_gaddr, elems_gaddr;
+	unsigned long bucket_gaddr, cur_gaddr;
+	struct amdgcn_label labels[12] = {0,};
+	struct amdgcn_param32 v_minus_one;
+	struct amdgcn_param64 sr64_carry;
+	struct amdgcn_param32 p32;
+	unsigned long lock_offset;
+	unsigned int elem_size;
+	int val_off, val_len;
+	int fixup_idx = 0;
+	bool first_cmp;
+	int koff, voff;
+
+	knod_map_obj_k =
+		(struct knod_bpf_map_obj *)knod_bpf_map_kaddr(priv, map_id);
+	knod_map_obj_g =
+		(struct knod_bpf_map_obj *)knod_bpf_map_gaddr(priv, map_id);
+	bucket_gaddr = (unsigned long)knod_map_obj_g +
+		       offsetof(struct knod_bpf_map_obj, bucket);
+	cur_gaddr = (unsigned long)knod_map_obj_g +
+		    offsetof(struct knod_bpf_map_obj, meta.hmeta.cur);
+	queue_gaddr = (unsigned long)knod_map_obj_k->meta.hmeta.q;
+	elems_gaddr = (unsigned long)knod_map_obj_k->meta.hmeta.elems;
+	elem_size = knod_map_obj_k->meta.hmeta.elem_size;
+
+	if (!knod_map_obj_g || !knod_map_obj_k)
+		WARN_ON_ONCE(1);
+
+	/* ======== Phase 1: Setup ======== */
+
+	/* Load key from stack -> r64[3..9] (KEY_IN_PKT) */
+	key_in_pkt = KEY_IN_PKT_64;
+	len = knod_map_obj_k->key_size;
+	off = key_stack_off;
+	while (len) {
+		if (len >= sizeof(unsigned long))
+			_len = sizeof(unsigned long);
+		else
+			_len = len;
+		knod_bpf_load_size(priv, meta,
+				       &r64[key_in_pkt],
+				       &stack[0],
+				       _len,
+				       512 + off);
+		key_in_pkt++;
+		len -= _len;
+		off += _len;
+	}
+
+	/* jhash -> r64[2].lo = hash */
+	knod_jhash(priv, meta,
+		       2,
+		       knod_map_obj_k->key_size,
+		       knod_map_obj_k->meta.hmeta.hashrnd);
+	knod_iset64(&p64[0], 0);
+	knod_mov32(priv, meta, r64[2].hi, p64[0].lo);
+
+	/* hash = hash & (n_buckets - 1) */
+	knod_iset32(&p64[0].lo,
+				 knod_map_obj_k->meta.hmeta.n_buckets - 1);
+	knod_and32(priv, meta, r64[2].lo, p64[0].lo, r64[2].lo);
+
+	/* r64[1] = bucket_gaddr */
+	knod_iset64(&p64[0], bucket_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+
+	/* bucket_addr = bucket_gaddr + hash * sizeof(int) -> r64[2] */
+	knod_iset64(&p64[1], sizeof(int));
+	knod_emit(priv, meta, v_mad_u64_u32, r64[2], sr64[0].lo,
+		  p64[1].lo, r64[2].lo, r64[1]);
+
+	/* Save bucket_addr to r64[15] for CAS insert */
+	knod_mov64(priv, meta, r64[15], r64[2]);
+
+	/* SREG3 = initial exec */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG3_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* ======== Phase 2: Sequential per-lane processing ======== */
+
+	/* SREG5 = exec (all lanes to process, for BUCKET_LOOP) */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG5_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* ---- BUCKET_LOOP: process one unique bucket per iteration ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_BUCKET_LOOP],
+			   meta->amdgpu_insns);
+
+	lock_offset =
+		(unsigned long)knod_map_obj_k->meta.hmeta.n_buckets *
+		sizeof(unsigned int);
+
+	knod_sset32(&s_bucket_lo,
+				 KNOD_AMDGPU_TMP_SREG1_HI);
+	knod_vset32(&v_tmp, KNOD_AMDGPU_TMP_VREG0_HI);
+	knod_iset32(&v_zero, 0);
+	knod_iset32(&v_one, 1);
+	knod_sset32(&s_exec_lo, AMDGCN_SREG_EXEC_LO);
+	knod_sset32(&s_exec_hi,
+				 AMDGCN_SREG_EXEC_LO + 1);
+	knod_sset32(&s_elem_id,
+				 KNOD_AMDGPU_TMP_SREG1_LO);
+	knod_sset64(&sr64_carry,
+				 KNOD_AMDGPU_TMP_SREG1_LO);
+
+	/* Pick first active lane's bucket addr */
+	knod_emit(priv, meta, v_readfirstlane_b32, KNOD_AMDGPU_TMP_SREG1_HI,
+		  r64[15].lo.v);
+
+	/* vcc = lanes with same bucket */
+	knod_emit(priv, meta, v_cmp_eq_u32, s_bucket_lo, r64[15].lo);
+
+	/* SREG5 = remaining lanes; exec = same-bucket lanes */
+	knod_emit(priv, meta, s_and_saveexec_b64, KNOD_AMDGPU_TMP_SREG5_LO,
+		  AMDGCN_SREG_VCC_LO);
+
+	/* SREG0 = same-bucket lanes */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG0_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* ---- Lock acquire ---- */
+	/* r64[10] = r64[15] + lock_offset */
+	knod_iset64(&p64[0], lock_offset);
+	knod_add64(priv, meta, r64[10], p64[0], r64[15]);
+
+	/* r64[11].lo = 1 (swap data) */
+	knod_emit(priv, meta, v_mov_b32_e32, r64[11].lo, v_one);
+
+	/* First-lane isolation via mbcnt */
+	knod_emit(priv, meta, v_mbcnt_lo_u32_b32, v_tmp, s_exec_lo,
+		  v_zero);
+	knod_emit(priv, meta, v_mbcnt_hi_u32_b32, v_tmp, s_exec_hi, v_tmp);
+	knod_emit(priv, meta, v_cmp_eq_u32, v_zero, v_tmp);
+	knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+
+	/* LOCK_RETRY: spin until lock acquired */
+	knod_bpf_set_label(meta, &labels[LABEL_LOCK_RETRY], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, global_atomic_swap, r64[11].hi, r64[10].lo,
+		  r64[11].lo, 0, 1);
+	knod_wait_vmcnt(priv, meta);
+
+	meta->amdgpu_insn[meta->amdgpu_insns].size =
+		emit_gfx10_v_cmp_ne_u32(
+			&meta->amdgpu_insn[meta->amdgpu_insns].gfx10,
+			v_zero, r64[11].hi);
+	meta->amdgpu_insn[meta->amdgpu_insns].type = AMDGCN_INSN_TYPE_VOPC;
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+
+	emit_s_cbranch_vccnz(priv->isa_version,
+			     &meta->amdgpu_insn[meta->amdgpu_insns],
+			     0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_LOCK_RETRY], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* Lock acquired - restore same-bucket lanes */
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG0_LO);
+
+	/* ---- INSERT_LANE: process one lane at a time ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_INSERT_LANE],
+			   meta->amdgpu_insns);
+
+	/* SREG4 = exec (remaining same-bucket lanes) */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG4_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* Pick first active lane via mbcnt */
+	knod_emit(priv, meta, v_mbcnt_lo_u32_b32, v_tmp, s_exec_lo,
+		  v_zero);
+	knod_emit(priv, meta, v_mbcnt_hi_u32_b32, v_tmp, s_exec_hi, v_tmp);
+	knod_emit(priv, meta, v_cmp_eq_u32, v_zero, v_tmp);
+	knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+
+	/* SREG2 = exec (single-lane mask) */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG2_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* r64[2] = r64[15] (bucket_addr for chain walk start) */
+	knod_mov64(priv, meta, r64[2], r64[15]);
+
+	/* ---- CHAIN_NEXT: walk chain ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_CHAIN_NEXT], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, global_load_dword, r64[0].lo, r64[2].lo, 0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* Mask out DELETED bit */
+	knod_iset32(&p32, KNOD_BPF_HASH_NEXT_MASK);
+	knod_emit(priv, meta, v_and_b32_e32, r64[0].lo, p32, r64[0].lo);
+
+	/* End-of-chain check (VOPC literal workaround) */
+	knod_emit(priv, meta, v_mov_b32_e32, r64[0].hi, p32);
+	knod_emit(priv, meta, v_cmp_eq_u32, r64[0].hi, r64[0].lo);
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+	emit_s_cbranch_execz(priv->isa_version,
+			     &meta->amdgpu_insn[meta->amdgpu_insns],
+			     0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_ALLOC_INSERT], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* elem_addr = elems + elem_id * elem_size -> r64[2] */
+	knod_iset64(&p64[0], elems_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+	knod_iset64(&p64[0], elem_size);
+	knod_mov32(priv, meta, r64[10].lo, p64[0].lo);
+	knod_emit(priv, meta, v_mad_u64_u32, r64[2], sr64_carry.lo,
+		  r64[10].lo, r64[0].lo, r64[1]);
+
+	/* Load elem.next for DELETED check */
+	knod_emit(priv, meta, global_load_dword, r64[0].hi, r64[2].lo, 0);
+
+	/* Load key from map element -> KEY_IN_MAP */
+	key_in_map = KEY_IN_MAP_32;
+	len = knod_map_obj_k->key_size;
+	off = offsetof(struct knod_bpf_hash_elem_obj, kv);
+
+	while (len >= 16) {
+		knod_emit(priv, meta, global_load_dwordx4, r32[key_in_map],
+			  r64[2].lo, off);
+		off += 16;
+		len -= 16;
+		key_in_map += 4;
+	}
+
+	if (len >= 8) {
+		knod_emit(priv, meta, global_load_dwordx2, r32[key_in_map],
+			  r64[2].lo, off);
+		off += 8;
+		len -= 8;
+		key_in_map += 2;
+	}
+
+	if (len >= 4) {
+		knod_emit(priv, meta, global_load_dword, r32[key_in_map],
+			  r64[2].lo, off);
+		off += 4;
+		len -= 4;
+		key_in_map += 1;
+	}
+
+	if (len) {
+		knod_emit(priv, meta, global_load_dword, r32[key_in_map],
+			  r64[2].lo, off);
+	}
+
+	knod_wait_vmcnt(priv, meta);
+
+	/* Key comparison -> SREG1 */
+	key_in_map = KEY_IN_MAP_32;
+	key_in_pkt = KEY_IN_PKT_32;
+	len = knod_map_obj_k->key_size;
+	first_cmp = true;
+
+	while (len >= 8) {
+		knod_emit(priv, meta, v_cmp_eq_u64, r32[key_in_map],
+			  r32[key_in_pkt]);
+
+		if (first_cmp) {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_EXEC_LO,
+				       AMDGCN_SREG_VCC_LO);
+			first_cmp = false;
+		} else {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_VCC_LO);
+		}
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+
+		key_in_map += 2;
+		key_in_pkt += 2;
+		len -= 8;
+	}
+
+	if (len >= 4) {
+		knod_emit(priv, meta, v_cmp_eq_u32, r32[key_in_map],
+			  r32[key_in_pkt]);
+
+		if (first_cmp) {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_EXEC_LO,
+				       AMDGCN_SREG_VCC_LO);
+			first_cmp = false;
+		} else {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_VCC_LO);
+		}
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+
+		key_in_map += 1;
+		key_in_pkt += 1;
+		len -= 4;
+	}
+
+	if (len) {
+		knod_emit(priv, meta, v_cmp_eq_u32, r32[key_in_map],
+			  r32[key_in_pkt]);
+
+		if (first_cmp) {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_EXEC_LO,
+				       AMDGCN_SREG_VCC_LO);
+			first_cmp = false;
+		} else {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_VCC_LO);
+		}
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+	}
+
+	/* DELETED check: SREG1 &= not_deleted */
+	knod_iset32(&p32,
+				 KNOD_BPF_HASH_NEXT_DELETED);
+	knod_emit(priv, meta, v_and_b32_e32, r64[0].hi, p32, r64[0].hi);
+	knod_iset32(&p32, 0);
+	knod_emit(priv, meta, v_cmp_eq_u32, p32, r64[0].hi);
+	knod_emit(priv, meta, s_and_b64, KNOD_AMDGPU_TMP_SREG1_LO,
+		  KNOD_AMDGPU_TMP_SREG1_LO, AMDGCN_SREG_VCC_LO);
+
+	/* Narrow exec to matched lane */
+	knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG1_LO);
+
+	/* Value overwrite for matched lane (exec-masked, skipped if no
+	 * match)
+	 */
+	knod_iset64(&p64[1],
+				 offsetof(struct knod_bpf_hash_elem_obj,
+					  kv) +
+				 knod_map_obj_k->key_size);
+	knod_add64(priv, meta, r64[0], p64[1], r64[2]);
+
+	val_off = 0;
+	val_len = knod_map_obj_k->value_size;
+
+	while (val_len >= 16) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off);
+		knod_bpf_load_size(priv, meta,
+				       &r64[11], &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off + 8);
+		knod_emit(priv, meta, global_store_dwordx4, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 16;
+		val_len -= 16;
+	}
+
+	if (val_len >= 8) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_dwordx2, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 8;
+		val_len -= 8;
+	}
+
+	if (val_len >= 4) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned int),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_dword, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 4;
+		val_len -= 4;
+	}
+
+	if (val_len >= 2) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned short),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_short, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 2;
+		val_len -= 2;
+	}
+
+	if (val_len >= 1) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned char),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_byte, r64[10].lo,
+			  r64[0].lo, val_off);
+	}
+
+	/* If matched, done with this lane */
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_LANE_DONE], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* No match: restore lane, continue chain walk */
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG2_LO);
+
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_CHAIN_NEXT], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* ---- ALLOC_INSERT: key not found, insert new elem ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_ALLOC_INSERT],
+			   meta->amdgpu_insns);
+
+	/* Restore single-lane exec */
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG2_LO);
+
+	/* Alloc from free pool: atomic_add(cur, -1) */
+	knod_iset32(&v_minus_one, -1);
+	knod_emit(priv, meta, v_mov_b32_e32, r64[11].lo, v_minus_one);
+
+	knod_iset64(&p64[0], cur_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+
+	knod_emit(priv, meta, global_atomic_add, r64[11].lo, r64[1].lo,
+		  r64[11].lo, 0, 1);
+	knod_wait_vmcnt(priv, meta);
+
+	/* my_cur = old_cur - 1 -> r64[0].lo */
+	knod_emit(priv, meta, v_mov_b32_e32, r64[0].lo, v_one);
+	knod_emit(priv, meta, v_sub_u32, r64[0].lo, r64[11].lo, r64[0].lo);
+
+	/* OOM check: if (my_cur < 0) -> skip insert */
+	knod_emit(priv, meta, v_cmp_gt_i32, v_zero, r64[0].lo);
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+	emit_s_cbranch_execz(priv->isa_version,
+			     &meta->amdgpu_insn[meta->amdgpu_insns],
+			     0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_LANE_DONE], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* queue_addr = queue_gaddr + my_cur * 4 -> r64[1] */
+	knod_iset64(&p64[0], queue_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+	knod_iset64(&p64[1], sizeof(unsigned int));
+	knod_emit(priv, meta, v_mad_u64_u32, r64[1], sr64_carry.lo,
+		  p64[1].lo, r64[0].lo, r64[1]);
+
+	/* elem_id = queue[my_cur] -> r64[0].lo */
+	knod_emit(priv, meta, global_load_dword, r64[0].lo, r64[1].lo, 0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* new_elem_addr = elems + elem_id * elem_size -> r64[2] */
+	knod_iset64(&p64[0], elems_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+	knod_iset64(&p64[0], elem_size);
+	knod_mov32(priv, meta, r64[10].lo, p64[0].lo);
+	knod_emit(priv, meta, v_mad_u64_u32, r64[2], sr64_carry.lo,
+		  r64[10].lo, r64[0].lo, r64[1]);
+
+	/* Save elem_id to SGPR (v_mad carry already done) */
+	knod_emit(priv, meta, v_readfirstlane_b32, KNOD_AMDGPU_TMP_SREG1_LO,
+		  r64[0].lo.v);
+
+	/* Load current bucket head -> r64[1].lo */
+	knod_emit(priv, meta, global_load_dword, r64[1].lo, r64[15].lo, 0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* new_elem.next = old_head */
+	knod_emit(priv, meta, global_store_dword, r64[1].lo, r64[2].lo, 0);
+
+	/* Write key to new element */
+	koff = offsetof(struct knod_bpf_hash_elem_obj, kv);
+
+	key_in_pkt = KEY_IN_PKT_32;
+	len = knod_map_obj_k->key_size;
+
+	while (len >= 8) {
+		knod_emit(priv, meta, global_store_dwordx2, r32[key_in_pkt],
+			  r64[2].lo, koff);
+		koff += 8;
+		len -= 8;
+		key_in_pkt += 2;
+	}
+
+	if (len >= 4) {
+		knod_emit(priv, meta, global_store_dword, r32[key_in_pkt],
+			  r64[2].lo, koff);
+		koff += 4;
+		len -= 4;
+		key_in_pkt += 1;
+	}
+
+	if (len >= 2) {
+		knod_emit(priv, meta, global_store_short, r32[key_in_pkt],
+			  r64[2].lo, koff);
+		koff += 2;
+		len -= 2;
+	}
+
+	if (len >= 1) {
+		knod_emit(priv, meta, global_store_byte, r32[key_in_pkt],
+			  r64[2].lo, koff);
+	}
+
+	/* Write value to new element */
+	voff = offsetof(struct knod_bpf_hash_elem_obj, kv) +
+	       knod_map_obj_k->key_size;
+
+	val_off = 0;
+	val_len = knod_map_obj_k->value_size;
+
+	knod_iset64(&p64[1], voff);
+	knod_add64(priv, meta, r64[0], p64[1], r64[2]);
+
+	while (val_len >= 16) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off);
+		knod_bpf_load_size(priv, meta,
+				       &r64[11], &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off + 8);
+		knod_emit(priv, meta, global_store_dwordx4, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 16;
+		val_len -= 16;
+	}
+
+	if (val_len >= 8) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned long),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_dwordx2, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 8;
+		val_len -= 8;
+	}
+
+	if (val_len >= 4) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned int),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_dword, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 4;
+		val_len -= 4;
+	}
+
+	if (val_len >= 2) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned short),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_short, r64[10].lo,
+			  r64[0].lo, val_off);
+		val_off += 2;
+		val_len -= 2;
+	}
+
+	if (val_len >= 1) {
+		knod_bpf_load_size(priv, meta,
+				       &r64[10], &stack[0],
+				       sizeof(unsigned char),
+				       512 + val_stack_off + val_off);
+		knod_emit(priv, meta, global_store_byte, r64[10].lo,
+			  r64[0].lo, val_off);
+	}
+
+	knod_wait_vmcnt(priv, meta);
+
+	/* Update bucket[hash] = new elem_id */
+	knod_emit(priv, meta, v_mov_b32_e32, r64[1].lo, s_elem_id);
+	knod_emit(priv, meta, global_store_dword, r64[1].lo, r64[15].lo,
+		  0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* ---- LANE_DONE: remove this lane, next lane ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_LANE_DONE], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG4_LO, KNOD_AMDGPU_TMP_SREG2_LO);
+
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_INSERT_LANE], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* ---- UNLOCK: release lock + next bucket ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_UNLOCK], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG0_LO);
+
+	/* Recompute lock_addr (r64[10] clobbered) */
+	knod_iset64(&p64[0], lock_offset);
+	knod_add64(priv, meta, r64[10], p64[0], r64[15]);
+
+	knod_emit(priv, meta, v_mov_b32_e32, r64[1].lo, v_zero);
+	knod_emit(priv, meta, global_store_dword, r64[1].lo, r64[10].lo,
+		  0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* Next bucket */
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG5_LO, KNOD_AMDGPU_TMP_SREG0_LO);
+
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_BUCKET_LOOP], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* ======== Phase 5: Restore ======== */
+
+	knod_bpf_set_label(meta, &labels[LABEL_OUT], meta->amdgpu_insns);
+	/* exec = SREG3 (restore all original lanes) */
+	knod_emit(priv, meta, s_or_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG3_LO);
+
+	/* bpf_reg64[0] = 0 (return value for all lanes) */
+	knod_iset64(&p64[0], 0);
+	knod_mov64(priv, meta, bpf_reg64[0], p64[0]);
+
+	for (idx = 0; idx < fixup_idx; idx++)
+		knod_bpf_fixup_branch(priv, &fixups[idx]);
+
+	return;
+#undef LABEL_BUCKET_LOOP
+#undef LABEL_LOCK_RETRY
+#undef LABEL_INSERT_LANE
+#undef LABEL_CHAIN_NEXT
+#undef LABEL_ALLOC_INSERT
+#undef LABEL_LANE_DONE
+#undef LABEL_UNLOCK
+}
+
+static void knod_bpf_map_delete_hash(struct knod_bpf_priv *priv,
+				     struct knod_insn_meta *meta,
+				     int map_id)
+{
+#define LABEL_BUCKET_LOOP	0
+#define LABEL_LOCK_RETRY	1
+#define LABEL_DELETE_LANE	2
+#define LABEL_CHAIN_NEXT	3
+#define LABEL_LANE_DONE		4
+#define LABEL_UNLOCK		5
+	struct knod_bpf_map_obj *knod_map_obj_k, *knod_map_obj_g;
+	struct amdgcn_param32 s_bucket_lo, v_tmp, v_zero, v_one;
+	int off, len, _len, idx, key_in_pkt, key_in_map;
+	struct amdgcn_branch_fixup fixups[12] = {0,};
+	unsigned long bucket_gaddr, gc_count_gaddr;
+	struct amdgcn_param32 s_exec_lo, s_exec_hi;
+	unsigned long gc_list_gaddr, elems_gaddr;
+	u32 key_stack_off = meta->kreg.stack_off;
+	struct amdgcn_label labels[12] = {0,};
+	struct amdgcn_param64 sr64_carry;
+	struct amdgcn_param32 v_del;
+	struct amdgcn_param32 p32;
+	unsigned long lock_offset;
+	unsigned int elem_size;
+	int fixup_idx = 0;
+	bool first_cmp;
+
+	knod_map_obj_k =
+		(struct knod_bpf_map_obj *)knod_bpf_map_kaddr(priv, map_id);
+	knod_map_obj_g =
+		(struct knod_bpf_map_obj *)knod_bpf_map_gaddr(priv, map_id);
+	bucket_gaddr = (unsigned long)knod_map_obj_g +
+		       offsetof(struct knod_bpf_map_obj, bucket);
+	gc_count_gaddr = (unsigned long)knod_map_obj_g +
+			 offsetof(struct knod_bpf_map_obj, meta.hmeta.gc_count);
+	gc_list_gaddr = (unsigned long)knod_map_obj_k->meta.hmeta.gc_list;
+	elems_gaddr = (unsigned long)knod_map_obj_k->meta.hmeta.elems;
+	elem_size = knod_map_obj_k->meta.hmeta.elem_size;
+
+	if (!knod_map_obj_g || !knod_map_obj_k)
+		WARN_ON_ONCE(1);
+
+	/* ======== Phase 1: Setup ======== */
+
+	/* Load key from stack -> r64[3..9] (KEY_IN_PKT) */
+	key_in_pkt = KEY_IN_PKT_64;
+	len = knod_map_obj_k->key_size;
+	off = key_stack_off;
+	while (len) {
+		if (len >= sizeof(unsigned long))
+			_len = sizeof(unsigned long);
+		else
+			_len = len;
+		knod_bpf_load_size(priv, meta,
+				       &r64[key_in_pkt],
+				       &stack[0],
+				       _len,
+				       512 + off);
+		key_in_pkt++;
+		len -= _len;
+		off += _len;
+	}
+
+	/* jhash -> r64[2].lo = hash */
+	knod_jhash(priv, meta,
+		       2,
+		       knod_map_obj_k->key_size,
+		       knod_map_obj_k->meta.hmeta.hashrnd);
+	knod_iset64(&p64[0], 0);
+	knod_mov32(priv, meta, r64[2].hi, p64[0].lo);
+
+	/* hash = hash & (n_buckets - 1) */
+	knod_iset32(&p64[0].lo,
+				 knod_map_obj_k->meta.hmeta.n_buckets - 1);
+	knod_and32(priv, meta, r64[2].lo, p64[0].lo, r64[2].lo);
+
+	/* r64[1] = bucket_gaddr */
+	knod_iset64(&p64[0], bucket_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+
+	/* bucket_addr = bucket_gaddr + hash * sizeof(int) -> r64[2] */
+	knod_iset64(&p64[1], sizeof(int));
+	knod_emit(priv, meta, v_mad_u64_u32, r64[2], sr64[0].lo,
+		  p64[1].lo, r64[2].lo, r64[1]);
+
+	/* Save bucket_addr to r64[15] */
+	knod_mov64(priv, meta, r64[15], r64[2]);
+
+	/* SREG3 = initial exec */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG3_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* ======== Phase 2: Sequential per-lane processing ======== */
+
+	/* SREG5 = exec (all lanes to process, for BUCKET_LOOP) */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG5_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* ---- BUCKET_LOOP: process one unique bucket per iteration ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_BUCKET_LOOP],
+			   meta->amdgpu_insns);
+
+	lock_offset =
+		(unsigned long)knod_map_obj_k->meta.hmeta.n_buckets *
+		sizeof(unsigned int);
+
+	knod_sset32(&s_bucket_lo,
+				 KNOD_AMDGPU_TMP_SREG1_HI);
+	knod_vset32(&v_tmp, KNOD_AMDGPU_TMP_VREG0_HI);
+	knod_iset32(&v_zero, 0);
+	knod_iset32(&v_one, 1);
+	knod_sset32(&s_exec_lo, AMDGCN_SREG_EXEC_LO);
+	knod_sset32(&s_exec_hi,
+				 AMDGCN_SREG_EXEC_LO + 1);
+	knod_sset64(&sr64_carry,
+				 KNOD_AMDGPU_TMP_SREG1_LO);
+
+	/* Pick first active lane's bucket addr */
+	knod_emit(priv, meta, v_readfirstlane_b32, KNOD_AMDGPU_TMP_SREG1_HI,
+		  r64[15].lo.v);
+
+	/* vcc = lanes with same bucket */
+	knod_emit(priv, meta, v_cmp_eq_u32, s_bucket_lo, r64[15].lo);
+
+	/* SREG5 = remaining lanes; exec = same-bucket lanes */
+	knod_emit(priv, meta, s_and_saveexec_b64, KNOD_AMDGPU_TMP_SREG5_LO,
+		  AMDGCN_SREG_VCC_LO);
+
+	/* SREG0 = same-bucket lanes */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG0_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* ---- Lock acquire ---- */
+	/* r64[10] = r64[15] + lock_offset */
+	knod_iset64(&p64[0], lock_offset);
+	knod_add64(priv, meta, r64[10], p64[0], r64[15]);
+
+	/* r64[11].lo = 1 (swap data) */
+	knod_emit(priv, meta, v_mov_b32_e32, r64[11].lo, v_one);
+
+	/* First-lane isolation via mbcnt */
+	knod_emit(priv, meta, v_mbcnt_lo_u32_b32, v_tmp, s_exec_lo,
+		  v_zero);
+	knod_emit(priv, meta, v_mbcnt_hi_u32_b32, v_tmp, s_exec_hi, v_tmp);
+	knod_emit(priv, meta, v_cmp_eq_u32, v_zero, v_tmp);
+	knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+
+	/* LOCK_RETRY: spin until lock acquired */
+	knod_bpf_set_label(meta, &labels[LABEL_LOCK_RETRY], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, global_atomic_swap, r64[11].hi, r64[10].lo,
+		  r64[11].lo, 0, 1);
+	knod_wait_vmcnt(priv, meta);
+
+	meta->amdgpu_insn[meta->amdgpu_insns].size =
+		emit_gfx10_v_cmp_ne_u32(
+			&meta->amdgpu_insn[meta->amdgpu_insns].gfx10,
+			v_zero, r64[11].hi);
+	meta->amdgpu_insn[meta->amdgpu_insns].type = AMDGCN_INSN_TYPE_VOPC;
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+
+	emit_s_cbranch_vccnz(priv->isa_version,
+			     &meta->amdgpu_insn[meta->amdgpu_insns],
+			     0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_LOCK_RETRY], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* Lock acquired - restore same-bucket lanes */
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG0_LO);
+
+	/* ---- DELETE_LANE: process one lane at a time ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_DELETE_LANE],
+			   meta->amdgpu_insns);
+
+	/* SREG4 = exec (remaining same-bucket lanes) */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG4_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* Pick first active lane via mbcnt */
+	knod_emit(priv, meta, v_mbcnt_lo_u32_b32, v_tmp, s_exec_lo,
+		  v_zero);
+	knod_emit(priv, meta, v_mbcnt_hi_u32_b32, v_tmp, s_exec_hi, v_tmp);
+	knod_emit(priv, meta, v_cmp_eq_u32, v_zero, v_tmp);
+	knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+
+	/* SREG2 = exec (single-lane mask) */
+	knod_emit(priv, meta, s_mov_b64, KNOD_AMDGPU_TMP_SREG2_LO,
+		  AMDGCN_SREG_EXEC_LO);
+
+	/* r64[2] = r64[15] (bucket_addr for chain walk start) */
+	knod_mov64(priv, meta, r64[2], r64[15]);
+
+	/* ---- CHAIN_NEXT: walk chain ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_CHAIN_NEXT], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, global_load_dword, r64[0].lo, r64[2].lo, 0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* Mask out DELETED bit */
+	knod_iset32(&p32, KNOD_BPF_HASH_NEXT_MASK);
+	knod_emit(priv, meta, v_and_b32_e32, r64[0].lo, p32, r64[0].lo);
+
+	/* End-of-chain check (VOPC literal workaround) */
+	knod_emit(priv, meta, v_mov_b32_e32, r64[0].hi, p32);
+	knod_emit(priv, meta, v_cmp_eq_u32, r64[0].hi, r64[0].lo);
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+	emit_s_cbranch_execz(priv->isa_version,
+			     &meta->amdgpu_insn[meta->amdgpu_insns],
+			     0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_LANE_DONE], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* elem_addr = elems + elem_id * elem_size -> r64[2] */
+	knod_iset64(&p64[0], elems_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+	knod_iset64(&p64[0], elem_size);
+	knod_mov32(priv, meta, r64[10].lo, p64[0].lo);
+	knod_emit(priv, meta, v_mad_u64_u32, r64[2], sr64_carry.lo,
+		  r64[10].lo, r64[0].lo, r64[1]);
+
+	/* Load elem.next for DELETED check */
+	knod_emit(priv, meta, global_load_dword, r64[0].hi, r64[2].lo, 0);
+
+	/* Load key from map element -> KEY_IN_MAP */
+	key_in_map = KEY_IN_MAP_32;
+	len = knod_map_obj_k->key_size;
+	off = offsetof(struct knod_bpf_hash_elem_obj, kv);
+
+	while (len >= 16) {
+		knod_emit(priv, meta, global_load_dwordx4, r32[key_in_map],
+			  r64[2].lo, off);
+		off += 16;
+		len -= 16;
+		key_in_map += 4;
+	}
+
+	if (len >= 8) {
+		knod_emit(priv, meta, global_load_dwordx2, r32[key_in_map],
+			  r64[2].lo, off);
+		off += 8;
+		len -= 8;
+		key_in_map += 2;
+	}
+
+	if (len >= 4) {
+		knod_emit(priv, meta, global_load_dword, r32[key_in_map],
+			  r64[2].lo, off);
+		off += 4;
+		len -= 4;
+		key_in_map += 1;
+	}
+
+	if (len) {
+		knod_emit(priv, meta, global_load_dword, r32[key_in_map],
+			  r64[2].lo, off);
+	}
+
+	knod_wait_vmcnt(priv, meta);
+
+	/* Key comparison -> SREG1 */
+	key_in_map = KEY_IN_MAP_32;
+	key_in_pkt = KEY_IN_PKT_32;
+	len = knod_map_obj_k->key_size;
+	first_cmp = true;
+
+	while (len >= 8) {
+		knod_emit(priv, meta, v_cmp_eq_u64, r32[key_in_map],
+			  r32[key_in_pkt]);
+
+		if (first_cmp) {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_EXEC_LO,
+				       AMDGCN_SREG_VCC_LO);
+			first_cmp = false;
+		} else {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_VCC_LO);
+		}
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+
+		key_in_map += 2;
+		key_in_pkt += 2;
+		len -= 8;
+	}
+
+	if (len >= 4) {
+		knod_emit(priv, meta, v_cmp_eq_u32, r32[key_in_map],
+			  r32[key_in_pkt]);
+
+		if (first_cmp) {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_EXEC_LO,
+				       AMDGCN_SREG_VCC_LO);
+			first_cmp = false;
+		} else {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_VCC_LO);
+		}
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+
+		key_in_map += 1;
+		key_in_pkt += 1;
+		len -= 4;
+	}
+
+	if (len) {
+		knod_emit(priv, meta, v_cmp_eq_u32, r32[key_in_map],
+			  r32[key_in_pkt]);
+
+		if (first_cmp) {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_EXEC_LO,
+				       AMDGCN_SREG_VCC_LO);
+			first_cmp = false;
+		} else {
+			emit_s_and_b64(priv->isa_version,
+				       &meta->amdgpu_insn[meta->amdgpu_insns],
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       KNOD_AMDGPU_TMP_SREG1_LO,
+				       AMDGCN_SREG_VCC_LO);
+		}
+		debug_insn(priv->isa_version,
+			   &meta->amdgpu_insn[meta->amdgpu_insns]);
+		meta->amdgpu_insns++;
+	}
+
+	/* DELETED check: SREG1 &= not_deleted */
+	knod_iset32(&p32,
+				 KNOD_BPF_HASH_NEXT_DELETED);
+	knod_emit(priv, meta, v_and_b32_e32, r64[0].hi, p32, r64[0].hi);
+	knod_iset32(&p32, 0);
+	knod_emit(priv, meta, v_cmp_eq_u32, p32, r64[0].hi);
+	knod_emit(priv, meta, s_and_b64, KNOD_AMDGPU_TMP_SREG1_LO,
+		  KNOD_AMDGPU_TMP_SREG1_LO, AMDGCN_SREG_VCC_LO);
+
+	/* Narrow exec to matched lane */
+	knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG1_LO);
+
+	/* ---- Match path: set DELETED + append to gc_list ---- */
+
+	/* atomic_or(elem.next, DELETED_BIT) - mark deleted */
+	knod_lset32(&v_del,
+				 KNOD_BPF_HASH_NEXT_DELETED);
+	knod_emit(priv, meta, v_mov_b32_e32, r64[11].lo, v_del);
+	knod_emit(priv, meta, global_atomic_or, r64[11].hi, r64[2].lo,
+		  r64[11].lo, 0, 0);
+
+	/* atomic_add(gc_count, 1) -> old_count in r64[11].lo */
+	knod_emit(priv, meta, v_mov_b32_e32, r64[11].lo, v_one);
+
+	knod_iset64(&p64[0], gc_count_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+
+	knod_emit(priv, meta, global_atomic_add, r64[11].lo, r64[1].lo,
+		  r64[11].lo, 0, 1);
+	knod_wait_vmcnt(priv, meta);
+
+	/* Store elem_id to gc_list[old_count] */
+	knod_iset64(&p64[0], gc_list_gaddr);
+	knod_mov64(priv, meta, r64[10], p64[0]);
+
+	knod_iset64(&p64[0], sizeof(unsigned int));
+	knod_emit(priv, meta, v_mad_u64_u32, r64[1], sr64_carry.lo,
+		  p64[0].lo, r64[11].lo, r64[10]);
+
+	knod_emit(priv, meta, global_store_dword, r64[0].lo, r64[1].lo, 0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* If matched, done with this lane */
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_LANE_DONE], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* No match: restore lane, continue chain walk */
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG2_LO);
+
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_CHAIN_NEXT], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* ---- LANE_DONE: remove this lane, next lane ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_LANE_DONE], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG4_LO, KNOD_AMDGPU_TMP_SREG2_LO);
+
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_DELETE_LANE], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* ---- UNLOCK: release lock + next bucket ---- */
+	knod_bpf_set_label(meta, &labels[LABEL_UNLOCK], meta->amdgpu_insns);
+
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG0_LO);
+
+	/* Recompute lock_addr (r64[10] clobbered) */
+	knod_iset64(&p64[0], lock_offset);
+	knod_add64(priv, meta, r64[10], p64[0], r64[15]);
+
+	knod_emit(priv, meta, v_mov_b32_e32, r64[1].lo, v_zero);
+	knod_emit(priv, meta, global_store_dword, r64[1].lo, r64[10].lo,
+		  0);
+	knod_wait_vmcnt(priv, meta);
+
+	/* Next bucket */
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  KNOD_AMDGPU_TMP_SREG5_LO, KNOD_AMDGPU_TMP_SREG0_LO);
+
+	emit_s_cbranch_execnz(priv->isa_version,
+			      &meta->amdgpu_insn[meta->amdgpu_insns],
+			      0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_BUCKET_LOOP], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* ======== Phase 3: Restore ======== */
+
+	/* exec = SREG3 (restore all original lanes) */
+	knod_emit(priv, meta, s_or_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG3_LO);
+
+	/* bpf_reg64[0] = 0 (return value) */
+	knod_iset64(&p64[0], 0);
+	knod_mov64(priv, meta, bpf_reg64[0], p64[0]);
+
+	for (idx = 0; idx < fixup_idx; idx++)
+		knod_bpf_fixup_branch(priv, &fixups[idx]);
+
+	return;
+#undef LABEL_BUCKET_LOOP
+#undef LABEL_LOCK_RETRY
+#undef LABEL_DELETE_LANE
+#undef LABEL_CHAIN_NEXT
+#undef LABEL_LANE_DONE
+#undef LABEL_UNLOCK
+}
+
+static void knod_bpf_map_delete_array(struct knod_bpf_priv *priv,
+				      struct knod_insn_meta *meta,
+				      int map_id)
+{
+	struct knod_bpf_map_obj *knod_map_obj_k, *knod_map_obj_g;
+	struct amdgcn_branch_fixup fixups[4] = {0,};
+	u32 key_stack_off = meta->kreg.stack_off;
+	struct amdgcn_label labels[10] = {0,};
+	int idx, val_off, val_len;
+	struct amdgcn_param32 v_zero;
+	unsigned long bucket_gaddr;
+	int fixup_idx = 0;
+
+	knod_map_obj_k =
+		(struct knod_bpf_map_obj *)knod_bpf_map_kaddr(priv, map_id);
+	knod_map_obj_g =
+		(struct knod_bpf_map_obj *)knod_bpf_map_gaddr(priv, map_id);
+	bucket_gaddr = (unsigned long)knod_map_obj_g +
+		       offsetof(struct knod_bpf_map_obj, bucket);
+
+	if (!knod_map_obj_g || !knod_map_obj_k)
+		WARN_ON_ONCE(1);
+
+	/* load key from stack -> r64[2] */
+	knod_bpf_load_size(priv, meta,
+			       &r64[2],
+			       &stack[0],
+			       sizeof(unsigned int),
+			       512 + key_stack_off);
+
+	/* r64[1] = bucket_gaddr */
+	knod_iset64(&p64[0], bucket_gaddr);
+	knod_mov64(priv, meta, r64[1], p64[0]);
+
+	/* clear r64[2].hi (key is 32-bit) */
+	knod_iset64(&p64[1], 0);
+	knod_mov32(priv, meta, r64[2].hi, p64[1].lo);
+
+	/* bpf_reg64[0] = 0 (return value) */
+	knod_mov64(priv, meta, bpf_reg64[0], p64[1]);
+
+	/* bounds check: if (key >= max_entries) -> skip */
+	knod_iset64(&p64[1], knod_map_obj_k->max_entries);
+	knod_mov64(priv, meta, r64[3], p64[1]);
+	knod_emit(priv, meta, v_cmp_ge_u64, r64[2], r64[3]);
+
+	/* structurized CFG: save OOB lanes, narrow exec */
+	knod_emit(priv, meta, s_and_b64, KNOD_AMDGPU_TMP_SREG3_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+	emit_s_cbranch_execz(priv->isa_version,
+			     &meta->amdgpu_insn[meta->amdgpu_insns],
+			     0);
+	knod_bpf_set_fixup(meta, &fixups[fixup_idx],
+			   &labels[LABEL_OUT], meta->amdgpu_insns);
+	debug_insn(priv->isa_version, &meta->amdgpu_insn[meta->amdgpu_insns]);
+	meta->amdgpu_insns++;
+	fixup_idx++;
+
+	/* dest = bucket_gaddr + key * value_size -> r64[0] */
+	knod_iset64(&p64[1], knod_map_obj_k->value_size);
+	knod_emit(priv, meta, v_mad_u64_u32, r64[0], sr64[0].lo,
+		  p64[1].lo, r64[2].lo, r64[1]);
+
+	/* Zero out value at dest */
+	knod_iset32(&v_zero, 0);
+	knod_emit(priv, meta, v_mov_b32_e32, r64[3].lo, v_zero);
+	knod_emit(priv, meta, v_mov_b32_e32, r64[3].hi, v_zero);
+	knod_emit(priv, meta, v_mov_b32_e32, r64[4].lo, v_zero);
+	knod_emit(priv, meta, v_mov_b32_e32, r64[4].hi, v_zero);
+
+	val_off = 0;
+	val_len = knod_map_obj_k->value_size;
+
+	while (val_len >= 16) {
+		knod_emit(priv, meta, global_store_dwordx4, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 16;
+		val_len -= 16;
+	}
+
+	if (val_len >= 8) {
+		knod_emit(priv, meta, global_store_dwordx2, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 8;
+		val_len -= 8;
+	}
+
+	if (val_len >= 4) {
+		knod_emit(priv, meta, global_store_dword, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 4;
+		val_len -= 4;
+	}
+
+	if (val_len >= 2) {
+		knod_emit(priv, meta, global_store_short, r64[3].lo,
+			  r64[0].lo, val_off);
+		val_off += 2;
+		val_len -= 2;
+	}
+
+	if (val_len >= 1) {
+		knod_emit(priv, meta, global_store_byte, r64[3].lo,
+			  r64[0].lo, val_off);
+	}
+
+	knod_wait_vmcnt(priv, meta);
+
+	/* structurized CFG: restore OOB lanes */
+	knod_bpf_set_label(meta, &labels[LABEL_OUT], meta->amdgpu_insns);
+	knod_emit(priv, meta, s_or_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, KNOD_AMDGPU_TMP_SREG3_LO);
+
+	for (idx = 0; idx < fixup_idx; idx++)
+		knod_bpf_fixup_branch(priv, &fixups[idx]);
+}
+
+static void knod_bpf_store_cache_size(struct knod_bpf_priv *priv,
+				     struct knod_insn_meta *meta,
+				     struct amdgcn_param64 *src,
+				     /* packet or stack */
+				     struct amdgcn_param32 *cache,
+				     int size, int off)
+{
+	struct amdgcn_param32 p32[2];
+
+	knod_jit_dbg(" %d: off = %d off_4 = %d size = %d\n", meta->bpf_insn_idx,
+		off, off%4, size);
+	WARN_ON(knod_param_is_literal(src->lo) ||
+		knod_param_is_literal(src->hi));
+	switch (size) {
+	case sizeof(unsigned long):
+		if ((off % 4) == 0) {
+			knod_mov32(priv, meta,
+				       cache[off / 4],
+				       src->lo);
+			knod_mov32(priv, meta,
+				       cache[(off / 4) + 1],
+				       src->hi);
+		} else if ((off % 4) == 1) {
+			WARN_ON_ONCE(1);
+		} else if ((off % 4) == 2) {
+			WARN_ON_ONCE(1);
+		} else {
+			WARN_ON_ONCE(1);
+		}
+		break;
+	case sizeof(unsigned int):
+		if ((off % 4) == 0) {
+			knod_mov32(priv, meta,
+				       cache[off / 4],
+				       src->lo);
+		} else if ((off % 4) == 1) {
+			knod_iset64(&p64[0], 8);
+			knod_lshlrev64(priv, meta, r64[0], p64[0], *src);
+
+			knod_iset32(&p32[0], 0xffffff00);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4],
+				       r32[2], r64[0].lo, cache[off / 4]);
+			knod_iset32(&p32[0], 0x000000ff);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[(off / 4) + 1], r32[2],
+				       r64[0].hi, cache[(off / 4) + 1]);
+		} else if ((off % 4) == 2) {
+			knod_iset64(&p64[0], 16);
+			knod_lshlrev64(priv, meta, r64[0], p64[0], *src);
+
+			knod_iset32(&p32[0], 0xffff0000);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4], r32[2],
+				       r64[0].lo, cache[off / 4]);
+			knod_iset32(&p32[0], 0x0000ffff);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[(off / 4) + 1], r32[2],
+				       r64[0].hi, cache[(off / 4) + 1]);
+		} else {
+			knod_iset64(&p64[0], 24);
+			knod_lshlrev64(priv, meta, r64[0], p64[0], *src);
+
+			knod_iset32(&p32[0], 0xffff0000);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4], r32[2],
+				       r64[0].lo, cache[off / 4]);
+			knod_iset32(&p32[0], 0x00ffffff);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[(off / 4) + 1], r32[2],
+				       r64[0].hi, cache[(off / 4) + 1]);
+		}
+		break;
+	case sizeof(unsigned short):
+		if ((off % 4) == 0) {
+			knod_iset32(&p32[0], 0x0000ffff);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4], r32[2],
+				       src->lo, cache[off / 4]);
+		} else if ((off % 4) == 1) {
+			knod_iset32(&p32[0], 8);
+			knod_lshlrev32(priv, meta, r32[0], p32[0],
+					   src->lo);
+			knod_iset32(&p32[0], 0x00ffff00);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4], r32[2],
+				       r32[0], cache[off / 4]);
+		} else if ((off % 4) == 2) {
+			knod_iset32(&p32[0], 16);
+			knod_lshlrev32(priv, meta, r32[0], p32[0],
+					   src->lo);
+			knod_iset32(&p32[0], 0xffff0000);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4], r32[2],
+				       r32[0], cache[off / 4]);
+		} else {
+			knod_iset64(&p64[0], 24);
+			knod_lshlrev64(priv, meta, r64[0], p64[0], *src);
+
+			knod_iset32(&p32[0], 0xff000000);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4], r32[2],
+				       r64[0].lo, cache[off / 4]);
+			knod_iset32(&p32[0], 0x000000ff);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[(off / 4) + 1], r32[2],
+				       r64[0].hi, cache[(off / 4) + 1]);
+		}
+		break;
+	case sizeof(unsigned char):
+		if ((off % 4) == 0) {
+			knod_iset32(&p32[0], 0x000000ff);
+			knod_mov32(priv, meta, r32[2], p32[0]);
+			knod_bfi32(priv, meta, cache[off / 4], r32[2],
+				       src->lo, cache[off / 4]);
+			return;
+		} else if ((off % 4) == 1) {
+			knod_iset32(&p32[0], 8);
+			knod_lshlrev32(priv, meta, r32[0], p32[0],
+					   src->lo);
+
+			knod_iset32(&p32[0], 0x0000ff00);
+		} else if ((off % 4) == 2) {
+			knod_iset32(&p32[0], 16);
+			knod_lshlrev32(priv, meta, r32[0], p32[0],
+					   src->lo);
+
+			knod_iset32(&p32[0], 0x00ff0000);
+		} else {
+			knod_iset32(&p32[0], 24);
+			knod_lshlrev32(priv, meta, r32[0], p32[0],
+					   src->lo);
+			knod_iset32(&p32[0], 0xff000000);
+		}
+
+		knod_mov32(priv, meta, r32[2], p32[0]);
+		knod_bfi32(priv, meta, cache[off / 4], r32[2], r32[0],
+			       cache[off / 4]);
+		break;
+	default:
+		WARN_ON_ONCE(1);
+	}
+}
+
+static bool knod_meta_is_exit(const struct knod_insn_meta *meta);
+static bool knod_bpf_is_retval_move_to_r0(const struct knod_insn_meta *meta);
+
+/*
+ * knod_bpf_emit_branch_tail - Emit EXEC mask manipulation after v_cmp for
+ * structurized per-lane branching. Replaces the old s_cbranch_vccnz/vccz.
+ *
+ * For FORWARD_SKIP:
+ *   Save jumping lanes -> narrow EXEC -> s_cbranch_execz
+ *   (skip if no active lanes)
+ *
+ * For DIRECT_EXIT:
+ *   Compute exit lanes -> update done_mask -> remove from EXEC (no branch)
+ *
+ * Emits the required EXEC mask manipulation in-place.
+ */
+static void knod_bpf_emit_direct_exit_retval(struct knod_bpf_priv *priv,
+					     struct knod_insn_meta *emit_meta,
+					     struct knod_insn_meta *target)
+{
+	struct amdgcn_param64 dst, src;
+	s64 imm;
+
+	if (!target || knod_meta_is_exit(target))
+		return;
+
+	if (WARN_ON_ONCE(!knod_bpf_is_retval_move_to_r0(target)))
+		return;
+
+	knod_vset64(&dst, KNOD_AMDGPU_VREG0_LO);
+
+	switch (target->insn.code) {
+	case BPF_ALU | BPF_MOV | BPF_X:
+	case BPF_ALU64 | BPF_MOV | BPF_X:
+		knod_vset64(&src, target->insn.src_reg * 2);
+		knod_mov64(priv, emit_meta, dst, src);
+		break;
+	case BPF_ALU | BPF_MOV | BPF_K:
+		imm = (u32)target->insn.imm;
+		knod_iset64(&src, imm);
+		knod_mov64(priv, emit_meta, dst, src);
+		break;
+	case BPF_ALU64 | BPF_MOV | BPF_K:
+		imm = (s64)(s32)target->insn.imm;
+		knod_iset64(&src, imm);
+		knod_mov64(priv, emit_meta, dst, src);
+		break;
+	default:
+		WARN_ON_ONCE(1);
+		break;
+	}
+}
+
+static void knod_bpf_emit_branch_tail(struct knod_bpf_priv *priv,
+				      struct knod_insn_meta *meta,
+				      struct knod_prog *knod_prog,
+				      short off)
+{
+	switch (meta->branch_type) {
+	case KNOD_BR_FORWARD_SKIP:
+		if (meta->jump_neg_op) {
+			/* JNE: VCC=0 -> jump, VCC=1 -> fall-through.
+			 * Save jump lanes (VCC=0): s[n] = exec & ~vcc
+			 */
+			knod_emit(priv, meta, s_andn2_b64,
+				  meta->exec_save_sreg,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_VCC_LO);
+
+			/* Keep fall-through (VCC=1): exec = exec & vcc */
+			knod_emit(priv, meta, s_and_b64,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_VCC_LO);
+		} else {
+			/* Normal: VCC=1 -> jump, VCC=0 -> fall-through.
+			 * Save jump lanes (VCC=1): s[n] = exec & vcc
+			 */
+			knod_emit(priv, meta, s_and_b64,
+				  meta->exec_save_sreg,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_VCC_LO);
+
+			/* Keep fall-through (VCC=0): exec = exec & ~vcc */
+			knod_emit(priv, meta, s_andn2_b64,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_VCC_LO);
+		}
+
+		/*
+		 * No GPU branch.  After the RPO reorder, branch scopes
+		 * interleave, so the jumping lanes must flow through every
+		 * following block under the EXEC mask and rejoin at their merge
+		 * point.  An s_cbranch_execz skipping ahead to the merge would
+		 * jump over other scopes' merge points and strand their saved
+		 * lanes (EXEC never restored -> act=0).
+		 */
+		break;
+
+	case KNOD_BR_DIRECT_EXIT:
+		if (meta->jump_neg_op) {
+			/* JNE: VCC=0 -> exit. exit_lanes = exec & ~vcc */
+			knod_emit(priv, meta, s_andn2_b64,
+				  KNOD_AMDGPU_TMP_SREG0_LO,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_VCC_LO);
+		} else {
+			/* Normal: VCC=1 -> exit. exit_lanes = exec & vcc */
+			knod_emit(priv, meta, s_and_b64,
+				  KNOD_AMDGPU_TMP_SREG0_LO,
+				  AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_VCC_LO);
+		}
+
+		/* Keep the lanes that did not take the exit path. */
+		knod_emit(priv, meta, s_andn2_b64,
+			  KNOD_AMDGPU_TMP_SREG1_LO,
+			  AMDGCN_SREG_EXEC_LO,
+			  KNOD_AMDGPU_TMP_SREG0_LO);
+
+		/* Replay a shared "r0 = action; exit" target under the
+		 * exiting lanes before marking them done.  Otherwise a direct
+		 * branch to the common exit can publish stale r0 scratch state.
+		 */
+		knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+			  KNOD_AMDGPU_TMP_SREG0_LO);
+		knod_bpf_emit_direct_exit_retval(priv, meta, meta->merge_point);
+
+		/* done_mask |= exit_lanes */
+		knod_emit(priv, meta, s_or_b64,
+			  knod_prog->done_mask_sreg,
+			  knod_prog->done_mask_sreg,
+			  AMDGCN_SREG_EXEC_LO);
+
+		/* Continue with the non-exit lanes. */
+		knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+			  KNOD_AMDGPU_TMP_SREG1_LO);
+
+		/* No branch - fall through with reduced EXEC.
+		 * No fixup needed.
+		 */
+		meta->jmp_dst = NULL;
+		break;
+
+	default:
+		WARN_ON_ONCE(1);
+		break;
+	}
+}
+
+/*
+ * --- Basic-block CFG analysis (foundation for block reordering) ---
+ *
+ * The emitter is a linear SIMT machine: instructions run in list order under
+ * an EXEC mask.  A *forward* jump is realized by masking off the jumping
+ * lanes and restoring them at the merge point.  A *backward* jump has no such
+ * realization unless it is a loop (real GPU branch + EXEC convergence, not yet
+ * implemented).
+ *
+ * LLVM tail-sharing and block placement routinely emit jumps that are
+ * backward in BPF byte order but are NOT loops - e.g. a UDP bounds check that
+ * jumps back to a shared XDP_PASS tail.  Classifying those as "exit" (the
+ * jmp_off < 0 heuristic in knod_bpf_analyze_cfg) silently miscompiles them:
+ * the jumping lanes exit carrying whatever R0 happened to hold instead of
+ * flowing to the real target.
+ *
+ * The fix is to classify by control-flow, not byte order:
+ *   1. partition the instruction stream into basic blocks,
+ *   2. build the control-flow graph (successor edges),
+ *   3. DFS for a reverse-postorder (RPO) and detect back-edges,
+ *   4. no back-edges (a DAG)  -> reorder blocks into RPO so every edge points
+ *      forward, then classify by linear position,
+ *   5. a real loop is present -> bail (-EOPNOTSUPP) until loop emission lands.
+ *
+ * Loop emission (step 5) is not implemented yet, so programs containing a
+ * loop are rejected with -EOPNOTSUPP.
+ */
+struct knod_bb {
+	struct knod_insn_meta *leader;	/* first instruction of the block */
+	struct knod_insn_meta *last;	/* last instruction of the block */
+	/* successors: [0] not-taken, [1] taken */
+	struct knod_bb *succ[2];
+	int n_succ;
+	/* reverse-postorder rank, -1 if unreachable */
+	int rpo;
+	/* DFS color: 0 white, 1 gray, 2 black */
+	int dfs;
+	bool loop_header;		/* target of a back-edge */
+	/* scratch: member of the loop being walked */
+	bool in_loop;
+	/* immediate dominator (self for entry) */
+	struct knod_bb *idom;
+};
+
+static bool knod_meta_is_exit(const struct knod_insn_meta *meta)
+{
+	u8 code = meta->insn.code;
+
+	return code == (BPF_JMP | BPF_EXIT) || code == (BPF_JMP32 | BPF_EXIT);
+}
+
+static struct knod_insn_meta *
+knod_bpf_next_meta(struct knod_prog *knod_prog, struct knod_insn_meta *meta)
+{
+	if (!meta || list_is_last(&meta->l, &knod_prog->insns))
+		return NULL;
+
+	return list_next_entry(meta, l);
+}
+
+static bool knod_bpf_is_retval_move_to_r0(const struct knod_insn_meta *meta)
+{
+	u8 code;
+
+	if (!meta || meta->insn.dst_reg != BPF_REG_0)
+		return false;
+
+	code = meta->insn.code;
+	return code == (BPF_ALU | BPF_MOV | BPF_X) ||
+	       code == (BPF_ALU64 | BPF_MOV | BPF_X) ||
+	       code == (BPF_ALU | BPF_MOV | BPF_K) ||
+	       code == (BPF_ALU64 | BPF_MOV | BPF_K);
+}
+
+static bool knod_bpf_is_direct_exit_target(struct knod_prog *knod_prog,
+					   struct knod_insn_meta *target)
+{
+	if (knod_meta_is_exit(target))
+		return true;
+
+	if (!knod_bpf_is_retval_move_to_r0(target))
+		return false;
+
+	return knod_meta_is_exit(knod_bpf_next_meta(knod_prog, target));
+}
+
+static bool knod_meta_is_ja(const struct knod_insn_meta *meta)
+{
+	u8 code = meta->insn.code;
+
+	return code == (BPF_JMP | BPF_JA | BPF_K) ||
+	       code == (BPF_JMP32 | BPF_JA | BPF_K);
+}
+
+/* A block ends after a terminator; the next instruction starts a new block. */
+static bool knod_meta_is_terminator(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_cond_jump(meta) || knod_meta_is_ja(meta) ||
+	       knod_meta_is_exit(meta);
+}
+
+/* Target instruction index of a conditional jump or BPF_JA. */
+static short knod_meta_jump_target_idx(const struct knod_insn_meta *meta)
+{
+	if (meta->insn.code == (BPF_JMP32 | BPF_JA | BPF_K))
+		return meta->bpf_insn_idx + meta->insn.imm + 1;
+	return meta->bpf_insn_idx + meta->insn.off + 1;
+}
+
+static struct knod_bb *knod_bb_of_leader(struct knod_bb *bbs, int n_bbs,
+					 const struct knod_insn_meta *meta)
+{
+	int i;
+
+	for (i = 0; i < n_bbs; i++)
+		if (bbs[i].leader == meta)
+			return &bbs[i];
+	return NULL;
+}
+
+/* Resolve the block a conditional jump / BPF_JA at @jmp transfers to. */
+static struct knod_bb *knod_bb_jump_target(struct knod_prog *knod_prog,
+					   struct knod_bb *bbs, int n_bbs,
+					   const struct knod_insn_meta *jmp)
+{
+	struct knod_insn_meta *tgt;
+
+	tgt = knod_bpf_lookup_meta(knod_prog, knod_meta_jump_target_idx(jmp));
+	return tgt ? knod_bb_of_leader(bbs, n_bbs, tgt) : NULL;
+}
+
+/*
+ * Partition knod_prog->insns into basic blocks.  A leader is the first
+ * instruction, any jump target, or the instruction after a terminator.
+ * Returns the block count or a negative errno; @bbs holds >= n_insns blocks.
+ */
+static int knod_bpf_build_bbs(struct knod_prog *knod_prog, struct knod_bb *bbs)
+{
+	struct knod_insn_meta *meta, *tgt;
+	struct knod_bb *cur = NULL;
+	int n_bbs = 0;
+	short tgt_idx;
+
+	/* Pass A: mark every jump target as a leader. */
+	list_for_each_entry(meta, &knod_prog->insns, l)
+		meta->flags &= ~FLAG_INSN_IS_JUMP_DST;
+
+	list_for_each_entry(meta, &knod_prog->insns, l) {
+		if (!is_mbpf_cond_jump(meta) && !knod_meta_is_ja(meta))
+			continue;
+		tgt_idx = knod_meta_jump_target_idx(meta);
+		tgt = knod_bpf_lookup_meta(knod_prog, tgt_idx);
+		if (!tgt) {
+			pr_warn("knod_cfg: bpf#%d jump target %d unresolved\n",
+				meta->bpf_insn_idx, tgt_idx);
+			return -EINVAL;
+		}
+		tgt->flags |= FLAG_INSN_IS_JUMP_DST;
+	}
+
+	/* Pass B: cut the list into blocks. */
+	list_for_each_entry(meta, &knod_prog->insns, l) {
+		if (!cur || (meta->flags & FLAG_INSN_IS_JUMP_DST)) {
+			cur = &bbs[n_bbs++];
+			cur->leader = meta;
+			cur->n_succ = 0;
+		}
+		cur->last = meta;
+
+		if (knod_meta_is_terminator(meta))
+			/* next instruction starts a new block */
+			cur = NULL;
+	}
+
+	return n_bbs;
+}
+
+/* Build successor edges for every block from its terminator. */
+static int knod_bpf_build_edges(struct knod_prog *knod_prog,
+				struct knod_bb *bbs, int n_bbs)
+{
+	struct knod_bb *bb, *fall, *tgt_bb;
+	struct knod_insn_meta *last;
+	int i;
+
+	for (i = 0; i < n_bbs; i++) {
+		bb = &bbs[i];
+		last = bb->last;
+		bb->n_succ = 0;
+
+		if (knod_meta_is_exit(last))
+			continue;			/* no successors */
+
+		/* Successor in list order: block led by the next
+		 * instruction.
+		 */
+		fall = NULL;
+		if (!list_is_last(&last->l, &knod_prog->insns))
+			fall = knod_bb_of_leader(bbs, n_bbs,
+						 list_next_entry(last, l));
+
+		if (is_mbpf_cond_jump(last)) {
+			tgt_bb = knod_bb_jump_target(knod_prog, bbs, n_bbs,
+						     last);
+			if (!fall || !tgt_bb)
+				return -EINVAL;
+			bb->succ[bb->n_succ++] = fall;		/* not taken */
+			bb->succ[bb->n_succ++] = tgt_bb;	/* taken */
+		} else if (knod_meta_is_ja(last)) {
+			tgt_bb = knod_bb_jump_target(knod_prog, bbs, n_bbs,
+						     last);
+			if (!tgt_bb)
+				return -EINVAL;
+			bb->succ[bb->n_succ++] = tgt_bb;
+		} else {
+			if (!fall)			/* fell off the end */
+				return -EINVAL;
+			bb->succ[bb->n_succ++] = fall;
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * Iterative DFS from the entry block.  Computes a reverse-postorder rank for
+ * every reachable block and flags back-edge targets as loop headers.  Returns
+ * the number of back-edges in *n_back, or a negative errno.
+ */
+static int knod_bpf_compute_rpo(struct knod_bb *bbs, int n_bbs,
+				struct knod_bb *entry, int *n_back)
+{
+	struct knod_bb **stack;
+	int *cursor;
+	int top = 0, post = 0, nb = 0, i;
+
+	for (i = 0; i < n_bbs; i++) {
+		bbs[i].dfs = 0;
+		bbs[i].rpo = -1;
+		bbs[i].loop_header = false;
+	}
+
+	stack = kcalloc(n_bbs, sizeof(*stack), GFP_KERNEL);
+	cursor = kcalloc(n_bbs, sizeof(*cursor), GFP_KERNEL);
+	if (!stack || !cursor) {
+		kfree(stack);
+		kfree(cursor);
+		return -ENOMEM;
+	}
+
+	entry->dfs = 1;
+	stack[top] = entry;
+	cursor[top] = 0;
+	top++;
+
+	while (top > 0) {
+		struct knod_bb *bb = stack[top - 1];
+
+		if (cursor[top - 1] < bb->n_succ) {
+			struct knod_bb *s = bb->succ[cursor[top - 1]++];
+
+			if (s->dfs == 0) {		/* tree edge */
+				s->dfs = 1;
+				stack[top] = s;
+				cursor[top] = 0;
+				top++;
+			} else if (s->dfs == 1) {	/* gray -> back-edge */
+				s->loop_header = true;
+				nb++;
+			}
+			/* s->dfs == 2 -> forward/cross edge, nothing to do */
+		} else {
+			/* finished: postorder */
+			bb->dfs = 2;
+			bb->rpo = post++;
+			top--;
+		}
+	}
+
+	/* postorder -> reverse-postorder rank */
+	for (i = 0; i < n_bbs; i++)
+		if (bbs[i].rpo >= 0)
+			bbs[i].rpo = post - 1 - bbs[i].rpo;
+
+	kfree(stack);
+	kfree(cursor);
+	*n_back = nb;
+	return 0;
+}
+
+/*
+ * Cooper-Harvey-Kennedy dominator intersect: walk the two fingers up the idom
+ * chain (toward the entry, which has the lowest RPO) until they meet.
+ */
+static struct knod_bb *knod_dom_intersect(struct knod_bb *a, struct knod_bb *b)
+{
+	while (a != b) {
+		while (a->rpo > b->rpo)
+			a = a->idom;
+		while (b->rpo > a->rpo)
+			b = b->idom;
+	}
+	return a;
+}
+
+/*
+ * Compute the immediate dominator of every reachable block (Cooper, Harvey,
+ * Kennedy, "A Simple, Fast Dominance Algorithm").  Iterates over RPO to a
+ * fixpoint; bb->idom is the block's immediate dominator, the entry dominating
+ * itself.  Requires bb->rpo from knod_bpf_compute_rpo.
+ */
+static int knod_bpf_compute_dom(struct knod_bb *bbs, int n_bbs,
+				struct knod_bb *entry)
+{
+	struct knod_bb **order;
+	int i, k, n_order = 0;
+	bool changed;
+
+	order = kcalloc(n_bbs, sizeof(*order), GFP_KERNEL);
+	if (!order)
+		return -ENOMEM;
+
+	for (i = 0; i < n_bbs; i++) {
+		bbs[i].idom = NULL;
+		if (bbs[i].rpo >= 0) {
+			order[bbs[i].rpo] = &bbs[i];
+			n_order++;
+		}
+	}
+	entry->idom = entry;
+
+	do {
+		changed = false;
+
+		/* process every reachable block but the entry, in RPO order */
+		for (k = 1; k < n_order; k++) {
+			struct knod_bb *n = order[k];
+			struct knod_bb *new_idom = NULL;
+			int b, s;
+
+			/* intersect over already-processed predecessors */
+			for (b = 0; b < n_bbs; b++) {
+				for (s = 0; s < bbs[b].n_succ; s++) {
+					if (bbs[b].succ[s] != n || !bbs[b].idom)
+						continue;
+					new_idom = new_idom ?
+						knod_dom_intersect(&bbs[b],
+								   new_idom) :
+						&bbs[b];
+				}
+			}
+
+			if (new_idom && n->idom != new_idom) {
+				n->idom = new_idom;
+				changed = true;
+			}
+		}
+	} while (changed);
+
+	kfree(order);
+	return 0;
+}
+
+/* Does block @a dominate block @b?  Walk @b up the idom chain to the entry. */
+static bool knod_dom_dominates(struct knod_bb *a, struct knod_bb *b)
+{
+	for (;;) {
+		if (b == a)
+			return true;
+		if (b->idom == b)	/* reached the entry */
+			return false;
+		b = b->idom;
+	}
+}
+
+/*
+ * Mark the natural loop body of back-edge @latch->@hdr in bb->in_loop: the
+ * header plus every block that reaches the latch without passing through the
+ * header, found by walking predecessors back from the latch.  @stack is
+ * caller-provided scratch of at least @n_bbs entries.
+ */
+static void knod_loop_mark_body(struct knod_bb *bbs, int n_bbs,
+				struct knod_bb *latch, struct knod_bb *hdr,
+				struct knod_bb **stack)
+{
+	int b, sp, k, top = 0;
+
+	for (k = 0; k < n_bbs; k++)
+		bbs[k].in_loop = false;
+
+	hdr->in_loop = true;
+	if (latch != hdr) {
+		latch->in_loop = true;
+		stack[top++] = latch;
+	}
+
+	while (top > 0) {
+		struct knod_bb *d = stack[--top];
+
+		for (b = 0; b < n_bbs; b++) {
+			if (bbs[b].in_loop)
+				continue;
+			for (sp = 0; sp < bbs[b].n_succ; sp++) {
+				if (bbs[b].succ[sp] != d)
+					continue;
+				bbs[b].in_loop = true;
+				stack[top++] = &bbs[b];
+				break;
+			}
+		}
+	}
+}
+
+/*
+ * Detect natural loops from the dominator tree and report their structure.
+ *
+ * A back-edge is an edge u->v whose target v dominates its source u - v is
+ * the loop header, u the latch.  Its natural loop body is the header plus the
+ * blocks that reach the latch without passing through the header; an exit edge
+ * leaves a body block for a non-body block.
+ *
+ * Loops are still rejected by the reorder (-EOPNOTSUPP); this only reports what
+ * was found (to dmesg, since a rejected program never attaches so /bpf/cfg is
+ * unavailable) so the detection can be verified before emission is built.
+ */
+static int knod_bpf_detect_loops(struct knod_bb *bbs, int n_bbs)
+{
+	struct knod_bb **stack;
+	int u, s, k, n_be = 0;
+
+	stack = kcalloc(n_bbs, sizeof(*stack), GFP_KERNEL);
+	if (!stack)
+		return -ENOMEM;
+
+	for (u = 0; u < n_bbs; u++) {
+		for (s = 0; s < bbs[u].n_succ; s++) {
+			struct knod_bb *hdr = bbs[u].succ[s];
+			int body = 0, exits = 0, sp;
+
+			if (!knod_dom_dominates(hdr, &bbs[u]))
+				continue;	/* not a back-edge */
+			n_be++;
+
+			knod_loop_mark_body(bbs, n_bbs, &bbs[u], hdr, stack);
+
+			for (k = 0; k < n_bbs; k++) {
+				if (!bbs[k].in_loop)
+					continue;
+				body++;
+				for (sp = 0; sp < bbs[k].n_succ; sp++)
+					if (!bbs[k].succ[sp]->in_loop)
+						exits++;
+			}
+
+			pr_info("knod_loop: back-edge bpf#%d -> bpf#%d (latch->header) body=%d exits=%d\n",
+				bbs[u].leader->bpf_insn_idx,
+				hdr->leader->bpf_insn_idx, body, exits);
+		}
+	}
+
+	kfree(stack);
+
+	if (n_be)
+		pr_info("knod_loop: %d back-edge(s) - %s\n", n_be,
+			n_be == 1 ? "single loop (simple-shape candidate)" :
+				    "nested/multiple loops (complex)");
+	return 0;
+}
+
+/*
+ * Block that lanes fall into in list order when the terminator is not taken:
+ * the not-taken successor of a conditional jump, or the sole successor of a
+ * block that ended only because the next instruction was a leader.  BPF_JA and
+ * EXIT have no such successor (control leaves explicitly).
+ */
+static struct knod_bb *knod_bb_fall_succ(struct knod_bb *bb)
+{
+	if (knod_meta_is_exit(bb->last) || knod_meta_is_ja(bb->last))
+		return NULL;
+	return bb->n_succ ? bb->succ[0] : NULL;
+}
+
+/*
+ * Reorder the instruction list into reverse-postorder so every control-flow
+ * edge points forward, and splice in a synthetic BPF_JA wherever a block's
+ * not-taken successor no longer follows it in list order.  After this the
+ * emitter's forward-only machinery (FORWARD_SKIP / FORWARD_GOTO) handles the
+ * whole program - including the backward-in-byte-order, non-loop jumps that
+ * the old jmp_off < 0 heuristic miscompiled.
+ *
+ * Loops (back-edges) are rejected with -EOPNOTSUPP until loop emission lands.
+ */
+static int knod_bpf_reorder_rpo(struct knod_prog *knod_prog,
+				struct knod_bb *bbs, int n_bbs, int n_back)
+{
+	struct knod_insn_meta *m, *nx, *sj;
+	struct knod_bb **order;
+	int n_order = 0, r, i, k, idx = 0;
+	LIST_HEAD(new_list);
+
+	if (n_back) {
+		pr_warn("knod_cfg: %d loop back-edge(s) - block reorder cannot lower loops yet (-EOPNOTSUPP)\n",
+			n_back);
+		return -EOPNOTSUPP;
+	}
+
+	order = kcalloc(n_bbs, sizeof(*order), GFP_KERNEL);
+	if (!order)
+		return -ENOMEM;
+
+	/* Reachable blocks in RPO, then any unreachable ones so no instruction
+	 * is dropped from the list.
+	 */
+	for (r = 0; r < n_bbs; r++)
+		for (i = 0; i < n_bbs; i++)
+			if (bbs[i].rpo == r) {
+				order[n_order++] = &bbs[i];
+				break;
+			}
+	for (i = 0; i < n_bbs; i++)
+		if (bbs[i].rpo < 0)
+			order[n_order++] = &bbs[i];
+
+	for (k = 0; k < n_order; k++) {
+		struct knod_bb *bb = order[k];
+		struct knod_bb *next = (k + 1 < n_order) ? order[k + 1] : NULL;
+		struct knod_bb *fall;
+
+		m = bb->leader;
+		while (true) {
+			nx = (m == bb->last) ? NULL : knod_meta_next(m);
+			list_move_tail(&m->l, &new_list);
+			if (m == bb->last)
+				break;
+			m = nx;
+		}
+
+		fall = knod_bb_fall_succ(bb);
+		if (!fall || (next && next->leader == fall->leader))
+			continue;
+
+		/* Not-taken successor no longer adjacent: route it
+		 * explicitly.
+		 */
+		sj = kzalloc_obj(*sj, GFP_KERNEL);
+		if (!sj) {
+			list_splice(&new_list, &knod_prog->insns);
+			kfree(order);
+			return -ENOMEM;
+		}
+		sj->insn.code = BPF_JMP | BPF_JA | BPF_K;
+		/* synthetic, never a jump target */
+		sj->bpf_insn_idx = -1;
+		/* consumed by classify_linear */
+		sj->jmp_dst = fall->leader;
+		INIT_LIST_HEAD(&sj->l);
+		list_add_tail(&sj->l, &new_list);
+	}
+
+	list_splice(&new_list, &knod_prog->insns);
+
+	list_for_each_entry(m, &knod_prog->insns, l)
+		m->linear_idx = idx++;
+
+	kfree(order);
+	return 0;
+}
+
+/*
+ * Classify branches by linear position after the RPO reorder.  Every edge is
+ * now forward, so a conditional jump is FORWARD_SKIP (or DIRECT_EXIT when it
+ * targets the exit), and every BPF_JA - real or synthetic - is FORWARD_GOTO
+ * (or DIRECT_EXIT).
+ */
+static int knod_bpf_classify_linear(struct knod_prog *knod_prog)
+{
+	struct knod_insn_meta *meta, *target;
+	short ti;
+
+	list_for_each_entry(meta, &knod_prog->insns, l) {
+		if (is_mbpf_cond_jump(meta)) {
+			meta->jump_neg_op = (mbpf_op(meta) == BPF_JNE);
+			ti = knod_meta_jump_target_idx(meta);
+			target = knod_bpf_lookup_meta(knod_prog, ti);
+		} else if (knod_meta_is_ja(meta)) {
+			/* synthetic JA carries its destination in jmp_dst;
+			 * a real BPF_JA is resolved from its offset.
+			 */
+			if (meta->jmp_dst) {
+				target = meta->jmp_dst;
+			} else {
+				ti = knod_meta_jump_target_idx(meta);
+				target = knod_bpf_lookup_meta(knod_prog, ti);
+			}
+		} else {
+			continue;
+		}
+
+		if (!target) {
+			pr_err("knod_cfg: bpf#%d unresolved branch target\n",
+			       meta->bpf_insn_idx);
+			return -EINVAL;
+		}
+
+		if (target->linear_idx <= meta->linear_idx)
+			pr_warn("knod_cfg: bpf#%d -> #%d still backward after reorder (linear %d -> %d)\n",
+				meta->bpf_insn_idx, target->bpf_insn_idx,
+				meta->linear_idx, target->linear_idx);
+
+		if (knod_bpf_is_direct_exit_target(knod_prog, target)) {
+			meta->branch_type = KNOD_BR_DIRECT_EXIT;
+			meta->merge_point = target;
+			continue;
+		}
+
+		meta->branch_type = is_mbpf_cond_jump(meta) ?
+			KNOD_BR_FORWARD_SKIP : KNOD_BR_FORWARD_GOTO;
+		meta->merge_point = target;
+		target->is_merge_point = true;
+	}
+
+	return 0;
+}
+
+/*
+ * Build the basic-block CFG, compute RPO, reorder the instruction list into
+ * RPO and insert synthetic jumps.  Returns 0, or a negative errno (a loop
+ * yields -EOPNOTSUPP).
+ */
+static int knod_bpf_build_cfg(struct knod_prog *knod_prog)
+{
+	struct knod_insn_meta *meta;
+	int n_insns = 0, n_bbs, n_back = 0, ret;
+	struct knod_bb *bbs;
+
+	list_for_each_entry(meta, &knod_prog->insns, l)
+		n_insns++;
+	if (!n_insns)
+		return 0;
+
+	bbs = kcalloc(n_insns, sizeof(*bbs), GFP_KERNEL);
+	if (!bbs)
+		return -ENOMEM;
+
+	n_bbs = knod_bpf_build_bbs(knod_prog, bbs);
+	if (n_bbs < 0) {
+		ret = n_bbs;
+		goto out_free;
+	}
+
+	ret = knod_bpf_build_edges(knod_prog, bbs, n_bbs);
+	if (ret)
+		goto out_free;
+
+	ret = knod_bpf_compute_rpo(bbs, n_bbs, &bbs[0], &n_back);
+	if (ret)
+		goto out_free;
+
+	ret = knod_bpf_compute_dom(bbs, n_bbs, &bbs[0]);
+	if (ret)
+		goto out_free;
+
+	if (n_back) {
+		ret = knod_bpf_detect_loops(bbs, n_bbs);
+		if (ret)
+			goto out_free;
+	}
+
+	/* Hand the block array to the prog for the /bpf/cfg view (freed at
+	 * teardown); kept even if the reorder below rejects a loop, so the
+	 * rejection can be inspected.
+	 */
+	kfree(knod_prog->bbs);
+	knod_prog->bbs = bbs;
+	knod_prog->n_bbs = n_bbs;
+	knod_prog->n_back = n_back;
+
+	return knod_bpf_reorder_rpo(knod_prog, bbs, n_bbs, n_back);
+
+out_free:
+	kfree(bbs);
+	return ret;
+}
+
+/*
+ * Assign exec_save SGPR pairs to the forward branches, recycling a pair once
+ * its merge point has been passed.  The peak concurrent live count is the
+ * actual SGPR requirement - usually far less than the total branch count.
+ */
+static int knod_bpf_alloc_exec_sregs(struct knod_bpf_priv *priv,
+				     struct knod_prog *knod_prog)
+{
+	struct {
+		u8 sreg;
+		struct knod_insn_meta *merge;
+	} live[72];
+	int exec_save_max, max_pairs, n_live, peak, j;
+	struct knod_insn_meta *meta;
+	u8 free_stack[72];
+	int free_top;
+
+	exec_save_max = (priv->isa_version == 10) ?
+		KNOD_AMDGPU_EXEC_SAVE_SREG_MAX_GFX10 :
+		KNOD_AMDGPU_EXEC_SAVE_SREG_MAX_GFX9;
+	max_pairs = (exec_save_max - knod_prog->exec_save_base + 1) / 2;
+
+	for (free_top = 0; free_top < max_pairs; free_top++)
+		free_stack[free_top] = knod_prog->exec_save_base +
+			(max_pairs - 1 - free_top) * 2;
+
+	n_live = 0;
+	peak = 0;
+
+	list_for_each_entry(meta, &knod_prog->insns, l) {
+		/* Reclaim pairs from scopes that merge at this insn */
+		for (j = n_live - 1; j >= 0; j--) {
+			if (live[j].merge == meta) {
+				free_stack[free_top++] = live[j].sreg;
+				live[j] = live[--n_live];
+			}
+		}
+
+		if (meta->branch_type != KNOD_BR_FORWARD_SKIP &&
+		    meta->branch_type != KNOD_BR_FORWARD_GOTO)
+			continue;
+
+		if (free_top == 0) {
+			pr_err("knod_cfg: exec_save exhausted, peak %d concurrent scopes (max %d)\n",
+			       peak, max_pairs);
+			return -ENOSPC;
+		}
+
+		meta->exec_save_sreg = free_stack[--free_top];
+		live[n_live].sreg = meta->exec_save_sreg;
+		live[n_live].merge = meta->merge_point;
+		n_live++;
+
+		if (n_live > peak)
+			peak = n_live;
+	}
+
+	knod_prog->exec_save_pairs_used = peak;
+	pr_debug("knod_cfg: done, peak %d concurrent scopes (total fwd jumps: %d+%d)\n",
+		 peak, peak, n_live);
+	return 0;
+}
+
+/*
+ * knod_bpf_analyze_cfg - Classify branches and allocate SGPRs for
+ * structurized CFG.
+ *
+ * Runs before instruction emission. For each conditional branch:
+ *   - Backward jump or jump to EXIT -> DIRECT_EXIT (no SGPR needed)
+ *   - Forward jump to non-EXIT -> FORWARD_SKIP, allocate SGPR pair
+ *
+ * The "save jumping lanes" pattern handles crossing scopes correctly:
+ *   branch: s_and_b64 s[n], exec, vcc; s_andn2_b64 exec, exec, vcc
+ *   merge:  s_or_b64 exec, exec, s[n]
+ *
+ * For JNE (jump_neg_op): VCC=0 -> jump, so lanes are swapped.
+ */
+static int knod_bpf_analyze_cfg(struct knod_bpf_priv *priv,
+				struct knod_prog *knod_prog)
+{
+	int ret;
+
+	/* Build the basic-block CFG, reorder the instruction list into RPO so
+	 * every branch is forward (inserting synthetic jumps where a not-taken
+	 * successor would no longer be adjacent), then classify each branch by
+	 * linear position.  A loop in the program is rejected (-EOPNOTSUPP).
+	 */
+	ret = knod_bpf_build_cfg(knod_prog);
+	if (ret)
+		return ret;
+	ret = knod_bpf_classify_linear(knod_prog);
+	if (ret)
+		return ret;
+
+	return knod_bpf_alloc_exec_sregs(priv, knod_prog);
+}
+
+/*
+ * Shader stores packet address and length into pass_meta_buf slot header.
+ * Host-side SDMA engine does the actual copy to the delivery page.
+ *
+ * At entry:
+ *   TMP_VREG10_LO (v42) = old_val * 2 (from pass_indices addressing)
+ *   DATA_VREG (v64:v65) = packet source VRAM address
+ *   DATA_END_VREG (v66:v67) = packet end address
+ *   PARAM_SREG (s28:s29) = param GTT address
+ *
+ * Stores at slot header:
+ *   +0: u32 len (DATA_END_LO - DATA_LO)
+ *   +8: u64 src_addr (DATA_VREG)
+ */
+static void knod_emit_pass_addr_store(struct knod_bpf_priv *priv,
+				      struct knod_insn_meta *meta)
+{
+	struct amdgcn_param32 p[3];
+
+	/* s_lshl_b32 s18, s15, 3 - queue_idx * 8 for pass_meta_buf_gaddr
+	 * stride
+	 */
+	knod_sset32(&p[0], KNOD_AMDGPU_TMP_SREG1_LO);
+	knod_sset32(&p[1], KNOD_AMDGPU_WORKGROUP_ID_Y_SREG);
+	knod_iset32(&p[2], 3);
+	knod_emit(priv, meta, s_lshl_b32, p[0], p[1], p[2]);
+
+	/* s_load_dwordx2 s[16:17], s[28:29], offsetof(pass_meta_buf_gaddr)
+	 * soffset=s18
+	 */
+	knod_sset32(&p[0], KNOD_AMDGPU_TMP_SREG0_LO);
+	knod_sset32(&p[1], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_emit(priv, meta, s_load_dwordx2_soff, p[0], p[1],
+		  offsetof(struct knod_bpf_param, pass_meta_buf_gaddr),
+		  KNOD_AMDGPU_TMP_SREG1_LO);
+
+	/* s_waitcnt lgkmcnt(0) */
+	knod_emit(priv, meta, s_waitcnt_lgkmcnt);
+
+	/* Compute slot offset: old_val << 12 = (old_val*2) << 11
+	 * v_lshlrev_b32 v44, 11, v42
+	 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG11_LO);
+	knod_iset32(&p[1], 11);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_emit(priv, meta, v_lshlrev_b32, p[0], p[1], p[2]);
+
+	/* v_mov_b32 v45, 0 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG11_HI);
+	knod_iset32(&p[1], 0);
+	knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+	/* v_add_co_u32 v44, s16, v44 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG11_LO);
+	knod_sset32(&p[1], KNOD_AMDGPU_TMP_SREG0_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG11_LO);
+	knod_emit(priv, meta, v_add_co_u32, p[0], p[1], p[2]);
+
+	/*
+	 * slot_hi = base_hi (NOT base_hi + carry).  The slot offset is at
+	 * most (pass_pkts_per_queue-1)*KNOD_PASS_SLOT_SIZE and the whole
+	 * pass_meta_buf is a single contiguous allocation that never straddles
+	 * a 4GiB boundary, so base_lo + offset never wraps and the carry is
+	 * always 0.  Avoid the v_add_co/v_addc carry chain entirely: on GFX9
+	 * the v_addc here was picking up a stale VCC (from the preceding
+	 * XDP_PASS v_cmp) instead of the v_add_co carry-out, setting slot_hi=1
+	 * and faulting at 0x1_xxxx.
+	 * v_mov_b32 v45, s17
+	 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG11_HI);
+	knod_sset32(&p[1], KNOD_AMDGPU_TMP_SREG0_HI);
+	knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+	/* v44:v45 = slot_addr in pass_meta_buf */
+
+	/* Store len: v_sub_u32 v0, DATA_END_LO, DATA_LO */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG0_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_DATA_END_VREG_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_emit(priv, meta, v_sub_u32, p[0], p[1], p[2]);
+
+	/* global_store_dword [slot+0], len */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG0_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG11_LO);
+	knod_emit(priv, meta, global_store_dword, p[0], p[1],
+		  offsetof(struct knod_pass_slot_hdr, len));
+
+	/* global_store_dwordx2 [slot+8], DATA_VREG (src_addr) */
+	knod_vset32(&p[0], KNOD_AMDGPU_DATA_VREG_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG11_LO);
+	knod_emit(priv, meta, global_store_dwordx2, p[0], p[1],
+		  offsetof(struct knod_pass_slot_hdr, src_addr));
+}
+
+static int knod_bpf_jit(struct knod_dev *knodev,
+			struct knod_prog *knod_prog)
+{
+	struct knod_bpf_priv *priv =
+		(struct knod_bpf_priv *)knodev->accel->xdp.priv;
+	short off, packet_off, stack_off;
+	struct knod_insn_meta *meta, *meta2;
+	struct amdgcn_param64 param64[2];
+	u32 insn_idx = 0, i;
+	struct amdgcn_param32 param[3];
+	struct amdgcn_param32 p32[2];
+	struct amdgcn_param32 p[3];
+	int s, d, imm, imm2;
+	int pass_branch_idx;
+	bool is_dw, fetch;
+	bool skip = false;
+	int pass_dwords;
+	int atomic_op;
+	int j;
+	int map_id;
+	u64 imm64;
+	int ret;
+
+	/* Analyze CFG before instruction emission */
+	ret = knod_bpf_analyze_cfg(priv, knod_prog);
+
+	if (ret)
+		return ret;
+
+	knod_bpf_layout_sregs(priv, knod_prog);
+	ret = knod_prog_prepare_insns(priv, knod_prog);
+	if (ret)
+		return ret;
+
+	knod_prog->max_stack_off = -knod_prog->max_stack_off;
+	knod_prog->max_stack_off = ALIGN(knod_prog->max_stack_off, 4);
+	knod_prog->max_packet_off = ALIGN(knod_prog->max_packet_off, 4);
+	/* NOTE:
+	 * packet is accessed with packet_off + size
+	 * largest size of it is unsigned long
+	 */
+	knod_prog->max_packet_off += sizeof(unsigned long);
+	if (knod_prog->max_packet_off > MAX_PACKET_CACHE) {
+		WARN_ON_ONCE(1);
+		knod_bpf_pkt_cache = 0;
+	}
+
+	/* Initialize all exec_save SGPRs to 0.
+	 * Without this, merge points that restore from exec_save SGPRs
+	 * of branches that were skipped (by an outer s_cbranch_execz)
+	 * would OR garbage into EXEC, enabling invalid lanes.
+	 * In the old code, BPF_EXIT used s_endpgm so execution never
+	 * reached those merge points; now it does.
+	 */
+	if (knod_prog->exec_save_pairs_used > 0) {
+		u8 sreg;
+
+		meta = knod_prog_pre_last_meta(knod_prog);
+
+		for (sreg = knod_prog->exec_save_base;
+		     sreg < knod_prog->exec_save_base +
+			    knod_prog->exec_save_pairs_used * 2;
+		     sreg += 2)
+			knod_emit(priv, meta, s_mov_b64, sreg,
+				  AMDGCN_SREG_INTEGER_0);
+	}
+
+	if (knod_bpf_pkt_cache) {
+		meta = knod_prog_pre_last_meta(knod_prog);
+
+		/* ctx->data is in DATA_VREG -> copy to r32[0] via v_mov */
+		knod_vset32(&param[0], r32[0].v);
+		knod_vset32(&param[1], KNOD_AMDGPU_DATA_VREG_LO);
+		knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+		knod_vset32(&param[0], r32[0].v + 1);
+		knod_vset32(&param[1], KNOD_AMDGPU_DATA_VREG_HI);
+		knod_emit(priv, meta, v_mov_b32_e32, param[0], param[1]);
+
+		knod_global_load_size_cache(priv, meta,
+					    &pkt_cache[0],
+					    r32[0],
+					    0, /* dst index */
+					    0, /* start offset */
+					    knod_prog->max_packet_off);
+	}
+
+	insn_idx = 0;
+	list_for_each_entry(meta, &knod_prog->pre_insns, l) {
+		for (i = 0; i < meta->amdgpu_insns; i++)
+			insn_idx += (meta->amdgpu_insn[i].size / 4);
+	}
+
+	list_for_each_entry(meta, &knod_prog->insns, l) {
+		if (skip) {
+			skip = false;
+			meta->amdgpu_insn_idx = AMDGPU_INSN_SKIP;
+			continue;
+		}
+		s = meta->insn.src_reg;
+		d = meta->insn.dst_reg;
+		imm = meta->insn.imm;
+		off = meta->insn.off;
+
+		meta->amdgpu_insn_idx = insn_idx;
+		meta->amdgpu_insns = 0;
+
+		/* Structurized CFG: restore EXEC at merge points */
+		if (meta->is_merge_point) {
+			struct knod_insn_meta *br;
+
+			list_for_each_entry(br, &knod_prog->insns, l) {
+				if ((br->branch_type == KNOD_BR_FORWARD_SKIP ||
+				     br->branch_type == KNOD_BR_FORWARD_GOTO) &&
+				    br->merge_point == meta) {
+					knod_emit(priv, meta, s_or_b64,
+						  AMDGCN_SREG_EXEC_LO,
+						  AMDGCN_SREG_EXEC_LO,
+						  br->exec_save_sreg);
+				}
+			}
+			/* Remove done lanes from restored EXEC */
+			knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_EXEC_LO,
+				  knod_prog->done_mask_sreg);
+				}
+
+		switch (meta->insn.code) {
+		/* ALU
+		 * If a destination register contains a pointer of STACK,
+		 * offset should not be minus.
+		 */
+		case BPF_ALU | BPF_MOV | BPF_X:
+		case BPF_ALU64 | BPF_MOV | BPF_X:
+			//r[d] = r[s];
+			knod_mov64(priv, meta, bpf_reg64[d], bpf_reg64[s]);
+			break;
+		case BPF_ALU | BPF_MOV | BPF_K:
+		case BPF_ALU64 | BPF_MOV | BPF_K:
+			//r[d] = imm;
+			knod_iset64(&p64[0], imm);
+			knod_mov64(priv, meta, bpf_reg64[d], p64[0]);
+			break;
+		case BPF_ALU | BPF_XOR | BPF_X:
+			knod_xor32(priv, meta,
+				       bpf_reg64[d].lo, bpf_reg64[d].lo,
+				       bpf_reg64[s].lo);
+			knod_iset64(&p64[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p64[0].lo);
+			break;
+		case BPF_ALU64 | BPF_XOR | BPF_X:
+			//r[d] ^= r[s];
+			knod_xor32(priv, meta,
+				       bpf_reg64[d].lo, bpf_reg64[d].lo,
+				       bpf_reg64[s].lo);
+			knod_xor32(priv, meta,
+				       bpf_reg64[d].hi, bpf_reg64[d].hi,
+				       bpf_reg64[s].hi);
+			break;
+		case BPF_ALU | BPF_XOR | BPF_K:
+		case BPF_ALU64 | BPF_XOR | BPF_K:
+			knod_iset64(&p64[0], imm);
+			knod_mov64(priv, meta, bpf_reg64[d], p64[0]);
+			knod_xor32(priv, meta, bpf_reg64[d].lo,
+				       bpf_reg64[d].lo, r64[0].lo);
+			break;
+			//r[d] ^= imm;
+			break;
+		case BPF_ALU | BPF_MOD | BPF_X:
+		case BPF_ALU64 | BPF_MOD | BPF_X:
+			//r[d] %= r[s];
+			knod_iset64(&p64[0], meta->umin_src);
+			knod_mod(priv, meta, bpf_reg64[d], p64[0],
+				     r64[0], r64[1], r64[2], r64[3], r64[4]);
+			break;
+		case BPF_ALU | BPF_MOD | BPF_K:
+		case BPF_ALU64 | BPF_MOD | BPF_K:
+			//r[d] %= imm;
+			/* The dividend fits 32 bits (verifier rejects wider
+			 * div/mod), so the 32-bit fold is valid even when
+			 * clang emitted this as a 64-bit ALU op (e.g. u32
+			 * hash % 65537 -> `r2 %= 65537`).
+			 */
+			if (meta->umax_dst <= U32_MAX && imm &&
+			    knod_mod_k32(priv, meta, bpf_reg64[d], imm))
+				break;
+			knod_iset64(&p64[0], imm);
+			knod_mod(priv, meta, bpf_reg64[d], p64[0],
+				     r64[0], r64[1], r64[2], r64[3], r64[4]);
+			break;
+		case BPF_ALU | BPF_AND | BPF_X:
+			knod_and32(priv, meta, bpf_reg64[d].lo,
+				       bpf_reg64[d].lo, bpf_reg64[s].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_AND | BPF_X:
+			//r[d] &= r[s];
+			knod_and64(priv, meta, bpf_reg64[d],
+				       bpf_reg64[d], bpf_reg64[s]);
+			break;
+		case BPF_ALU | BPF_AND | BPF_K:
+		case BPF_ALU64 | BPF_AND | BPF_K:
+			//r[d] &= imm;
+			knod_iset32(&p32[0], imm);
+			knod_and32(priv, meta, bpf_reg64[d].lo, p32[0],
+				       bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU | BPF_OR | BPF_X:
+			knod_or32(priv, meta, bpf_reg64[d].lo,
+				  bpf_reg64[d].lo, bpf_reg64[s].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_OR | BPF_X:
+			//r[d] |= r[s];
+			knod_or32(priv, meta, bpf_reg64[d].lo,
+				  bpf_reg64[d].lo, bpf_reg64[s].lo);
+			knod_or32(priv, meta, bpf_reg64[d].hi,
+				  bpf_reg64[d].hi, bpf_reg64[s].hi);
+			break;
+		case BPF_ALU | BPF_OR | BPF_K:
+		case BPF_ALU64 | BPF_OR | BPF_K:
+			//r[d] |= imm;
+			knod_iset32(&p32[0], imm);
+			knod_or32(priv, meta,
+				bpf_reg64[d].lo, p32[0], bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU | BPF_ADD | BPF_X:
+			knod_add32(priv, meta, bpf_reg64[d].lo,
+				       bpf_reg64[d].lo, bpf_reg64[s].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_ADD | BPF_X:
+			knod_add64(priv, meta, bpf_reg64[d],
+				       bpf_reg64[d],
+				       bpf_reg64[s]);
+
+			//r[d] += r[s];
+			break;
+		case BPF_ALU | BPF_ADD | BPF_K:
+		case BPF_ALU64 | BPF_ADD | BPF_K:
+			//r[d] += imm;
+			knod_iset32(&p32[0], imm);
+			knod_add32(priv, meta, bpf_reg64[d].lo,
+				       p32[0], bpf_reg64[d].lo);
+			/* NOTE:
+			 * imm is 24bit.
+			 * But should we set hi to 0?
+			 */
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU | BPF_SUB | BPF_X:
+			//r[d] -= r[s];
+			knod_sub32(priv, meta, bpf_reg64[d].lo,
+				       bpf_reg64[d].lo, bpf_reg64[s].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_SUB | BPF_X:
+			//r[d] -= r[s];
+
+			knod_sub64(priv, meta, bpf_reg64[d], bpf_reg64[d],
+				       bpf_reg64[s]);
+			break;
+		case BPF_ALU | BPF_SUB | BPF_K:
+		case BPF_ALU64 | BPF_SUB | BPF_K:
+			//r[d] -= imm;
+			knod_iset64(&p64[0], imm);
+			knod_subrev64(priv, meta, bpf_reg64[d], p64[0],
+					  bpf_reg64[s]);
+			break;
+		case BPF_ALU | BPF_MUL | BPF_X:
+			knod_mul_lo32(priv, meta, bpf_reg64[d].lo,
+					  bpf_reg64[d].lo, bpf_reg64[s].lo);
+			break;
+		case BPF_ALU64 | BPF_MUL | BPF_X:
+			//r[d] *= r[s];
+			knod_mov64(priv, meta, r64[0], bpf_reg64[d]);
+			knod_mov64(priv, meta, r64[1], bpf_reg64[s]);
+			knod_mul64(priv, meta,
+				       bpf_reg64[d],
+				       r64[0],
+				       r64[1],
+				       r64[2]);
+			break;
+		case BPF_ALU | BPF_MUL | BPF_K:
+			knod_iset32(&p32[0], imm);
+			knod_mul_lo32(priv, meta, bpf_reg64[d].lo,
+					  p32[0], bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_MUL | BPF_K:
+			//r[d] *= imm;
+			knod_iset64(&p64[0], imm);
+			knod_mov64(priv, meta, r64[0], bpf_reg64[d]);
+			knod_mov64(priv, meta, r64[1], p64[0]);
+			knod_mul64(priv, meta,
+				       bpf_reg64[d],
+				       r64[0],
+				       r64[1],
+				       r64[2]);
+			break;
+		case BPF_ALU | BPF_DIV | BPF_X:
+		case BPF_ALU64 | BPF_DIV | BPF_X:
+			//r[d] /= r[s];
+			knod_iset64(&p64[0], meta->umin_src);
+			knod_div(priv, meta, bpf_reg64[d], p64[0],
+				     r64[0], r64[1], r64[2], r64[3]);
+			break;
+		case BPF_ALU | BPF_DIV | BPF_K:
+		case BPF_ALU64 | BPF_DIV | BPF_K:
+			//r[d] /= imm;
+			knod_iset64(&p64[0], imm);
+			knod_div(priv, meta, bpf_reg64[d], p64[0],
+				     r64[0], r64[1], r64[2], r64[3]);
+			break;
+		case BPF_ALU | BPF_NEG:
+			knod_iset32(&p32[0], 0);
+			knod_sub32(priv, meta, bpf_reg64[d].lo, p32[0],
+				       bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_NEG:
+			//r[d] = -r[d];
+			WARN_ON_ONCE(1);
+			break;
+		case BPF_ALU | BPF_LSH | BPF_X:
+			knod_lshlrev32(priv, meta, bpf_reg64[d].lo,
+					   bpf_reg64[s].lo, bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_LSH | BPF_X:
+			//r[d] <<= r[s];
+			knod_lshlrev64(priv, meta, bpf_reg64[d],
+					   bpf_reg64[s], bpf_reg64[d]);
+			break;
+		case BPF_ALU | BPF_LSH | BPF_K:
+			knod_iset32(&p32[0], imm);
+			knod_lshlrev32(priv, meta, bpf_reg64[d].lo, p32[0],
+					   bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_LSH | BPF_K:
+			//r[d] <<= imm;
+			knod_iset64(&p64[0], imm);
+			knod_lshlrev64(priv, meta, bpf_reg64[d], p64[0],
+					   bpf_reg64[d]);
+			break;
+		case BPF_ALU | BPF_RSH | BPF_X:
+			knod_lshrrev32(priv, meta, bpf_reg64[d].lo,
+				       bpf_reg64[s].lo,
+				       bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_RSH | BPF_X:
+			//r[d] >>= r[s];
+			knod_lshrrev64(priv, meta, bpf_reg64[d],
+					   bpf_reg64[s], bpf_reg64[d]);
+			break;
+		case BPF_ALU | BPF_RSH | BPF_K:
+			knod_iset32(&p32[0], imm);
+			knod_lshrrev32(priv, meta, bpf_reg64[d].lo, p32[0],
+					   bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_RSH | BPF_K:
+			//r[d] >>= imm;
+			knod_iset64(&p64[0], imm);
+			knod_lshrrev64(priv, meta, bpf_reg64[d],
+					   p64[0], bpf_reg64[d]);
+			break;
+		case BPF_ALU | BPF_ARSH | BPF_X:
+			knod_ashrrev32(priv, meta, bpf_reg64[d].lo,
+					   bpf_reg64[s].lo, bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_ARSH | BPF_X:
+			//r[d] >>= r[s];
+			knod_ashrrev64(priv, meta, bpf_reg64[d],
+					   bpf_reg64[s], bpf_reg64[d]);
+			break;
+		case BPF_ALU | BPF_ARSH | BPF_K:
+			knod_iset32(&p32[0], imm);
+			knod_ashrrev32(priv, meta, bpf_reg64[d].lo,
+					   p32[0], bpf_reg64[d].lo);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			break;
+		case BPF_ALU64 | BPF_ARSH | BPF_K:
+			//r[d] >>= imm;
+			knod_iset64(&p64[0], imm);
+			knod_ashrrev64(priv, meta, bpf_reg64[d],
+					   p64[0], bpf_reg64[d]);
+			break;
+		case BPF_LD | BPF_IMM | BPF_DW:
+			meta2 = list_next_entry(meta, l);
+			if (WARN_ON_ONCE(!meta2))
+				return -EINVAL;
+			imm2 = meta2->insn.imm;
+			skip = true;
+			imm64 = (u64)imm2 << 32 | (u32)imm;
+			switch (s) {
+			case 0x00:
+				//r[d] = imm64;
+				knod_mov64_imm(priv, meta, d * 2,
+						   imm64);
+
+				break;
+			case 0x01:
+				/* r[d] = param->maps[imm]; */
+				imm64 = knod_bpf_get_map_gaddr(priv,
+							       meta,
+							       meta2);
+				map_id = knod_bpf_get_map_id(priv,
+							     meta,
+							     meta2);
+				knod_mov64_imm(priv, meta, d * 2,
+						   imm64);
+				break;
+			default:
+				WARN_ON_ONCE(1);
+				break;
+			}
+			break;
+			/* Legacy BPF packet access, not needed */
+		case BPF_LD | BPF_ABS | BPF_B:
+		case BPF_LD | BPF_ABS | BPF_H:
+		case BPF_LD | BPF_ABS | BPF_W:
+		case BPF_LD | BPF_IND | BPF_B:
+		case BPF_LD | BPF_IND | BPF_H:
+		case BPF_LD | BPF_IND | BPF_W:
+			//err = pc | 0x0700;
+			//exit = true;
+			WARN_ON_ONCE(1);
+			break;
+		case BPF_LDX | BPF_MEM | BPF_B:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->sreg.stack_off + off;
+				knod_bpf_load_size(priv, meta,
+						       &bpf_reg64[d],
+						       &stack[0],
+						       sizeof(unsigned char),
+						       512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_load_ubyte,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_load_ubyte,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_load_ubyte,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->sreg.packet_off +
+						off;
+					knod_bpf_load_size(priv, meta,
+							       &bpf_reg64[d],
+							       &pkt_cache[0],
+							sizeof(unsigned char),
+							       packet_off);
+				} else {
+					knod_emit(priv, meta, global_load_ubyte,
+						  bpf_reg64[d].lo,
+						  bpf_reg64[s].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			knod_wait_vmcnt(priv, meta);
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			//ptr = (__global void *)r[s] + off;
+			//r[d] = *(__global unsigned char *)ptr;
+			break;
+		case BPF_LDX | BPF_MEM | BPF_H:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->sreg.stack_off + off;
+				knod_bpf_load_size(priv, meta,
+						       &bpf_reg64[d],
+						       &stack[0],
+						       sizeof(unsigned short),
+						       512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_load_ushort,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_load_ushort,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_load_ushort,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == SCALAR_VALUE) {
+				knod_emit(priv, meta, global_load_ushort,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->sreg.packet_off +
+						off;
+					knod_bpf_load_size(priv, meta,
+							       &bpf_reg64[d],
+							       &pkt_cache[0],
+							sizeof(unsigned short),
+							       packet_off);
+				} else if (priv->isa_version == 10 &&
+					   (off & 1)) {
+					knod_bpf_emit_gfx10_unaligned_load(
+						priv, meta,
+						sizeof(unsigned short),
+						bpf_reg64[d],
+						bpf_reg64[s].lo, off);
+				} else {
+					knod_emit(priv, meta,
+						  global_load_ushort,
+						  bpf_reg64[d].lo,
+						  bpf_reg64[s].lo, off);
+				}
+			} else {
+				knod_jit_err(" type = %d\n", meta->ptr.type);
+				WARN_ON_ONCE(1);
+			}
+			//ptr = (__global void *)r[s] + off;
+			//r[d] = *(__global unsigned short *)ptr;
+			knod_iset32(&p32[0], 0);
+			knod_mov32(priv, meta, bpf_reg64[d].hi, p32[0]);
+			knod_wait_vmcnt(priv, meta);
+			break;
+		case BPF_LDX | BPF_MEM | BPF_W:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->sreg.stack_off + off;
+				knod_bpf_load_size(priv, meta,
+						       &bpf_reg64[d],
+						       &stack[0],
+						       sizeof(unsigned int),
+						       512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				if (off == offsetof(struct xdp_md, data)) {
+					knod_mov32(priv, meta,
+						bpf_reg64[d].lo,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_VREG_LO,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+					knod_mov32(priv, meta,
+						bpf_reg64[d].hi,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_VREG_HI,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+				} else if (off == offsetof(struct xdp_md,
+							   data_end)) {
+					knod_mov32(priv, meta,
+						bpf_reg64[d].lo,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_END_VREG_LO,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+					knod_mov32(priv, meta,
+						bpf_reg64[d].hi,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_END_VREG_HI,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+				} else {
+					emit_global_load_dwordx2(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						bpf_reg64[d].lo,
+						bpf_reg64[s].lo,
+						off * 2);
+					debug_insn(priv->isa_version,
+						   &meta->amdgpu_insn[meta->amdgpu_insns]);
+					meta->amdgpu_insns++;
+				}
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_load_dword,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_load_dword,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->sreg.packet_off +
+						off;
+					knod_bpf_load_size(priv, meta,
+							       &bpf_reg64[d],
+							       &pkt_cache[0],
+							sizeof(unsigned int),
+							       packet_off);
+				} else if (priv->isa_version == 10 &&
+					   (off & 3)) {
+					knod_bpf_emit_gfx10_unaligned_load(
+						priv, meta,
+						sizeof(unsigned int),
+						bpf_reg64[d],
+						bpf_reg64[s].lo, off);
+				} else {
+					knod_emit(priv, meta, global_load_dword,
+						  bpf_reg64[d].lo,
+						  bpf_reg64[s].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			//ptr = (__global void *)r[s] + off;
+			//r[d] = *(__global unsigned int *)ptr;
+			if (meta->ptr.type != PTR_TO_CTX) {
+				knod_iset32(&p32[0], 0);
+				knod_mov32(priv, meta, bpf_reg64[d].hi,
+					       p32[0]);
+			}
+			knod_wait_vmcnt(priv, meta);
+			break;
+		case BPF_LDX | BPF_MEM | BPF_DW:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->sreg.stack_off + off;
+				knod_bpf_load_size(priv, meta,
+						       &bpf_reg64[d],
+						       &stack[0],
+						       sizeof(unsigned long),
+						       512+stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				if (off == offsetof(struct xdp_md, data)) {
+					knod_mov32(priv, meta,
+						bpf_reg64[d].lo,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_VREG_LO,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+					knod_mov32(priv, meta,
+						bpf_reg64[d].hi,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_VREG_HI,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+				} else if (off == offsetof(struct xdp_md,
+							   data_end)) {
+					knod_mov32(priv, meta,
+						bpf_reg64[d].lo,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_END_VREG_LO,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+					knod_mov32(priv, meta,
+						bpf_reg64[d].hi,
+					    (struct amdgcn_param32){
+					    .v = KNOD_AMDGPU_DATA_END_VREG_HI,
+					    .type = AMDGCN_PARAM_TYPE_VGPR});
+				} else {
+					knod_emit(priv, meta,
+						  global_load_dwordx2,
+						  bpf_reg64[d].lo,
+						  bpf_reg64[s].lo, off * 2);
+				}
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_load_dwordx2,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_load_dwordx2,
+					  bpf_reg64[d].lo,
+					  bpf_reg64[s].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->sreg.packet_off +
+						off;
+					knod_bpf_load_size(priv, meta,
+							       &bpf_reg64[d],
+							       &pkt_cache[0],
+							sizeof(unsigned long),
+							       packet_off);
+				} else if (priv->isa_version == 10 &&
+					   (off & 3)) {
+					knod_bpf_emit_gfx10_unaligned_load(
+						priv, meta,
+						sizeof(unsigned long),
+						bpf_reg64[d],
+						bpf_reg64[s].lo, off);
+				} else {
+					knod_emit(priv, meta,
+						  global_load_dwordx2,
+						  bpf_reg64[d].lo,
+						  bpf_reg64[s].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			//ptr = (__global void *)r[s] + off;
+			//r[d] = *(__global unsigned long *)ptr;
+			knod_wait_vmcnt(priv, meta);
+			break;
+		case BPF_STX | BPF_MEM | BPF_B:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_bpf_store_cache_size(priv, meta,
+						&bpf_reg64[s],
+						&stack[0],
+						sizeof(u8),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_byte,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_byte,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_byte,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv,
+							meta,
+							&bpf_reg64[s],
+							&pkt_cache[0],
+							sizeof(u8),
+							packet_off);
+				} else {
+					knod_emit(priv, meta, global_store_byte,
+						  bpf_reg64[s].lo,
+						  bpf_reg64[d].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_STX | BPF_MEM | BPF_H:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_bpf_store_cache_size(priv, meta,
+						&bpf_reg64[s],
+						&stack[0],
+						sizeof(u16),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_short,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_short,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_short,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv,
+							meta,
+							&bpf_reg64[s],
+							&pkt_cache[0],
+							sizeof(u16),
+							packet_off);
+				} else {
+					knod_emit(priv, meta,
+						  global_store_short,
+						  bpf_reg64[s].lo,
+						  bpf_reg64[d].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_STX | BPF_MEM | BPF_W:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_bpf_store_cache_size(priv, meta,
+						&bpf_reg64[s],
+						&stack[0],
+						sizeof(u32),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_dword,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_dword,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_dword,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv,
+							meta,
+							&bpf_reg64[s],
+							&pkt_cache[0],
+							sizeof(u32),
+							packet_off);
+				} else {
+					knod_emit(priv, meta,
+						  global_store_dword,
+						  bpf_reg64[s].lo,
+						  bpf_reg64[d].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_STX | BPF_MEM | BPF_DW:
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_bpf_store_cache_size(priv, meta,
+						&bpf_reg64[s],
+						&stack[0],
+						sizeof(u64),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_dwordx2,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_dwordx2,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_dwordx2,
+					  bpf_reg64[s].lo,
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv, meta,
+							&bpf_reg64[s],
+							&pkt_cache[0],
+							sizeof(u64),
+							packet_off);
+				} else {
+					knod_emit(priv, meta,
+						  global_store_dwordx2,
+						  bpf_reg64[s].lo,
+						  bpf_reg64[d].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_STX | BPF_ATOMIC | BPF_W:
+		case BPF_STX | BPF_ATOMIC | BPF_DW:
+			is_dw = BPF_SIZE(meta->insn.code) == BPF_DW;
+			atomic_op = imm & ~BPF_FETCH;
+			fetch = imm & BPF_FETCH;
+
+			/*
+			 * BPF atomic: *(dst_reg + off) op= src_reg
+			 * If BPF_FETCH: src_reg = old value
+			 * BPF_CMPXCHG: expect in r0, new in src_reg,
+			 *   old value returned in r0.
+			 *
+			 * global_atomic_* with glc=1 returns old value in vdst.
+			 * For non-FETCH ops use glc=0 (fire-and-forget).
+			 *
+			 * 64-bit atomics (global_atomic_*_x2) hang on GFX9
+			 * VRAM. GFX10+ supports them.
+			 */
+			if (is_dw && priv->isa_version == 9) {
+				pr_err("knod: 64-bit atomic not supported on GFX9\n");
+				return -EOPNOTSUPP;
+			}
+
+			/*
+			 * For CMPXCHG/FETCH: drain pending loads so addr/data
+			 * VGPRs are ready. For non-fetch ADD wave reduction,
+			 * addr was already waited for at map_lookup, and data
+			 * is from ALU - no waitcnt needed.
+			 */
+			if (imm == BPF_CMPXCHG || fetch)
+				knod_wait_vmcnt(priv, meta);
+
+			if (imm == BPF_CMPXCHG) {
+				/* cmpswap: data = {expect(r0), new(src)}.
+				 * AMD cmpswap data reg pair must be
+				 * consecutive:
+				 *   32-bit: {cmp, new} = 2 consecutive VGPRs
+				 *   64-bit: {cmp_lo, cmp_hi, new_lo, new_hi}
+				 * Copy r0 and src into TMP consecutive pair.
+				 */
+				struct amdgcn_param32 tmp0_lo, tmp0_hi,
+						      tmp1_lo, tmp1_hi;
+
+				knod_vset32(&tmp0_lo,
+					KNOD_AMDGPU_TMP_VREG0_LO);
+				knod_vset32(&tmp0_hi,
+					KNOD_AMDGPU_TMP_VREG0_HI);
+				knod_vset32(&tmp1_lo,
+					KNOD_AMDGPU_TMP_VREG1_LO);
+				knod_vset32(&tmp1_hi,
+					KNOD_AMDGPU_TMP_VREG1_HI);
+
+				if (!is_dw) {
+					/* TMP0_LO = r0 (expect),
+					 * TMP0_HI = src (new)
+					 */
+					knod_mov32(priv, meta,
+						       tmp0_lo,
+						       bpf_reg64[0].lo);
+					knod_mov32(priv, meta,
+						       tmp0_hi,
+						       bpf_reg64[s].lo);
+
+					emit_global_atomic_cmpswap(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						tmp0_lo, bpf_reg64[d].lo,
+						tmp0_lo, off, 1);
+					debug_insn(priv->isa_version,
+						   &meta->amdgpu_insn[meta->amdgpu_insns]);
+					meta->amdgpu_insns++;
+					knod_wait_vmcnt(priv, meta);
+					/* Return old value in r0 */
+					knod_mov32(priv, meta,
+						       bpf_reg64[0].lo,
+						       tmp0_lo);
+				} else {
+					/* 64-bit:
+					 * {r0_lo, r0_hi, src_lo, src_hi}
+					 */
+					knod_mov32(priv, meta,
+						       tmp0_lo,
+						       bpf_reg64[0].lo);
+					knod_mov32(priv, meta,
+						       tmp0_hi,
+						       bpf_reg64[0].hi);
+					knod_mov32(priv, meta,
+						       tmp1_lo,
+						       bpf_reg64[s].lo);
+					knod_mov32(priv, meta,
+						       tmp1_hi,
+						       bpf_reg64[s].hi);
+
+					emit_global_atomic_cmpswap_x2(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						tmp0_lo, bpf_reg64[d].lo,
+						tmp0_lo, off, 1);
+					debug_insn(priv->isa_version,
+						   &meta->amdgpu_insn[meta->amdgpu_insns]);
+					meta->amdgpu_insns++;
+					knod_wait_vmcnt(priv, meta);
+					knod_mov32(priv, meta,
+						       bpf_reg64[0].lo,
+						       tmp0_lo);
+					knod_mov32(priv, meta,
+						       bpf_reg64[0].hi,
+						       tmp0_hi);
+				}
+			} else if (!fetch && atomic_op == BPF_ADD) {
+				/*
+				 * Wave reduction for BPF_ADD (non-fetch):
+				 * Instead of all lanes doing atomic_add(val),
+				 * count active lanes, multiply by val, and
+				 * have a single lane do atomic_add(count*val).
+				 *
+				 * Assumes src_reg is uniform across all active
+				 * lanes (true for constant increments like
+				 * +=1).
+				 *
+				 *   s_bcnt1_i32_b64 s_tmp, exec
+				 *   v_mul_lo_u32    v_tmp, s_tmp, v_src
+				 *   v_mbcnt_lo      v_tmp2, exec_lo, 0
+				 *   v_mbcnt_hi      v_tmp2, exec_hi, v_tmp2
+				 *   v_cmp_eq_u32    vcc, v_tmp2, 0
+				 *   s_and_saveexec  s_save, vcc
+				 *   global_atomic_add addr, v_tmp, off
+				 *   s_waitcnt       vmcnt(0)
+				 *   s_mov_b64       exec, s_save
+				 */
+				struct amdgcn_param32 v_tmp, v_tmp2,
+						      s_count, s_exec_lo,
+						      s_exec_hi, v_zero;
+
+				knod_vset32(&v_tmp,
+					KNOD_AMDGPU_TMP_VREG0_LO);
+				knod_vset32(&v_tmp2,
+					KNOD_AMDGPU_TMP_VREG0_HI);
+				knod_sset32(&s_count,
+					KNOD_AMDGPU_TMP_SREG0_LO);
+				knod_sset32(&s_exec_lo,
+					AMDGCN_SREG_EXEC_LO);
+				knod_sset32(&s_exec_hi,
+					AMDGCN_SREG_EXEC_LO + 1);
+				knod_iset32(&v_zero, 0);
+
+				/* s_bcnt1_i32_b64 s_count, exec */
+				knod_emit(priv, meta, s_bcnt1_i32_b64,
+					  KNOD_AMDGPU_TMP_SREG0_LO,
+					  AMDGCN_SREG_EXEC_LO);
+
+				/* v_mul_lo_u32 v_tmp, s_count, v_src */
+				knod_emit(priv, meta, v_mul_lo_u32, v_tmp,
+					  s_count, bpf_reg64[s].lo);
+
+				/* v_mbcnt_lo_u32_b32 v_tmp2, exec_lo, 0 */
+				knod_emit(priv, meta, v_mbcnt_lo_u32_b32,
+					  v_tmp2, s_exec_lo, v_zero);
+
+				/* v_mbcnt_hi_u32_b32 v_tmp2, exec_hi, v_tmp2 */
+				knod_emit(priv, meta, v_mbcnt_hi_u32_b32,
+					  v_tmp2, s_exec_hi, v_tmp2);
+
+				/* v_cmp_eq_u32 vcc, 0, v_tmp2 ->
+				 * first active lane
+				 */
+				knod_emit(priv, meta, v_cmp_eq_u32, v_zero,
+					  v_tmp2);
+
+				/* s_and_saveexec_b64 s_save, vcc */
+				knod_emit(priv, meta, s_and_saveexec_b64,
+					  KNOD_AMDGPU_TMP_SREG0_LO,
+					  AMDGCN_SREG_VCC_LO);
+
+				if (is_dw) {
+					struct amdgcn_param32 v_tmp_hi;
+
+					knod_vset32(&v_tmp_hi,
+						KNOD_AMDGPU_TMP_VREG0_HI);
+					/*
+					 * x2 atomics consume a consecutive
+					 * VGPR pair, and the 32-bit addend
+					 * lands in the host-visible low dword
+					 * when it is placed in the second
+					 * register.
+					 */
+					knod_emit(priv, meta, v_mov_b32_e32,
+						  v_tmp_hi, v_tmp);
+					knod_emit(priv, meta, v_mov_b32_e32,
+						  v_tmp, v_zero);
+					/* global_atomic_add_x2 addr,
+					 * {0, v_tmp_hi}, off
+					 */
+					knod_emit(priv, meta,
+						  global_atomic_add_x2, v_tmp,
+						  bpf_reg64[d].lo, v_tmp, off,
+						  0);
+				} else {
+					/* global_atomic_add addr, v_tmp, off
+					 * (single lane)
+					 */
+					knod_emit(priv, meta, global_atomic_add,
+						  v_tmp,
+						  bpf_reg64[d].lo, v_tmp, off,
+						  0);
+				}
+
+				/*
+				 * No s_waitcnt needed: glc=0 atomic doesn't
+				 * increment vmcnt. The GPU guarantees all
+				 * pending ops complete before wave exit.
+				 */
+
+				/* s_mov_b64 exec, s_save */
+				knod_emit(priv, meta, s_mov_b64,
+					  AMDGCN_SREG_EXEC_LO,
+					  KNOD_AMDGPU_TMP_SREG0_LO);
+			} else if (!is_dw) {
+				/* 32-bit: AND, OR, XOR, XCHG, or fetch ops */
+				struct amdgcn_param32 vdst, data_p;
+
+				if (fetch) {
+					vdst = bpf_reg64[s].lo;
+				} else {
+					knod_vset32(&vdst,
+						KNOD_AMDGPU_TMP_VREG0_LO);
+				}
+				data_p = bpf_reg64[s].lo;
+
+				switch (atomic_op) {
+				case BPF_ADD:
+					emit_global_atomic_add(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				case BPF_AND:
+					emit_global_atomic_and(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				case BPF_OR:
+					emit_global_atomic_or(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				case BPF_XOR:
+					emit_global_atomic_xor(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				default: /* BPF_XCHG */
+					emit_global_atomic_swap(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				}
+			} else {
+				/* 64-bit: AND, OR, XOR, XCHG, or fetch ops */
+				struct amdgcn_param32 vdst, data_p;
+
+				if (fetch) {
+					vdst = bpf_reg64[s].lo;
+				} else {
+					knod_vset32(&vdst,
+						KNOD_AMDGPU_TMP_VREG0_LO);
+				}
+				data_p = bpf_reg64[s].lo;
+
+				switch (atomic_op) {
+				case BPF_ADD:
+					emit_global_atomic_add_x2(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				case BPF_AND:
+					emit_global_atomic_and_x2(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				case BPF_OR:
+					emit_global_atomic_or_x2(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				case BPF_XOR:
+					emit_global_atomic_xor_x2(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				default: /* BPF_XCHG */
+					emit_global_atomic_swap_x2(
+						priv->isa_version,
+						&meta->amdgpu_insn[meta->amdgpu_insns],
+						vdst, bpf_reg64[d].lo,
+						data_p, off, fetch);
+					break;
+				}
+				debug_insn(priv->isa_version,
+					   &meta->amdgpu_insn[meta->amdgpu_insns]);
+				meta->amdgpu_insns++;
+				/* Always wait for atomic completion */
+				knod_wait_vmcnt(priv, meta);
+			}
+			break;
+		case BPF_ST | BPF_MEM | BPF_B:
+			knod_iset32(&p32[0], imm);
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_iset64(&p64[0], imm);
+				knod_bpf_store_cache_size(priv, meta,
+						&p64[0],
+						&stack[0],
+						sizeof(u8),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_byte, p32[0],
+					  bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_byte, p32[0],
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_byte, p32[0],
+					  bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				knod_iset64(&p64[0], imm);
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv,
+							meta,
+							&p64[0],
+							&pkt_cache[0],
+							sizeof(u8),
+							packet_off);
+				} else {
+					knod_emit(priv, meta, global_store_byte,
+						  p64[0].lo,
+						  bpf_reg64[d].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_ST | BPF_MEM | BPF_H:
+			knod_iset32(&p32[0], imm);
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_iset64(&p64[0], imm);
+				knod_bpf_store_cache_size(priv, meta,
+						&p64[0],
+						&stack[0],
+						sizeof(u16),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_short,
+					  p32[0], bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_short,
+					  p32[0], bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_short,
+					  p32[0], bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				knod_iset64(&p64[0], imm);
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv,
+							meta,
+							&p64[0],
+							&pkt_cache[0],
+							sizeof(u16),
+							packet_off);
+				} else {
+					knod_emit(priv, meta,
+						  global_store_short, p64[0].lo,
+						  bpf_reg64[d].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_ST | BPF_MEM | BPF_W:
+			knod_iset32(&p32[0], imm);
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_iset64(&p64[0], imm);
+				knod_bpf_store_cache_size(priv, meta,
+						&p64[0],
+						&stack[0],
+						sizeof(u32),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_dword,
+					  p32[0], bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_dword,
+					  p32[0], bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_dword,
+					  p32[0], bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				knod_iset64(&p64[0], imm);
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv,
+							meta,
+							&p64[0],
+							&pkt_cache[0],
+							sizeof(u32),
+							packet_off);
+				} else {
+					knod_emit(priv, meta,
+						  global_store_dword, p64[0].lo,
+						  bpf_reg64[d].lo, off);
+				}
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_ST | BPF_MEM | BPF_DW:
+			knod_iset32(&p32[0], imm);
+			if (meta->ptr.type == PTR_TO_STACK) {
+				stack_off = meta->dreg.stack_off + off;
+				knod_iset64(&p64[0], imm);
+				knod_bpf_store_cache_size(priv, meta,
+						&p64[0],
+						&stack[0],
+						sizeof(u64),
+						512 + stack_off);
+			} else if (meta->ptr.type == PTR_TO_CTX) {
+				knod_emit(priv, meta, global_store_dwordx2,
+					  p32[0], bpf_reg64[d].lo, off * 2);
+			} else if (meta->ptr.type == PTR_TO_MAP_VALUE) {
+				knod_emit(priv, meta, global_store_dwordx2,
+					  p32[0], bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_MAP_KEY) {
+				knod_emit(priv, meta, global_store_dwordx2,
+					  p32[0], bpf_reg64[d].lo, off);
+			} else if (meta->ptr.type == PTR_TO_PACKET) {
+				knod_iset64(&p64[0], imm);
+				if (knod_bpf_pkt_cache) {
+					packet_off = meta->dreg.packet_off +
+						off;
+					knod_bpf_store_cache_size(priv, meta,
+							&p64[0],
+							&pkt_cache[0],
+							sizeof(u64),
+							packet_off);
+				} else {
+					knod_emit(priv, meta,
+						  global_store_dwordx2,
+						  p64[0].lo,
+						  bpf_reg64[d].lo, off);
+				}
+				knod_iset32(&p32[0], imm);
+			} else {
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_JMP32 | BPF_JA | BPF_K:
+			if (meta->branch_type == KNOD_BR_DIRECT_EXIT) {
+				knod_bpf_emit_direct_exit_retval(priv, meta,
+						meta->merge_point);
+
+				/* Unconditional goto exit:
+				 * all active lanes done
+				 */
+				knod_emit(priv, meta, s_or_b64,
+					  knod_prog->done_mask_sreg,
+					  knod_prog->done_mask_sreg,
+					  AMDGCN_SREG_EXEC_LO);
+				knod_emit(priv, meta, s_mov_b64,
+					  AMDGCN_SREG_EXEC_LO,
+					  AMDGCN_SREG_INTEGER_0);
+			} else if (meta->branch_type == KNOD_BR_FORWARD_GOTO) {
+				/* Structurized: save all active lanes, clear
+				 * EXEC.  Lanes resume at merge_point (target).
+				 */
+				knod_emit(priv, meta, s_mov_b64,
+					  meta->exec_save_sreg,
+					  AMDGCN_SREG_EXEC_LO);
+				knod_emit(priv, meta, s_mov_b64,
+					  AMDGCN_SREG_EXEC_LO,
+					  AMDGCN_SREG_INTEGER_0);
+			} else {
+				/* Reorder classifies every JA as FORWARD_GOTO
+				 * or DIRECT_EXIT; reaching here is a bug.
+				 */
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_JMP | BPF_JA | BPF_K:
+			if (meta->branch_type == KNOD_BR_DIRECT_EXIT) {
+				knod_bpf_emit_direct_exit_retval(priv, meta,
+						meta->merge_point);
+
+				/* Unconditional goto exit:
+				 * all active lanes done
+				 */
+				knod_emit(priv, meta, s_or_b64,
+					  knod_prog->done_mask_sreg,
+					  knod_prog->done_mask_sreg,
+					  AMDGCN_SREG_EXEC_LO);
+				knod_emit(priv, meta, s_mov_b64,
+					  AMDGCN_SREG_EXEC_LO,
+					  AMDGCN_SREG_INTEGER_0);
+			} else if (meta->branch_type == KNOD_BR_FORWARD_GOTO) {
+				/* Structurized: save all active lanes, clear
+				 * EXEC.  Lanes resume at merge_point (target).
+				 */
+				knod_emit(priv, meta, s_mov_b64,
+					  meta->exec_save_sreg,
+					  AMDGCN_SREG_EXEC_LO);
+				knod_emit(priv, meta, s_mov_b64,
+					  AMDGCN_SREG_EXEC_LO,
+					  AMDGCN_SREG_INTEGER_0);
+			} else {
+				/* Reorder classifies every JA as FORWARD_GOTO
+				 * or DIRECT_EXIT; reaching here is a bug.
+				 */
+				WARN_ON_ONCE(1);
+			}
+			break;
+		case BPF_JMP32 | BPF_JEQ | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_eq_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JEQ | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_eq_u64, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JEQ | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_eq_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JEQ | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_eq_u64, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JGT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_gt_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JGT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_gt_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JGT | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_gt_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JGT | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], s * 2);
+			knod_emit(priv, meta, v_cmp_gt_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JGE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_ge_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JGE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_ge_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JGE | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_ge_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JGE | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], s * 2);
+			knod_emit(priv, meta, v_cmp_ge_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JLT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_lt_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JLT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_lt_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JLT | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_lt_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JLT | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], s * 2);
+			knod_emit(priv, meta, v_cmp_lt_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JLE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_le_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JLE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_le_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JLE | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_le_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JLE | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], 2 * 2);
+			knod_emit(priv, meta, v_cmp_le_u64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSGT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_gt_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSGT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_gt_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSGT | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_gt_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSGT | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], s * 2);
+			knod_emit(priv, meta, v_cmp_gt_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSGE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_ge_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSGE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_ge_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSGE | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_ge_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSGE | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], s * 2);
+			knod_emit(priv, meta, v_cmp_ge_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSLT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_lt_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSLT | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_lt_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSLT | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_lt_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSLT | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], s * 2);
+			knod_emit(priv, meta, v_cmp_lt_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSLE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_le_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSLE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_le_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSLE | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_le_i32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSLE | BPF_X:
+			knod_vset64(&param64[0], d * 2);
+			knod_vset64(&param64[1], s * 2);
+			knod_emit(priv, meta, v_cmp_le_i64, param64[0],
+				  param64[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSET | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], d * 2);
+			knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_and_b32_e32, param[0],
+				  param[1], param[2]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_eq_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSET | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], d * 2);
+			knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_and_b32_e32, param[0],
+				  param[1], param[2]);
+			knod_vset32(&param[0], (d * 2) + 1);
+			knod_vset32(&param[1], (d * 2) + 1);
+			knod_vset32(&param[2], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_emit(priv, meta, v_and_b32_e32, param[0],
+				  param[1], param[2]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_eq_u64, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JSET | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], d * 2);
+			knod_vset32(&param[2], s * 2);
+			knod_emit(priv, meta, v_and_b32_e32, param[0],
+				  param[1], param[2]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JSET | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], d * 2);
+			knod_vset32(&param[2], s * 2);
+			knod_emit(priv, meta, v_and_b32_e32, param[0],
+				  param[1], param[2]);
+			knod_vset32(&param[0], (d * 2) + 1);
+			knod_vset32(&param[1], (d * 2) + 1);
+			knod_vset32(&param[2], (s * 2) + 1);
+			knod_emit(priv, meta, v_and_b32_e32, param[0],
+				  param[1], param[2]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JNE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_eq_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JNE | BPF_K:
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_iset32(&param[1], imm);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], KNOD_AMDGPU_TMP_VREG0_HI);
+			knod_iset32(&param[1], 0);
+			knod_emit(priv, meta, v_mov_b32_e32, param[0],
+				  param[1]);
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_emit(priv, meta, v_cmp_eq_u64, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_JNE | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_eq_u32, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP | BPF_JNE | BPF_X:
+			knod_vset32(&param[0], d * 2);
+			knod_vset32(&param[1], s * 2);
+			knod_emit(priv, meta, v_cmp_eq_u64, param[0],
+				  param[1]);
+			knod_bpf_emit_branch_tail(priv, meta, knod_prog, off);
+			break;
+		case BPF_JMP32 | BPF_CALL:
+		case BPF_JMP | BPF_CALL:
+			switch (imm) {
+			case 1:
+				if (map_id == -1) {
+					WARN_ON_ONCE(1);
+					break;
+				}
+				knod_bpf_map_lookup(priv, meta, map_id);
+				map_id = -1;
+				break;
+			case 2: {
+				struct knod_bpf_map_obj *_map_obj;
+
+				if (map_id == -1) {
+					WARN_ON_ONCE(1);
+					break;
+				}
+				_map_obj = knod_bpf_map_kaddr(priv, map_id);
+				if (!_map_obj) {
+					WARN_ON_ONCE(1);
+					break;
+				}
+				if (_map_obj->map_type == BPF_MAP_TYPE_ARRAY)
+					knod_bpf_map_update_array(priv, meta,
+						map_id);
+				else if (_map_obj->map_type ==
+					 BPF_MAP_TYPE_HASH)
+					knod_bpf_map_update_hash(priv, meta,
+						map_id);
+				else
+					WARN_ON_ONCE(1);
+				map_id = -1;
+				break;
+			}
+			case 3: {
+				struct knod_bpf_map_obj *_map_obj;
+
+				if (map_id == -1) {
+					WARN_ON_ONCE(1);
+					break;
+				}
+				_map_obj = knod_bpf_map_kaddr(priv, map_id);
+				if (!_map_obj) {
+					WARN_ON_ONCE(1);
+					break;
+				}
+				if (_map_obj->map_type == BPF_MAP_TYPE_ARRAY)
+					knod_bpf_map_delete_array(priv, meta,
+						map_id);
+				else if (_map_obj->map_type ==
+					 BPF_MAP_TYPE_HASH)
+					knod_bpf_map_delete_hash(priv, meta,
+						map_id);
+				else
+					WARN_ON_ONCE(1);
+				map_id = -1;
+				break;
+			}
+			case 5:
+				knod_bpf_ktime_get_ns(priv, meta);
+				break;
+			case 44:
+				knod_bpf_xdp_adjust_head(priv, meta);
+				break;
+			case 65:
+				knod_bpf_xdp_adjust_tail(priv, meta);
+				break;
+			default:
+				WARN_ON_ONCE(1);
+				break;
+			}
+			break;
+		case BPF_JMP32 | BPF_EXIT:
+		case BPF_JMP | BPF_EXIT:
+			/* Structurized CFG: BPF_EXIT is NOT a terminator.
+			 * Mark all active lanes as done and clear EXEC.
+			 * Actual exit handling (retval store, pkt_cache,
+			 * PASS block, s_endpgm) is in the unified
+			 * fallthrough EXIT at the end of the stream.
+			 * This follows the LLVM StructurizeCFG model where
+			 * all lanes must reach the single exit point.
+			 */
+			knod_emit(priv, meta, s_or_b64,
+				  knod_prog->done_mask_sreg,
+				  knod_prog->done_mask_sreg,
+				  AMDGCN_SREG_EXEC_LO);
+
+			knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+				  AMDGCN_SREG_INTEGER_0);
+			break;
+		case BPF_ALU | BPF_END | BPF_TO_BE: {
+			struct amdgcn_param32 v_dst_lo, v_dst_hi, v_tmp, s_sel;
+
+			knod_vset32(&v_dst_lo, d * 2);
+			knod_vset32(&v_dst_hi, d * 2 + 1);
+			knod_vset32(&v_tmp, KNOD_AMDGPU_TMP_VREG0_LO);
+			knod_sset32(&s_sel, KNOD_AMDGPU_TMP_SREG0_LO);
+
+			switch (imm) {
+			case 16:
+				/* bswap16+zext: {0,0,byte0,byte1} */
+				knod_iset32(&param[0], 0x0C0C0001);
+				knod_emit(priv, meta, s_mov_b32, s_sel,
+					  param[0]);
+
+				knod_emit(priv, meta, v_perm_b32, v_dst_lo,
+					  v_dst_lo, v_dst_lo, s_sel);
+
+				knod_iset32(&param[0], 0);
+				knod_emit(priv, meta, v_mov_b32_e32, v_dst_hi,
+					  param[0]);
+				break;
+			case 32:
+				/* bswap32+zext */
+				knod_iset32(&param[0], 0x00010203);
+				knod_emit(priv, meta, s_mov_b32, s_sel,
+					  param[0]);
+
+				knod_emit(priv, meta, v_perm_b32, v_dst_lo,
+					  v_dst_lo, v_dst_lo, s_sel);
+
+				knod_iset32(&param[0], 0);
+				knod_emit(priv, meta, v_mov_b32_e32, v_dst_hi,
+					  param[0]);
+				break;
+			case 64: {
+				struct amdgcn_param32 v_src_hi;
+
+				knod_vset32(&v_src_hi, d * 2 + 1);
+
+				/* bswap32 selector */
+				knod_iset32(&param[0], 0x00010203);
+				knod_emit(priv, meta, s_mov_b32, s_sel,
+					  param[0]);
+
+				/* tmp = bswap32(lo) */
+				knod_emit(priv, meta, v_perm_b32, v_tmp,
+					  v_dst_lo, v_dst_lo, s_sel);
+
+				/* new_lo = bswap32(hi) */
+				knod_emit(priv, meta, v_perm_b32, v_dst_lo,
+					  v_src_hi, v_src_hi, s_sel);
+
+				/* new_hi = tmp (bswap32(old_lo)) */
+				knod_emit(priv, meta, v_mov_b32_e32, v_dst_hi,
+					  v_tmp);
+				break;
+			}
+			default:
+				WARN_ON_ONCE(1);
+				break;
+			}
+			break;
+		}
+		case BPF_ALU | BPF_END | BPF_TO_LE: {
+			struct amdgcn_param32 v_dst_lo, v_dst_hi;
+
+			knod_vset32(&v_dst_lo, d * 2);
+			knod_vset32(&v_dst_hi, d * 2 + 1);
+
+			switch (imm) {
+			case 16:
+				knod_iset32(&param[0], 0xFFFF);
+				knod_emit(priv, meta, v_and_b32_e32, v_dst_lo,
+					  param[0], v_dst_lo);
+
+				knod_iset32(&param[0], 0);
+				knod_emit(priv, meta, v_mov_b32_e32, v_dst_hi,
+					  param[0]);
+				break;
+			case 32:
+				knod_iset32(&param[0], 0);
+				knod_emit(priv, meta, v_mov_b32_e32, v_dst_hi,
+					  param[0]);
+				break;
+			case 64:
+				break;
+			default:
+				WARN_ON_ONCE(1);
+				break;
+			}
+			break;
+		}
+		default:
+			WARN_ON_ONCE(1);
+			break;
+		}
+
+		WARN_ON(meta->amdgpu_insns >= KNOD_META_INSNS);
+		for (i = 0; i < meta->amdgpu_insns; i++)
+			insn_idx += (meta->amdgpu_insn[i].size / 4);
+	}
+
+	/* Fallthrough EXIT: publish a verdict for every in-bounds lane.
+	 * Lanes that did not reach BPF_EXIT are forced to XDP_DROP below.
+	 */
+	meta = kzalloc(sizeof(*meta), GFP_KERNEL);
+	if (!meta)
+		return -ENOMEM;
+	list_add_tail(&meta->l, &knod_prog->post_insns);
+
+	/* Any in-bounds lane outside done_mask gets a conservative DROP
+	 * verdict instead of publishing stale VGPR state or leaving the
+	 * recycle-time poison in bd->act.
+	 */
+	knod_emit(priv, meta, s_andn2_b64, AMDGCN_SREG_EXEC_LO,
+		  knod_prog->initial_exec_sreg, knod_prog->done_mask_sreg);
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG0_LO);
+	knod_iset32(&p[1], XDP_DROP);
+	knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+	/* Store bd->act for every lane that participated in this dispatch.
+	 * Done lanes retain their low32 action in v0; unfinished lanes publish
+	 * the fallback DROP written above.
+	 */
+	knod_emit(priv, meta, s_mov_b64, AMDGCN_SREG_EXEC_LO,
+		  knod_prog->initial_exec_sreg);
+
+	/* BPF/XDP verdicts are low32; do not spend a second GTT dword per
+	 * packet.
+	 */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG0_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_SLOT_VREG_LO);
+	knod_emit(priv, meta, global_store_dword, p[0], p[1],
+		  offsetof(struct spsc_bd, act));
+
+	if (knod_prog->uses_adjust)
+		knod_bpf_emit_offlen_writeback(priv, meta);
+
+	/* pkt_cache writeback: flush modified packet data back to VRAM */
+	if (knod_bpf_pkt_cache) {
+		knod_vset32(&p[0], r32[0].v);
+		knod_vset32(&p[1],
+					 KNOD_AMDGPU_DATA_VREG_LO);
+		knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+		knod_vset32(&p[0], r32[0].v + 1);
+		knod_vset32(&p[1],
+					 KNOD_AMDGPU_DATA_VREG_HI);
+		knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+		knod_global_store_size_cache(priv, meta,
+					     &pkt_cache[0],
+					     r32[0],
+					     0, /* dst index */
+					     0, /* start offset */
+					     knod_prog->max_packet_off);
+	}
+
+	/* XDP_PASS detection */
+	knod_iset32(&p[0], XDP_PASS);
+	knod_vset32(&p[1], KNOD_AMDGPU_VREG0_LO);
+	knod_emit(priv, meta, v_cmp_eq_u32, p[0], p[1]);
+
+	pass_branch_idx = meta->amdgpu_insns;
+	knod_emit(priv, meta, s_cbranch_vccz, 0);
+
+	/* EXEC &= VCC (only PASS lanes) */
+	knod_emit(priv, meta, s_and_b64, AMDGCN_SREG_EXEC_LO,
+		  AMDGCN_SREG_EXEC_LO, AMDGCN_SREG_VCC_LO);
+
+	/* v_mov param addr to VGPR pair */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_sset32(&p[1], KNOD_AMDGPU_PARAM_SREG_LO);
+	knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_HI);
+	knod_sset32(&p[1], KNOD_AMDGPU_PARAM_SREG_HI);
+	knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+	/* Per-queue pass_count: offset TMP_VREG9 by queue_idx * 4 */
+	/* v_mov_b32 v2, s15 (queue_idx -> VGPR) */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG1_LO);
+	knod_sset32(&p[1],
+				 KNOD_AMDGPU_WORKGROUP_ID_Y_SREG);
+	knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+	/* v_lshlrev_b32 v2, 2, v2 (queue_idx * 4) */
+	knod_vset32(&p[0], KNOD_AMDGPU_VREG1_LO);
+	knod_iset32(&p[1], 2);
+	knod_vset32(&p[2], KNOD_AMDGPU_VREG1_LO);
+	knod_emit(priv, meta, v_lshlrev_b32, p[0], p[1], p[2]);
+
+	/* v_add_u32 v40, v2, v40 (param_addr_lo += queue_idx * 4) */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_VREG1_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_emit(priv, meta, v_add_u32, p[0], p[1], p[2]);
+
+	/* v_mov TMP10_LO, 1 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_iset32(&p[1], 1);
+	knod_emit(priv, meta, v_mov_b32_e32, p[0], p[1]);
+
+	/* global_atomic_add TMP10_LO, TMP9, TMP10_LO,
+	 *                   offsetof(pass_count)
+	 * GLC=1 to receive old_val in vdst (needed for per-lane slot
+	 * index).  With GLC=0 vdst is NOT written, leaving TMP10_LO
+	 * as the addend (1) -- every PASS lane then computes slot=1
+	 * and races on the same pass_meta_buf entry, leaving slot 0 empty.
+	 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_emit(priv, meta, global_atomic_add, p[0], p[1], p[2],
+		  offsetof(struct knod_bpf_param, pass_count), 1);
+
+	/* s_waitcnt vmcnt(0) */
+	knod_emit(priv, meta, s_waitcnt_vmcnt);
+
+	/* v_sub_u32 v40, v40, v2 (restore param_addr_lo) */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_VREG1_LO);
+	knod_emit(priv, meta, v_sub_u32, p[0], p[1], p[2]);
+
+	/* old_val * 2 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_iset32(&p[1], 1);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_emit(priv, meta, v_lshlrev_b32, p[0], p[1], p[2]);
+
+	/* addr_lo += old_val * 2 */
+	knod_vset32(&p[0], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG10_LO);
+	knod_vset32(&p[2], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_emit(priv, meta, v_add_u32, p[0], p[1], p[2]);
+
+	/* global_store_short pass_indices[old_val],
+	 *                    BACKLOG_IDX_VREG
+	 * dst=data, src=addr in wrapper convention
+	 */
+	knod_vset32(&p[0],
+				 KNOD_AMDGPU_BACKLOG_IDX_VREG);
+	knod_vset32(&p[1], KNOD_AMDGPU_TMP_VREG9_LO);
+	knod_emit(priv, meta, global_store_short, p[0], p[1],
+		  offsetof(struct knod_bpf_param, pass_indices));
+
+	/* Copy PASS packet data (shader mode) or store src addr (SDMA mode) */
+	knod_emit_pass_addr_store(priv, meta);
+
+	/* Patch branch offset */
+	pass_dwords = 0;
+
+	for (j = pass_branch_idx + 1; j < meta->amdgpu_insns; j++)
+		pass_dwords += meta->amdgpu_insn[j].size / 4;
+	emit_s_cbranch_vccz(priv->isa_version,
+			    &meta->amdgpu_insn[pass_branch_idx],
+			    pass_dwords);
+
+	knod_emit(priv, meta, s_endpgm);
+
+	for (j = 0; j < meta->amdgpu_insns; j++)
+		insn_idx += meta->amdgpu_insn[j].size / 4;
+
+	if (priv->isa_version == 10) {
+		if (insn_idx % 256) {
+			meta = kzalloc_obj(*meta, GFP_KERNEL);
+			if (!meta)
+				return -ENOMEM;
+			list_add_tail(&meta->l, &knod_prog->post_insns);
+		}
+
+		while (insn_idx % 256) {
+			if (meta->amdgpu_insns >= KNOD_META_INSNS) {
+				meta = kzalloc_obj(*meta, GFP_KERNEL);
+				if (!meta)
+					return -ENOMEM;
+				list_add_tail(&meta->l, &knod_prog->post_insns);
+			}
+			knod_emit(priv, meta, s_code_end);
+			insn_idx +=
+				meta->amdgpu_insn[meta->amdgpu_insns - 1].size /
+				4;
+		}
+	}
+
+	return 0;
+}
+
+static int knod_bpf_translate(struct bpf_prog *prog)
+{
+	struct knod_prog *knod_prog = prog->aux->offload->dev_priv;
+	struct knod_dev *knodev = knod_prog->knodev;
+	int ret;
+
+	knod_bpf_map_setup(prog);
+	ret = knod_bpf_jit(knodev, knod_prog);
+	if (ret < 0) {
+		pr_err("knod: failed to JIT: %d\n", ret);
+		return ret;
+	}
+
+	knod_setup_bpf_prog(prog);
+
+	return 0;
+}
+
+static void knod_bpf_destroy_prog(struct bpf_prog *prog)
+{
+	struct knod_prog *knod_prog = prog->aux->offload->dev_priv;
+	struct knod_dev *knodev = knod_prog->knodev;
+	struct knod_bpf_priv *priv = knodev->accel->xdp.priv;
+
+	/*
+	 * Normally the prog was already uninstalled (offload with a NULL prog
+	 * flipped back to pass).  Guard the abnormal path where the prog is
+	 * freed while still tracked: flip to pass first so the worker stops
+	 * dispatching this code.  The compiled code lives in a kernel slot and
+	 * is no longer read once we flip away; knod_prog is CPU-only IR the GPU
+	 * never touches, so it is safe to free synchronously.
+	 */
+	if (priv && READ_ONCE(priv->prog) == prog) {
+		WRITE_ONCE(priv->prog, NULL);
+		knod_bpf_reload_pass(knodev);
+	}
+	knod_prog_free(knod_prog);
+}
+
+static const struct bpf_prog_offload_ops knod_bpf_dev_ops = {
+	.insn_hook      = knod_bpf_verify_insn,
+	.finalize       = knod_bpf_finalize,
+	.prepare        = knod_bpf_verifier_prep,
+	.translate      = knod_bpf_translate,
+	.destroy        = knod_bpf_destroy_prog,
+};
+
+static int knod_bpf_setup_prog_hw_checks(struct knod_dev *knodev,
+					 struct netdev_bpf *bpf)
+{
+	if (!bpf->prog)
+		return 0;
+
+	return 0;
+}
+
+static int knod_bpf_map_get_next_key(struct bpf_offloaded_map *offmap,
+				     void *key, void *next_key)
+{
+	unsigned int *nkey = (unsigned int *)next_key;
+	unsigned int *_key = (unsigned int *)key;
+
+	if (offmap->map.map_type == BPF_MAP_TYPE_ARRAY ||
+	    offmap->map.map_type == BPF_MAP_TYPE_PERCPU_ARRAY) {
+		if (key == NULL)
+			*nkey = 0;
+		else
+			*nkey = (*_key) + 1;
+
+		if (*nkey >= offmap->map.max_entries)
+			return -ENOENT;
+	} else if (offmap->map.map_type == BPF_MAP_TYPE_HASH) {
+		if (key == NULL)
+			return knod_bpf_map_hash_get_first_key(offmap,
+							       next_key);
+		else
+			return knod_bpf_map_hash_get_next_key(offmap, key,
+							      nkey);
+	}
+
+	return 0;
+}
+
+static int knod_bpf_map_lookup_elem(struct bpf_offloaded_map *offmap,
+				       void *key, void *value)
+{
+	return __knod_bpf_map_lookup_elem(offmap, key, value);
+}
+
+static int knod_bpf_map_update_elem(struct bpf_offloaded_map *offmap,
+				    void *key, void *value, u64 flags)
+{
+	return __knod_bpf_map_update_elem(offmap, key, value, flags);
+}
+
+static int knod_bpf_map_delete_elem(struct bpf_offloaded_map *offmap, void *key)
+{
+	return __knod_bpf_map_delete_elem(offmap, key);
+}
+
+static const struct bpf_map_dev_ops knod_bpf_map_ops = {
+	.map_get_next_key       = knod_bpf_map_get_next_key,
+	.map_lookup_elem        = knod_bpf_map_lookup_elem,
+	.map_update_elem        = knod_bpf_map_update_elem,
+	.map_delete_elem        = knod_bpf_map_delete_elem,
+};
+
+static int knod_bpf_map_alloc(struct knod_dev *knodev,
+			      struct bpf_offloaded_map *offmap)
+{
+	int err;
+
+	if (offmap->map.map_type != BPF_MAP_TYPE_ARRAY &&
+	    offmap->map.map_type != BPF_MAP_TYPE_HASH &&
+	    offmap->map.map_type != BPF_MAP_TYPE_PERCPU_ARRAY) {
+		knod_jit_dbg(" unsupported map type: %d\n",
+			offmap->map.map_type);
+		return -EOPNOTSUPP;
+	}
+
+	err = __knod_bpf_map_alloc(knodev, offmap);
+	if (err) {
+		knod_jit_dbg(" err = %d\n", err);
+		return err;
+	}
+
+	offmap->dev_ops = &knod_bpf_map_ops;
+	return 0;
+}
+
+static int knod_bpf_xdp_install(struct knod_dev *knodev,
+				struct netdev_bpf *bpf)
+{
+	int err = 0;
+
+	ASSERT_RTNL();
+
+	switch (bpf->command) {
+	case XDP_SETUP_PROG:
+		WARN_ON_ONCE(1);
+		break;
+	case XDP_SETUP_PROG_HW:
+		err = knod_bpf_setup_prog_hw_checks(knodev, bpf);
+		if (err)
+			return err;
+
+		err = knod_bpf_xdp_set_prog(knodev, bpf);
+		break;
+	case BPF_OFFLOAD_MAP_ALLOC:
+		err = knod_bpf_map_alloc(knodev, bpf->offmap);
+		break;
+	case BPF_OFFLOAD_MAP_FREE:
+		knod_bpf_map_free(knodev, bpf->offmap);
+		break;
+	default:
+		knod_jit_dbg(" bpf->command = %d\n", bpf->command);
+		err = -EINVAL;
+		break;
+	}
+
+	return err;
+}
+
+static inline int bpf_debugfs_insn(struct knod_bpf_priv *priv,
+				   struct knod_insn_meta *meta,
+				   struct seq_file *m,
+				   int insn_idx)
+{
+	struct amdgcn_insn *insn = &meta->amdgpu_insn[insn_idx];
+
+	if (priv->isa_version == 10)
+		gfx10_debugfs_insn(insn, m);
+	else if (priv->isa_version == 9)
+		gfx9_debugfs_insn(insn, m);
+	else
+		WARN_ON_ONCE(1);
+
+	return insn->size;
+}
+
+/*
+ * Print one disassembled GPU instruction at @offset, then drop the disasm's
+ * trailing newline and append @tag as a right-hand comment aligned to a fixed
+ * column (tabs expand to 8) so the origin lines up regardless of mnemonic
+ * width.  Returns the instruction size in dwords.
+ */
+static int bpf_debugfs_insn_tagged(struct knod_bpf_priv *priv,
+				   struct knod_insn_meta *meta,
+				   struct seq_file *m, int j,
+				   int offset, const char *tag)
+{
+	size_t col, p, line_start = m->count;
+	int sz;
+
+	seq_printf(m, "%d:\t", offset);
+	sz = bpf_debugfs_insn(priv, meta, m, j);
+	if (seq_has_overflowed(m))
+		return sz;
+
+	if (m->count > line_start && m->buf[m->count - 1] == '\n')
+		m->count--;
+	col = 0;
+	for (p = line_start; p < m->count; p++)
+		col = m->buf[p] == '\t' ? (col + 8) & ~(size_t)7 : col + 1;
+	while (col < 96) {
+		seq_putc(m, ' ');
+		col++;
+	}
+	seq_printf(m, " ; %s\n", tag);
+
+	return sz;
+}
+
+/*
+ * Print the instructions a second time, re-sorted into BPF source order so the
+ * dump reads like the program.  The offsets are the real (reordered) GPU
+ * offsets, so they appear out of sequence - that shows where the reorder
+ * placed each block.  Synthetic jumps have no BPF source insn and are last.
+ */
+static void bpf_insn_show_bpf_order(struct knod_bpf_priv *priv,
+				    struct seq_file *m)
+{
+	struct knod_insn_meta *meta;
+	int idx, max_idx = -1, off2, i;
+	bool synth_hdr = false;
+	char tag[24];
+
+	seq_puts(m, "===[INSTRUCTIONS (bpf order)]===\n");
+
+	list_for_each_entry(meta, &priv->knod_prog->insns, l)
+		if (meta->bpf_insn_idx > max_idx)
+			max_idx = meta->bpf_insn_idx;
+
+	for (idx = 0; idx <= max_idx; idx++) {
+		list_for_each_entry(meta, &priv->knod_prog->insns, l) {
+			if (meta->bpf_insn_idx != idx || !meta->amdgpu_insns)
+				continue;
+			scnprintf(tag, sizeof(tag), "bpf#%d", idx);
+			off2 = meta->amdgpu_insn_idx;
+			for (i = 0; i < meta->amdgpu_insns; i++)
+				off2 += bpf_debugfs_insn_tagged(priv, meta, m,
+								i, off2, tag);
+		}
+	}
+
+	list_for_each_entry(meta, &priv->knod_prog->insns, l) {
+		if (meta->bpf_insn_idx >= 0 || !meta->amdgpu_insns)
+			continue;
+		if (!synth_hdr) {
+			seq_puts(m, "  [synthetic jumps]\n");
+			synth_hdr = true;
+		}
+		scnprintf(tag, sizeof(tag), "synth JA->#%d",
+			  meta->jmp_dst ? meta->jmp_dst->bpf_insn_idx : -1);
+		off2 = meta->amdgpu_insn_idx;
+		for (i = 0; i < meta->amdgpu_insns; i++)
+			off2 += bpf_debugfs_insn_tagged(priv, meta, m,
+							i, off2, tag);
+	}
+}
+
+static int bpf_insn_show(struct seq_file *m, void *v)
+{
+	struct knod_bpf_priv *priv = (struct knod_bpf_priv *)m->private;
+	struct knod_insn_meta *meta;
+	struct knod_prog *kp;
+	int i, insn_idx = 0;
+	bool have_prog;
+
+	if (!priv)
+		return 0;
+
+	/*
+	 * Show the kernel the GPU actually dispatches: the XDP prog when one is
+	 * attached, otherwise the retained pass-through kernel.
+	 */
+	have_prog = READ_ONCE(priv->prog);
+	if (have_prog) {
+		kp = priv->knod_prog;
+	} else {
+		kp = priv->pass_knod_prog;
+		seq_puts(m, "no XDP prog attached -- pass-through kernel:\n");
+	}
+	if (!kp)
+		return 0;
+
+	seq_puts(m, "===[PROLOGUE]===\n");
+	list_for_each_entry(meta, &kp->pre_insns, l) {
+		for (i = 0; i < meta->amdgpu_insns; i++) {
+			seq_printf(m, "%d:\t", insn_idx);
+			insn_idx += bpf_debugfs_insn(priv, meta, m, i);
+		}
+	}
+
+	/* Emission (RPO) order - the actual GPU layout.  Each line is tagged
+	 * with its origin BPF insn since the reorder makes this differ from the
+	 * BPF byte order; synthetic jumps inserted by the reorder have none.
+	 */
+	seq_puts(m, "===[INSTRUCTIONS]===\n");
+	list_for_each_entry(meta, &kp->insns, l) {
+		char tag[24];
+
+		if (meta->bpf_insn_idx < 0)
+			scnprintf(tag, sizeof(tag), "synth JA->#%d",
+				  meta->jmp_dst ?
+				  meta->jmp_dst->bpf_insn_idx : -1);
+		else
+			scnprintf(tag, sizeof(tag), "bpf#%d",
+				  meta->bpf_insn_idx);
+
+		for (i = 0; i < meta->amdgpu_insns; i++)
+			insn_idx += bpf_debugfs_insn_tagged(priv, meta, m, i,
+							    insn_idx, tag);
+	}
+
+	seq_puts(m, "===[EPILOG]===\n");
+	list_for_each_entry(meta, &kp->post_insns, l) {
+		for (i = 0; i < meta->amdgpu_insns; i++) {
+			seq_printf(m, "%d:\t", insn_idx);
+			insn_idx += bpf_debugfs_insn(priv, meta, m, i);
+		}
+	}
+
+	if (have_prog)
+		bpf_insn_show_bpf_order(priv, m);
+
+	return 0;
+}
+
+static int bpf_insn_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, bpf_insn_show, inode->i_private);
+}
+
+static const struct file_operations bpf_insn_fops = {
+	.owner   = THIS_MODULE,
+	.open    = bpf_insn_open,
+	.read    = seq_read,
+	.llseek  = seq_lseek,
+	.release = single_release,
+};
+
+static const char *knod_branch_type_str(enum knod_branch_type type)
+{
+	switch (type) {
+	case KNOD_BR_NONE:		return "NONE";
+	case KNOD_BR_DIRECT_EXIT:	return "DIRECT_EXIT";
+	case KNOD_BR_FORWARD_SKIP:	return "FORWARD_SKIP";
+	case KNOD_BR_FORWARD_GOTO:	return "FORWARD_GOTO";
+	default:			return "UNKNOWN";
+	}
+}
+
+static int bpf_cfg_show(struct seq_file *m, void *v)
+{
+	struct knod_bpf_priv *priv = (struct knod_bpf_priv *)m->private;
+	struct knod_insn_meta *meta;
+
+	if (!priv || !priv->knod_prog)
+		return 0;
+
+	seq_puts(m, "===[STRUCTURIZED CFG]===\n");
+	seq_printf(m, "exec_save_pairs_used: %u\n",
+		   priv->knod_prog->exec_save_pairs_used);
+	seq_printf(m, "done_mask: s[%d:%d]\n",
+		   priv->knod_prog->done_mask_sreg,
+		   priv->knod_prog->done_mask_sreg + 1);
+	seq_printf(m, "initial_exec: s[%d:%d]\n",
+		   priv->knod_prog->initial_exec_sreg,
+		   priv->knod_prog->initial_exec_sreg + 1);
+	seq_puts(m, "\n");
+
+	seq_printf(m, "%-6s %-8s %-14s %-10s %-10s %-8s\n",
+		   "bpf#", "opcode", "branch_type", "exec_save", "merge_pt",
+		   "is_merge");
+
+	list_for_each_entry(meta, &priv->knod_prog->insns, l) {
+		bool is_jmp = is_mbpf_jmp(meta);
+
+		if (!is_jmp && !meta->is_merge_point)
+			continue;
+
+		seq_printf(m, "%-6d 0x%02x     ",
+			   meta->bpf_insn_idx, meta->insn.code);
+
+		if (meta->branch_type != KNOD_BR_NONE) {
+			seq_printf(m, "%-14s s[%d:%d]    ",
+				   knod_branch_type_str(meta->branch_type),
+				   meta->exec_save_sreg,
+				   meta->exec_save_sreg + 1);
+			if (meta->merge_point)
+				seq_printf(m, "%-10d ",
+					   meta->merge_point->bpf_insn_idx);
+			else
+				seq_printf(m, "%-10s ", "-");
+		} else if (is_jmp) {
+			seq_printf(m, "%-14s %-10s %-10s ",
+				   knod_branch_type_str(KNOD_BR_NONE),
+				   "-", "-");
+		} else {
+			seq_printf(m, "%-14s %-10s %-10s ",
+				   "", "", "");
+		}
+
+		if (meta->is_merge_point) {
+			struct knod_insn_meta *br;
+
+			seq_puts(m, "YES      restore:");
+			list_for_each_entry(br, &priv->knod_prog->insns, l) {
+				if ((br->branch_type == KNOD_BR_FORWARD_SKIP ||
+				     br->branch_type == KNOD_BR_FORWARD_GOTO) &&
+				    br->merge_point == meta)
+					seq_printf(m, " s[%d:%d](from bpf#%d)",
+						   br->exec_save_sreg,
+						   br->exec_save_sreg + 1,
+						   br->bpf_insn_idx);
+			}
+			seq_puts(m, "\n");
+		} else {
+			seq_puts(m, "\n");
+		}
+	}
+
+	/* Basic-block CFG from the reorder analysis (origin BPF order). */
+	if (priv->knod_prog->bbs) {
+		struct knod_bb *bbs = priv->knod_prog->bbs;
+		int nb = priv->knod_prog->n_bbs;
+		int k, s;
+
+		seq_printf(m, "\n[BASIC BLOCKS]  %d blocks, %d back-edge(s) -> %s\n",
+			   nb, priv->knod_prog->n_back,
+			   priv->knod_prog->n_back ? "HAS LOOP" : "DAG");
+
+		for (k = 0; k < nb; k++) {
+			struct knod_bb *bb = &bbs[k];
+
+			seq_printf(m, "BB%-3d bpf#%d..#%d  rpo=%d  idom=#%d  succ={",
+				   k, bb->leader->bpf_insn_idx,
+				   bb->last->bpf_insn_idx, bb->rpo,
+				   bb->idom ?
+				   bb->idom->leader->bpf_insn_idx : -1);
+			for (s = 0; s < bb->n_succ; s++)
+				seq_printf(m, "%s#%d", s ? "," : "",
+					   bb->succ[s]->leader->bpf_insn_idx);
+			seq_printf(m, "}%s\n",
+				   bb->loop_header ? "  LOOP_HDR" : "");
+		}
+	}
+
+	return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(bpf_cfg);
+
+static int knod_stats_show(struct seq_file *s, void *unused)
+{
+	struct knod_bpf_priv *priv = s->private;
+	u64 p50 = 0, p99 = 0, p999 = 0, acc;
+	struct knod_bpf_stats *stats;
+	u64 ccnt, dcnt;
+	int i;
+
+	stats = &priv->stats;
+	ccnt = stats->completion_count;
+	dcnt = stats->dispatch_count;
+	seq_printf(s, "enabled:             %s\n",
+		   static_branch_unlikely(&knod_stats_key) ? "yes" : "no");
+
+	seq_puts(s, "\n--- dispatch ---\n");
+	seq_printf(s, "count:               %llu\n", dcnt);
+	seq_printf(s, "avg_ns:              %llu\n",
+		   dcnt ? stats->dispatch_total_ns / dcnt : 0);
+	seq_printf(s, "max_ns:              %llu\n", stats->dispatch_max_ns);
+	seq_printf(s, "backlogs_avg:        %llu\n",
+		   dcnt ? stats->backlogs_total / dcnt : 0);
+
+	seq_puts(s, "\nbacklogs histogram:\n");
+	for (i = 0; i < KNOD_BL_BUCKETS; i++)
+		seq_printf(s, "  %-10s %llu\n",
+			   bl_labels[i], stats->backlogs_hist[i]);
+
+	seq_puts(s, "\n--- completion ---\n");
+	seq_printf(s, "count:               %llu\n", ccnt);
+	seq_printf(s, "avg_ns:              %llu\n",
+		   ccnt ? stats->completion_total_ns / ccnt : 0);
+	seq_printf(s, "max_ns:              %llu\n",
+		   stats->completion_max_ns);
+
+	seq_puts(s, "\nlatency histogram:\n");
+	for (i = 0; i < KNOD_LAT_BUCKETS; i++)
+		seq_printf(s, "  %-10s %llu\n",
+			   lat_labels[i], stats->completion_hist[i]);
+
+	if (ccnt) {
+		acc = 0;
+		for (i = 0; i < KNOD_LAT_BUCKETS; i++) {
+			acc += stats->completion_hist[i];
+			if (!p50 && acc * 1000 >= ccnt * 500)
+				p50 = i;
+			if (!p99 && acc * 1000 >= ccnt * 990)
+				p99 = i;
+			if (!p999 && acc * 1000 >= ccnt * 999)
+				p999 = i;
+		}
+		seq_printf(s, "\np50:  %s\n", lat_labels[p50]);
+		seq_printf(s, "p99:  %s\n", lat_labels[p99]);
+		seq_printf(s, "p999: %s\n", lat_labels[p999]);
+	}
+
+	seq_puts(s, "\n--- decode_act ---\n");
+	seq_printf(s, "count:               %llu\n", stats->decode_act_count);
+	seq_printf(s, "avg_ns:              %llu\n",
+		   stats->decode_act_count ?
+		   stats->decode_act_total_ns / stats->decode_act_count : 0);
+	seq_printf(s, "max_ns:              %llu\n", stats->decode_act_max_ns);
+
+	return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(knod_stats);
+
+static ssize_t knod_stats_enable_write(struct file *file,
+				       const char __user *buf,
+				       size_t count, loff_t *ppos)
+{
+	bool val;
+
+	if (kstrtobool_from_user(buf, count, &val))
+		return -EINVAL;
+
+	if (val)
+		static_branch_enable(&knod_stats_key);
+	else
+		static_branch_disable(&knod_stats_key);
+
+	return count;
+}
+
+static ssize_t knod_stats_enable_read(struct file *file,
+				      char __user *buf,
+				      size_t count, loff_t *ppos)
+{
+	char tmp[4];
+	int len;
+
+	len = scnprintf(tmp, sizeof(tmp), "%d\n",
+			static_branch_unlikely(&knod_stats_key) ? 1 : 0);
+
+	return simple_read_from_buffer(buf, count, ppos, tmp, len);
+}
+
+static const struct file_operations knod_stats_enable_fops = {
+	.owner = THIS_MODULE,
+	.read  = knod_stats_enable_read,
+	.write = knod_stats_enable_write,
+};
+
+static ssize_t knod_stats_reset_write(struct file *file,
+		const char __user *buf,
+		size_t count, loff_t *ppos)
+{
+	struct knod_bpf_priv *priv = file->private_data;
+
+	memset(&priv->stats, 0, sizeof(priv->stats));
+	return count;
+}
+
+static const struct file_operations knod_stats_reset_fops = {
+	.owner = THIS_MODULE,
+	.open  = simple_open,
+	.write = knod_stats_reset_write,
+};
+
+static int knod_debugfs_init(struct knod_bpf_priv *priv)
+{
+	struct dentry *dir = priv->knod->debug_dir;
+	struct dentry *bpf_dir;
+
+	if (!dir)
+		return -ENOENT;
+
+	bpf_dir = debugfs_create_dir("bpf", dir);
+	if (IS_ERR(bpf_dir))
+		return PTR_ERR(bpf_dir);
+
+	priv->debug_dir = bpf_dir;
+
+	debugfs_create_file("insn", 0644,
+			    bpf_dir, priv, &bpf_insn_fops);
+	debugfs_create_file("cfg", 0444, bpf_dir, priv,
+			    &bpf_cfg_fops);
+	debugfs_create_file("stats", 0444, bpf_dir, priv,
+			    &knod_stats_fops);
+	debugfs_create_file("stats_enable", 0644, bpf_dir, priv,
+			    &knod_stats_enable_fops);
+	debugfs_create_file("stats_reset", 0200, bpf_dir, priv,
+			    &knod_stats_reset_fops);
+	debugfs_create_bool("poll_mode", 0644, bpf_dir, &knod_bpf_poll_mode);
+	debugfs_create_u32("dispatch_delay_us", 0644, bpf_dir,
+			   &knod_bpf_dispatch_delay_us);
+
+	return 0;
+}
+
+static void knod_debugfs_cleanup(struct knod_bpf_priv *priv)
+{
+	if (!priv->debug_dir)
+		return;
+
+	debugfs_remove_recursive(priv->debug_dir);
+	priv->debug_dir = NULL;
+}
+
+/* Called when attached or module loading time */
+/* attach: allocate the permanent per-attach priv struct. */
+static int knod_accel_xdp_init(struct knod_dev *knodev)
+{
+	struct knod_accel *accel = knodev->accel;
+	struct knod_bpf_priv *priv;
+
+	priv = __knod_accel_xdp_init(accel, knodev);
+	if (IS_ERR(priv))
+		return PTR_ERR(priv);
+	return 0;
+}
+
+/* detach: free the permanent priv struct. */
+static void knod_accel_xdp_exit(struct knod_dev *knodev)
+{
+	struct knod_accel *accel = knodev->accel;
+	struct knod_bpf_priv *priv = accel->xdp.priv;
+
+	__knod_accel_xdp_exit(accel, priv);
+}
+
+/*
+ * Feature select, phase B: register the BPF offload device so user XDP
+ * progs/maps can bind to it.  Called after ->activate() set up the GPU
+ * buffers, while xdp_ops already points at the BPF ops.
+ */
+static int knod_bpf_offload_init(struct knod_dev *knodev)
+{
+	struct knod_accel *accel = knodev->accel;
+	struct knod_bpf_priv *priv = accel->xdp.priv;
+	struct bpf_offload_dev *bpf_dev;
+	int err;
+
+	bpf_dev = bpf_offload_dev_create(&knod_bpf_dev_ops, priv);
+	err = PTR_ERR_OR_ZERO(bpf_dev);
+	if (err)
+		return err;
+	err = bpf_offload_dev_netdev_register(bpf_dev, knodev->netdev);
+	if (err) {
+		bpf_offload_dev_destroy(bpf_dev);
+		return err;
+	}
+	knod_debugfs_init(priv);
+	accel->xdp.bpf_dev = bpf_dev;
+	return 0;
+}
+
+/*
+ * Feature deselect, phase 1: unregister the BPF offload device.  This
+ * force-frees any user XDP progs/maps still bound; the map-free ndo is
+ * routed back through accel_ops.xdp_ops->xdp_install, so the caller keeps
+ * xdp_ops pointed at the BPF ops until this returns.
+ */
+static void knod_bpf_offload_uninit(struct knod_dev *knodev)
+{
+	struct knod_accel *accel = knodev->accel;
+	struct knod_bpf_priv *priv = accel->xdp.priv;
+
+	knod_debugfs_cleanup(priv);
+	bpf_offload_dev_netdev_unregister(accel->xdp.bpf_dev, knodev->netdev);
+	bpf_offload_dev_destroy(accel->xdp.bpf_dev);
+	accel->xdp.bpf_dev = NULL;
+}
+
+struct knod_accel_xdp_ops accel_xdp_ops = {
+	/* attach/detach: permanent priv struct */
+	.init = &knod_accel_xdp_init,
+	.exit = &knod_accel_xdp_exit,
+	/* feature select: GPU compute buffers (A) + offload dev (B) */
+	.activate = &knod_bpf_activate,
+	.deactivate = &knod_bpf_deactivate,
+	.busy = &knod_bpf_busy,
+	.xdp_offload_init = &knod_bpf_offload_init,
+	.xdp_offload_uninit = &knod_bpf_offload_uninit,
+	/* interface up/down (or feature switch): worker + GPU drain */
+	.start = &knod_bpf_start,
+	.stop = &knod_bpf_stop,
+	.xdp_install = &knod_bpf_xdp_install,
+};
+
+static int __init knod_bpf_init_module(void)
+{
+	pr_info("knod-bpf module load\n");
+
+	/* knod_accel_xdp_register() already calls xdp_ops->init() on every
+	 * registered accel, so a second per-accel init loop here would just
+	 * re-create the "bpf" debugfs dir ("already exists" warning) and leak
+	 * a duplicate offload dev.
+	 */
+	knod_dev_lock();
+	knod_accel_xdp_register(&accel_xdp_ops);
+	knod_dev_unlock();
+
+	return 0;
+}
+late_initcall(knod_bpf_init_module);
+
+static void __exit knod_bpf_cleanup_module(void)
+{
+	struct knod_bpf_priv *priv, *tmp;
+	struct knod_accel *accel;
+
+	rtnl_lock();
+	knod_dev_lock();
+	list_for_each_entry_safe(priv, tmp, &priv_list, list) {
+		accel = priv->accel;
+		if (accel->knodev)
+			accel_xdp_ops.exit(accel->knodev);
+	}
+	knod_accel_xdp_unregister();
+	knod_dev_unlock();
+	rtnl_unlock();
+	pr_info("knod-bpf module unload\n");
+}
+module_exit(knod_bpf_cleanup_module);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Taehee Yoo <ap420073@gmail.com>");
+MODULE_DESCRIPTION("AMDGPU BPF offload backend");
+MODULE_VERSION("multi-aql");
diff --git a/drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.h b/drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.h
new file mode 100644
index 000000000000..de6df06c4f2f
--- /dev/null
+++ b/drivers/gpu/drm/amd/amdkfd/knod/knod_bpf.h
@@ -0,0 +1,597 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (c) 2021 Taehee Yoo <ap420073@gmail.com>
+ * Copyright (c) 2021 Hoyeon Lee <hoyeon.rhee@gmail.com>
+ */
+
+#ifndef KFD_BPF_H_INCLUDED
+#define KFD_BPF_H_INCLUDED
+
+#include <uapi/linux/bpf.h>
+#include <net/xdp.h>
+#include <net/netmem.h>
+#include <net/netlink.h>
+#include <net/page_pool/helpers.h>
+#include <net/ip.h>
+#include <net/net_namespace.h>
+#include <net/gro_cells.h>
+#include <net/rtnetlink.h>
+#include <net/protocol.h>
+#include <net/netns/generic.h>
+#include <net/xdp.h>
+#include <net/netdev_lock.h>
+#include <net/spsc_ring.h>
+#include <linux/bpf.h>
+#include <linux/bpf_verifier.h>
+#include <linux/kthread.h>
+#include <linux/sched.h>
+#include <linux/skbuff.h>
+#include <linux/net.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/etherdevice.h>
+#include <linux/hash.h>
+#include <linux/netdevice.h>
+#include <linux/types.h>
+#include <linux/bpf.h>
+#include <linux/bpf_verifier.h>
+#include <linux/debugfs.h>
+#include <linux/kernel.h>
+#include <linux/mutex.h>
+#include <linux/rtnetlink.h>
+#include <linux/workqueue.h>
+#include <linux/ktime.h>
+#include <linux/static_key.h>
+#include "knod_amdgpu_insn.h"
+#include "../../../../../../net/core/devmem.h"
+#include "../amdgpu/amdgpu_vm.h"
+#include "knod_bpf.h"
+#include "kfd_knod.h"
+
+#define KNOD_BPF_BACKLOGS_MAX		65536
+#define KNOD_BPF_INFLIGHT		3	/* triple-buffered dispatches */
+#define KNOD_BPF_WORKGROUPS_DEFAULT     256
+#define KNOD_BPF_WORKGROUPS_MIN         64
+#define KNOD_BPF_WORKGROUPS_MAX         256
+#define KNOD_BPF_EXPIRE_DEFAULT		10
+#define KNOD_BPF_EXPIRE_MIN		1
+#define KNOD_BPF_EXPIRE_MAX		1000
+#define QUEUE_SIZE_DGPU			8192
+#define QUEUE_SIZE_IGPU			2048
+#define KNOD_MAX_BDS			(KNOD_BPF_BACKLOGS_MAX / KNOD_SPSC_MAX)
+
+#define MAX_KEY_SIZE		64 /* 64Bytes */
+#define MAX_PACKET_CACHE	256 /* 256Bytes */
+
+#define knod_prog_first_meta(knod_prog)					\
+	list_first_entry(&(knod_prog)->insns, struct knod_insn_meta, l)
+#define knod_prog_last_meta(knod_prog)					\
+	list_last_entry(&(knod_prog)->insns, struct knod_insn_meta, l)
+#define knod_prog_pre_last_meta(knod_prog)				\
+	list_last_entry(&(knod_prog)->pre_insns, struct knod_insn_meta, l)
+#define knod_meta_next(meta)     list_next_entry(meta, l)
+#define knod_meta_prev(meta)     list_prev_entry(meta, l)
+
+#define knod_for_each_insn_walk2(knod_prog, pos, next)			  \
+	for (pos = list_first_entry(&(knod_prog)->insns, typeof(*pos), l),\
+			next = list_next_entry(pos, l);			  \
+			&(knod_prog)->insns != &pos->l &&                 \
+			&(knod_prog)->insns != &next->l;                  \
+			pos = knod_meta_next(pos),                        \
+			next = knod_meta_next(pos))
+
+#define knod_for_each_insn_walk3(knod_prog, pos, next, next2)		  \
+	for (pos = list_first_entry(&(knod_prog)->insns, typeof(*pos), l),\
+			next = list_next_entry(pos, l),			  \
+			next2 = list_next_entry(next, l);		  \
+			&(knod_prog)->insns != &pos->l &&		  \
+			&(knod_prog)->insns != &next->l &&		  \
+			&(knod_prog)->insns != &next2->l;		  \
+			pos = knod_meta_next(pos),			  \
+			next = knod_meta_next(pos),			  \
+			next2 = knod_meta_next(next))
+
+struct xdp_md_obj {
+	u64 data;
+	u64 data_end;
+	u64 data_meta;
+	/* Below access go through struct xdp_rxq_info */
+	u64 ingress_ifindex; /* rxq->dev->ifindex */
+	u64 rx_queue_index;  /* rxq->queue_index  */
+
+	u64 egress_ifindex;  /* txq->dev->ifindex */
+	u64 retval;
+};
+
+struct knod_bpf_subparam_obj {
+	struct xdp_md_obj ctx;
+};
+
+#define KNOD_BPF_HASH_NEXT_END		0x7FFFFFFFU
+#define KNOD_BPF_HASH_NEXT_DELETED	0x80000000U
+#define KNOD_BPF_HASH_NEXT_MASK		0x7FFFFFFFU
+
+struct knod_bpf_hash_elem_obj {
+	unsigned int next;
+	unsigned char kv[];
+};
+
+struct knod_bpf_map_hash_meta_obj {
+	unsigned int n_buckets;
+	unsigned int hashrnd;
+	unsigned int cur;
+	unsigned int elem_size;
+	void *q;
+	void *elems;
+	unsigned int gc_count;
+	void *gc_list;
+};
+
+struct knod_bpf_map_array_meta_obj {
+	u32 per_instance_size;	/* value_size * max_entries (one instance) */
+	u32 n_instances;	/* 1 for ARRAY, num_possible_cpus for PERCPU */
+};
+
+union knod_bpf_map_meta_obj {
+	struct knod_bpf_map_hash_meta_obj hmeta;
+	struct knod_bpf_map_array_meta_obj ameta;
+};
+
+struct knod_bpf_map_obj {
+	enum bpf_map_type map_type;
+	unsigned int key_size;
+	unsigned int value_size;
+	unsigned int max_entries;
+	unsigned int id;
+	unsigned long map_extra; /* any per-map-type extra fields */
+	unsigned int map_flags;
+	union knod_bpf_map_meta_obj meta;
+	int mutex;
+	unsigned char bucket[];
+};
+
+struct knod_bpf_map {
+	struct list_head list;
+	struct knod_mem *mem, *queue_mem, *hash_elems_mem, *gc_mem;
+	/* ptr to mem_k->kaddr */
+	struct knod_bpf_map_obj *knod_map_obj;
+	struct bpf_offloaded_map *offmap;
+	struct knod_bpf_priv *priv;
+};
+
+struct knod_bpf_queue_desc {
+	u64 pool_gaddr;		/* SPSC pool GTT address for this queue */
+	u64 base_gaddr;		/* dma-buf base address for this queue */
+	u32 count;		/* number of packets from this queue */
+	/* was start_idx; kept for global_load_dwordx4 layout */
+	u32 _pad;
+	u32 ring_start;		/* acquired cursor at peek time */
+	u32 ring_mask;		/* capacity - 1 */
+};
+
+struct knod_bpf_param {
+	u32 nr_backlogs;
+	u32 nr_queues;
+	u32 spsc_stride;
+	u32 _pad0;
+	u64 ktime_ns;		/* snapshot of ktime_get_ns() at dispatch */
+	u32 pass_count[KNOD_SPSC_MAX];	/* per-queue atomic XDP_PASS counter */
+	/* per-queue GTT pass_meta_buf GPU addr */
+	u64 pass_meta_buf_gaddr[KNOD_SPSC_MAX];
+	struct knod_bpf_queue_desc queues[KNOD_SPSC_MAX];
+	/* backlog indices of PASS packets */
+	u16 pass_indices[KNOD_BPF_BACKLOGS_MAX];
+	struct knod_bpf_subparam_obj sub[KNOD_BPF_BACKLOGS_MAX];
+};
+
+struct knod_packet {
+	union {
+		netmem_ref netmem;
+		void *kaddr;
+	};
+	u16 len;
+	u16 off;
+};
+
+/* Single Queue Worok */
+struct knod_bpf_work_sq {
+	struct list_head list;
+	struct knod_mem *param;
+	int queue_idx[KNOD_SPSC_MAX];
+	struct spsc_bd *bds[KNOD_BPF_BACKLOGS_MAX];
+	ktime_t dispatch_time;
+	s64 sigval;
+	unsigned long expire;
+	int backlogs;
+};
+
+struct knod_bpf_reg_state {
+	struct bpf_reg_state reg;
+	int stack_off;
+	int packet_off;
+	bool var_off;
+};
+
+/* Structurized CFG branch types */
+enum knod_branch_type {
+	KNOD_BR_NONE = 0,	/* not a branch */
+	/* backward jump to exit: inline retval + done_mask update */
+	KNOD_BR_DIRECT_EXIT,
+	KNOD_BR_FORWARD_SKIP,	/* forward jump: skip region via EXEC mask */
+	KNOD_BR_FORWARD_GOTO,	/* forward jump crossing other branch scopes */
+};
+
+#define KNOD_META_INSNS		1024
+#define AMDGPU_INSN_SKIP	-1
+struct knod_insn_meta {
+	struct bpf_insn insn;
+	short bpf_insn_idx;
+
+	struct amdgcn_insn amdgpu_insn[KNOD_META_INSNS];
+	u32 amdgpu_insn_idx;
+	u32 amdgpu_insns;
+
+	union {
+		/* pointer ops (ld/st/xadd) */
+		struct {
+			struct bpf_reg_state ptr;
+			struct bpf_insn *paired_st;
+			s16 ldst_gather_len;
+			bool ptr_not_const;
+			struct {
+				s16 range_start;
+				s16 range_end;
+				bool do_init;
+			} pkt_cache;
+			bool xadd_over_16bit;
+			bool xadd_maybe_16bit;
+		};
+		/* jump */
+		struct {
+			struct knod_insn_meta *jmp_dst;
+			bool jump_neg_op;
+			u32 num_insns_after_br; /* only for BPF-to-BPF calls */
+			/* structurized CFG */
+			enum knod_branch_type branch_type;
+			/* SGPR index for s_and_saveexec_b64 */
+			u8 exec_save_sreg;
+			/* where EXEC is restored */
+			struct knod_insn_meta *merge_point;
+		};
+		/* function calls */
+		struct {
+			u32 func_id;
+			struct bpf_reg_state arg1;
+			struct knod_bpf_reg_state arg2;
+		};
+		/* We are interested in range info for operands of ALU
+		 * operations. For example, shift amount, multiplicand and
+		 * multiplier etc.
+		 */
+		struct {
+			u64 umin_src;
+			u64 umax_src;
+			u64 umin_dst;
+			u64 umax_dst;
+		};
+	};
+
+	struct knod_bpf_reg_state sreg;
+	struct knod_bpf_reg_state dreg;
+	struct knod_bpf_reg_state kreg;
+	struct knod_bpf_reg_state vreg;
+	unsigned int off;
+	unsigned short flags;
+	unsigned short subprog_idx;
+	bool is_merge_point;	/* EXEC restore target */
+	u8 restore_sreg;	/* SGPR to restore EXEC from at merge point */
+	int linear_idx;		/* position in the (reordered) emission list */
+	struct list_head l;
+};
+
+/* Encode one GPU instruction at @meta's running slot and advance it.
+ * @meta->amdgpu_insns is both the cursor during emission and the final
+ * instruction count afterwards.  @fn names a knod_amdgpu_insn.h encoder
+ * without its emit_ prefix (e.g. v_add32 for emit_v_add32); the macro
+ * pastes it back, so call sites read knod_emit(priv, meta, v_add32, ...).
+ */
+#define knod_emit(priv, meta, fn, ...)					\
+	do {								\
+		struct knod_insn_meta *__m = (meta);			\
+									\
+		emit_##fn((priv)->isa_version,				\
+			  &__m->amdgpu_insn[__m->amdgpu_insns],		\
+			  ##__VA_ARGS__);				\
+		debug_insn((priv)->isa_version,				\
+			   &__m->amdgpu_insn[__m->amdgpu_insns]);	\
+		__m->amdgpu_insns++;					\
+	} while (0)
+
+/* JIT debug/error trace: auto-prefix with "knod_jit <func>:<line>".
+ * knod_jit_dbg() is a pr_debug(), so it is off by default and toggled
+ * with dynamic debug; knod_jit_err() always fires.
+ */
+#define knod_jit_dbg(fmt, ...)						\
+	pr_debug("knod_jit %s:%d" fmt, __func__, __LINE__, ##__VA_ARGS__)
+#define knod_jit_err(fmt, ...)						\
+	pr_err("knod_jit %s:%d" fmt, __func__, __LINE__, ##__VA_ARGS__)
+
+#define BPF_SIZE_MASK   0x18
+
+struct knod_bb;		/* basic-block CFG analysis (knod_bpf.c) */
+
+struct knod_prog {
+	struct knod *knod;
+	struct knod_dev *knodev;
+
+	u64 *prog;
+	unsigned int prog_len;
+	unsigned int __prog_alloc_len;
+	int max_stack_off;
+	int max_packet_off;
+
+	struct knod_insn_meta *meta;
+	enum bpf_prog_type type;
+	struct list_head pre_insns;
+	struct list_head post_insns;
+	struct list_head insns;
+	unsigned int n_insns;
+	unsigned int pre_n_insns;
+	int insn_idx;
+
+	/* Structurized CFG state */
+	/* GFX9: 34, GFX10: 32 (s[32:33] safe on RDNA) */
+	u8 done_mask_sreg;
+	u8 exec_save_base;        /* GFX9: 36, GFX10: 34 */
+	/* in-bounds EXEC snapshot for verdict publish */
+	u8 initial_exec_sreg;
+	/* number of SGPR pairs allocated for EXEC saves */
+	u8 exec_save_pairs_used;
+	bool uses_adjust;
+
+	/* Basic-block CFG analysis, retained for the /bpf/cfg view. */
+	struct knod_bb *bbs;
+	int n_bbs;
+	int n_back;
+};
+
+#define KNOD_XDP_MEMCPY 0
+#define KNOD_XDP_PT	1
+#define KNOD_XDP_NETMEM	2
+#define KNOD_XDP_NONE	3
+#define KNOD_XDP_DEFAULT	KNOD_XDP_PT
+
+#define KNOD_LAT_BUCKETS 10
+#define KNOD_BL_BUCKETS  8
+
+struct knod_bpf_stats {
+	u64 dispatch_total_ns;
+	u64 dispatch_count;
+	u64 dispatch_max_ns;
+
+	u64 completion_total_ns;
+	u64 completion_count;
+	u64 completion_max_ns;
+	u64 completion_hist[KNOD_LAT_BUCKETS];
+
+	u64 backlogs_total;
+	u64 backlogs_hist[KNOD_BL_BUCKETS];
+
+	u64 decode_act_total_ns;
+	u64 decode_act_count;
+	u64 decode_act_max_ns;
+};
+
+#define KNOD_PASS_SLOT_SIZE	PAGE_SIZE
+
+/* pass_meta_buf slot header, written by the shader (offsetof used by the
+ * codegen).  The host read path is gone now that PASS delivery goes via the
+ * NIC act handler + knod_d2h_copy; the shader still stores {len, src_addr}
+ * here pending removal of that store.
+ */
+struct knod_pass_slot_hdr {
+	u32 len;		/* packet length */
+	u32 _pad;
+	u64 src_addr;		/* VRAM source address (SDMA mode only) */
+};
+
+struct knod_bpf_priv {
+	struct list_head list;
+	struct knod *knod;
+	struct knod_accel *accel;
+	struct knod_dev *knodev;
+	struct net_device *dev;
+	struct knod_prog *knod_prog;
+	/* retained pass IR for debugfs insn dump */
+	struct knod_prog *pass_knod_prog;
+	struct bpf_prog *prog;
+	struct amdgpu_vm *vm;
+	u64 queue_base_gaddr[KNOD_SPSC_MAX];
+	struct knod_bpf_work_sq *inflight[KNOD_BPF_INFLIGHT];
+	unsigned int inflight_cnt;
+	ktime_t next_dispatch_time;
+	struct task_struct *worker_task;
+	struct list_head free_list_sqw;
+	struct mutex map_op_lock;
+	/* maps awaiting deferred free by the worker */
+	struct list_head dead_maps;
+	u32 maps_tick_skip;
+	struct dentry *debug_dir;
+	struct knod_bpf_stats stats;
+	void *prog_buf;
+	void *pass_prog_buf;
+	u32 pass_prog_size;
+	/* descriptor + live shader bytes per kernel slot */
+	u32 kernel_image_len[2];
+	/* knod->kernels[] slot the GPU dispatches */
+	int active_idx;
+	/* knod->kernels[] slot holding the pass kernel */
+	int pass_idx;
+	/*
+	 * XDP_PASS shader-to-GTT metadata (shader-written; host read path
+	 * removed). GTT metadata: shader-written headers.
+	 */
+	struct knod_mem *pass_meta_buf;
+	u32 pass_pkts_per_queue;	/* backlogs / nr_works */
+	/* batch size per queue */
+	int batch_size;
+	int nr_works;
+	int isa_version;
+	bool installing_kernel;
+	int start;
+};
+
+static inline u8 mbpf_class(const struct knod_insn_meta *meta)
+{
+	return BPF_CLASS(meta->insn.code);
+}
+
+static inline u8 mbpf_src(const struct knod_insn_meta *meta)
+{
+	return BPF_SRC(meta->insn.code);
+}
+
+static inline u8 mbpf_op(const struct knod_insn_meta *meta)
+{
+	return BPF_OP(meta->insn.code);
+}
+
+static inline u8 mbpf_mode(const struct knod_insn_meta *meta)
+{
+	return BPF_MODE(meta->insn.code);
+}
+
+static inline bool is_mbpf_alu(const struct knod_insn_meta *meta)
+{
+	return mbpf_class(meta) == BPF_ALU64 || mbpf_class(meta) == BPF_ALU;
+}
+
+static inline bool is_mbpf_load(const struct knod_insn_meta *meta)
+{
+	return (meta->insn.code & ~BPF_SIZE_MASK) == (BPF_LDX | BPF_MEM);
+}
+
+static inline bool is_mbpf_jmp32(const struct knod_insn_meta *meta)
+{
+	return mbpf_class(meta) == BPF_JMP32;
+}
+
+static inline bool is_mbpf_jmp64(const struct knod_insn_meta *meta)
+{
+	return mbpf_class(meta) == BPF_JMP;
+}
+
+static inline bool is_mbpf_jmp(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_jmp32(meta) || is_mbpf_jmp64(meta);
+}
+
+static inline bool is_mbpf_store(const struct knod_insn_meta *meta)
+{
+	return (meta->insn.code & ~BPF_SIZE_MASK) == (BPF_STX | BPF_MEM);
+}
+
+static inline bool is_mbpf_load_pkt(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_load(meta) && meta->ptr.type == PTR_TO_PACKET;
+}
+
+static inline bool is_mbpf_store_pkt(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_store(meta) && meta->ptr.type == PTR_TO_PACKET;
+}
+
+static inline bool is_mbpf_classic_load(const struct knod_insn_meta *meta)
+{
+	u8 code = meta->insn.code;
+
+	return BPF_CLASS(code) == BPF_LD &&
+	       (BPF_MODE(code) == BPF_ABS || BPF_MODE(code) == BPF_IND);
+}
+
+static inline bool is_mbpf_classic_store(const struct knod_insn_meta *meta)
+{
+	u8 code = meta->insn.code;
+
+	return BPF_CLASS(code) == BPF_ST && BPF_MODE(code) == BPF_MEM;
+}
+
+static inline bool is_mbpf_classic_store_pkt(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_classic_store(meta) && meta->ptr.type == PTR_TO_PACKET;
+}
+
+static inline bool is_mbpf_atomic(const struct knod_insn_meta *meta)
+{
+	return (meta->insn.code & ~BPF_SIZE_MASK) == (BPF_STX | BPF_ATOMIC);
+}
+
+static inline bool is_mbpf_mul(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_alu(meta) && mbpf_op(meta) == BPF_MUL;
+}
+
+static inline bool is_mbpf_div(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_alu(meta) && mbpf_op(meta) == BPF_DIV;
+}
+
+static inline bool is_mbpf_mod(const struct knod_insn_meta *meta)
+{
+	return is_mbpf_alu(meta) && mbpf_op(meta) == BPF_MOD;
+}
+
+static inline bool is_mbpf_cond_jump(const struct knod_insn_meta *meta)
+{
+	u8 op;
+
+	if (is_mbpf_jmp32(meta))
+		return true;
+
+	if (!is_mbpf_jmp64(meta))
+		return false;
+
+	op = mbpf_op(meta);
+	return op != BPF_JA && op != BPF_EXIT && op != BPF_CALL;
+}
+
+static inline bool is_mbpf_helper_call(const struct knod_insn_meta *meta)
+{
+	struct bpf_insn insn = meta->insn;
+
+	return insn.code == (BPF_JMP | BPF_CALL) &&
+		insn.src_reg != BPF_PSEUDO_CALL;
+}
+
+static inline bool is_mbpf_pseudo_call(const struct knod_insn_meta *meta)
+{
+	struct bpf_insn insn = meta->insn;
+
+	return insn.code == (BPF_JMP | BPF_CALL) &&
+		insn.src_reg == BPF_PSEUDO_CALL;
+}
+
+static inline bool is_mbpf_map_call(const struct knod_insn_meta *meta)
+{
+	struct bpf_insn insn = meta->insn;
+
+	return insn.code == (BPF_JMP | BPF_CALL) && insn.imm <= 3;
+}
+
+#define STACK_FRAME_ALIGN 64
+
+#define FLAG_INSN_IS_JUMP_DST                   BIT(0)
+#define FLAG_INSN_IS_SUBPROG_START              BIT(1)
+#define FLAG_INSN_PTR_CALLER_STACK_FRAME        BIT(2)
+/* Instruction is pointless, noop even on its own */
+#define FLAG_INSN_SKIP_NOOP                     BIT(3)
+/* Instruction is optimized out based on preceding instructions */
+#define FLAG_INSN_SKIP_PREC_DEPENDENT           BIT(4)
+/* Instruction is optimized by the verifier */
+#define FLAG_INSN_SKIP_VERIFIER_OPT             BIT(5)
+/* Instruction needs to zero extend to high 32-bit */
+#define FLAG_INSN_DO_ZEXT                       BIT(6)
+
+#define FLAG_INSN_SKIP_MASK             (FLAG_INSN_SKIP_NOOP | \
+					 FLAG_INSN_SKIP_PREC_DEPENDENT | \
+					 FLAG_INSN_SKIP_VERIFIER_OPT)
+#endif
-- 
2.43.0


  parent reply	other threads:[~2026-07-19 18:01 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-19 17:58 [RFC PATCH net-next 00/13] net: knod: in-kernel network offload device Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 01/13] net: knod: add uapi and core headers Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 02/13] net: devmem: extend memory provider for knod Taehee Yoo
2026-07-20 19:43   ` Mina Almasry
2026-07-19 17:58 ` [RFC PATCH net-next 03/13] net: core: add XDP_MODE_HW offload hook " Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 04/13] net: knod: add offload device core and control plane Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 05/13] bpf: offload: allow PERCPU_ARRAY maps for offloaded programs Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 06/13] drm/amdkfd: prepare kfd core for the knod provider Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 07/13] drm/amdkfd: add knod provider core Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 08/13] drm/amdkfd: add GPU instruction emitter and disassembler Taehee Yoo
2026-07-20 20:05   ` Natalie Vock
2026-07-20 20:53     ` Andrew Lunn
2026-07-19 17:58 ` Taehee Yoo [this message]
2026-07-19 17:58 ` [RFC PATCH net-next 10/13] net/mlx5e: add knod XDP offload support Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 11/13] bnxt_en: " Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 12/13] selftests: drivers/net: add knod tests Taehee Yoo
2026-07-19 17:58 ` [RFC PATCH net-next 13/13] drm/amdkfd: add IPsec full-packet offload Taehee Yoo
2026-07-20 19:18 ` [RFC PATCH net-next 00/13] net: knod: in-kernel network offload device Mina Almasry

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260719175857.4071636-10-ap420073@gmail.com \
    --to=ap420073@gmail.com \
    --cc=Felix.Kuehling@amd.com \
    --cc=airlied@gmail.com \
    --cc=alexander.deucher@amd.com \
    --cc=amd-gfx@lists.freedesktop.org \
    --cc=andrew+netdev@lunn.ch \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=christian.koenig@amd.com \
    --cc=daniel@iogearbox.net \
    --cc=davem@davemloft.net \
    --cc=donald.hunter@gmail.com \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=eddyz87@gmail.com \
    --cc=edumazet@google.com \
    --cc=emil@etsalapatis.com \
    --cc=hawk@kernel.org \
    --cc=horms@kernel.org \
    --cc=hoyeon.rhee@gmail.com \
    --cc=ilias.apalodimas@linaro.org \
    --cc=john.fastabend@gmail.com \
    --cc=jolsa@kernel.org \
    --cc=justinstitt@google.com \
    --cc=kees@kernel.org \
    --cc=kuba@kernel.org \
    --cc=leon@kernel.org \
    --cc=linaro-mm-sig@lists.linaro.org \
    --cc=linux-hardening@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=linux-media@vger.kernel.org \
    --cc=linux-rdma@vger.kernel.org \
    --cc=llvm@lists.linux.dev \
    --cc=martin.lau@linux.dev \
    --cc=mbloch@nvidia.com \
    --cc=memxor@gmail.com \
    --cc=michael.chan@broadcom.com \
    --cc=morbo@google.com \
    --cc=nathan@kernel.org \
    --cc=ndesaulniers@google.com \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=pavan.chebbi@broadcom.com \
    --cc=saeedm@nvidia.com \
    --cc=sdf@fomichev.me \
    --cc=shuah@kernel.org \
    --cc=simona@ffwll.ch \
    --cc=song@kernel.org \
    --cc=sumit.semwal@linaro.org \
    --cc=tariqt@nvidia.com \
    --cc=yonghong.song@linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox