Background

I was setting up a fine-tuning pipeline for Foundation-Sec-8B on a Windows 11 machine with an RX 9070 XT using ROCm 7.14. The stack: PyTorch 2.12.0+rocm7.14.0, PEFT, standard HuggingFace Transformers. GPU correctly detected, VRAM visible, torch.cuda.is_available() returned True.

The first sign was the crash output in the terminal itself — the process printed a memory address alongside 0xC0000005 before dying. No Python exception was raised, no traceback. Just a raw AV and an exit.

This is the analysis of what caused it, what the wrong fix looked like, and the correct patch.


Environment

GPU AMD Radeon RX 9070 XT (gfx1201, RDNA 4)
OS Windows 11 (build 26100)
ROCm 7.14.0 (torch.version.hip = 7.14.60850)
PyTorch 2.12.0+rocm7.14.0
Driver Adrenalin 32.0.31041.1004
Issue ROCm/TheRock #7732

Reproducing the Crash

Any operation dispatching a HIP kernel crashes immediately:

1
2
import torch
t = torch.tensor([1.0, 2.0, 3.0]).cuda()  # process dies here

No exception is raised. The Python process exits with 0xC0000005.


WinDbg Analysis

Standard crash dump analysis shows the process is already dead at exit. To catch the access violation at its origin, attach WinDbg with child process tracking and break on first-chance AV:

windbg -o -G rocm_env\Scripts\python.exe finetune.py

At the initial break:

sxe av
g

The process breaks mid-execution with:

(1820.1f1c): Access violation - code c0000005 (first chance)
amdhip64_7!hipProfilerRegisterChunkCallbackExt+0x5c257:
00007ffd`4bf08bb7  mov rax, qword ptr [rcx+8]

r gives:

rcx = 7b2c450fc892ac84   ← not null — garbage pointer
rip = 00007ffd4bf08bb7

~* kn on thread 9:

# 00  amdhip64_7!hipProfilerRegisterChunkCallbackExt+0x5c257
# 01  amdhip64_7!hipRegisterTracerCallback+0x14a0e8
# 02  amdhip64_7!hipRegisterTracerCallback+0x135c16
# 03  amdhip64_7!hipRegisterTracerCallback+0x120ece
# 04  amdhip64_7!hipRegisterTracerCallback+0x10c296
# 05  amdhip64_7!hipRegisterTracerCallback+0xf93ce
# 06  amdhip64_7!hipLaunchKernel+0x88
# 07  torch_hip!at::native::...

The call chain: hipLaunchKernel+0x85 dispatches into the ROCprofiler callback registry. The registry walks a linked list. One node contains rcx = 7b2c450fc892ac84 — a garbage, non-null pointer. Dereferencing [rcx+8] produces the AV.


Root Cause

hipLaunchKernel calls into the ROCprofiler callback notification chain on every kernel dispatch via an indirect call at +0x85:

1
2
3
4
; hipLaunchKernel+0x82:
48 8b ce        mov    rcx, rsi
41 ff d2        call   r10        ; ← dispatch to profiler chain
eb 04           jmp    +4

r10 holds a function pointer to the callback notification wrapper. Inside that chain, hipProfilerRegisterChunkCallbackExt walks a linked list of registered profiler callbacks. On Windows gfx1201, this list contains an uninitialized or corrupted node — likely a race condition during runtime initialization on first kernel dispatch.

The linked list walk:

1
2
3
4
5
6
; hipProfilerRegisterChunkCallbackExt+0x5c257:
48 8b 41 08     mov rax, [rcx+8]    ; rcx = 7b2c450fc892ac84 → CRASH
49 3b c0        cmp rax, r8
74 21           je  (not found)
48 8b 09        mov rcx, [rcx]      ; next node
...

First Patch — Wrong

The obvious fix: NOP out the call r10 at hipLaunchKernel+0x85.

File offset 0x4549B5:
  Before: 41 FF D2   (call r10)
  After:  90 90 90   (NOP)

The crash stopped. Training appeared to run. GPU showed 100% compute in Task Manager.

But something was wrong. Checking model weights after loading:

1
2
3
emb = model.model.embed_tokens.weight
print(emb.float().abs().max().item())
# 0.0

Every tensor transferred to GPU was zero:

1
2
3
t = torch.tensor([0.265625], dtype=torch.bfloat16)
print(t.cuda())
# tensor([0.], device='cuda:0')

The NOP stopped the crash but also prevented actual kernel execution. Looking at the context after the patched call:

1
2
3
4
0x4549b5: 90 90 90        NOP NOP NOP        patched
0x4549b8: eb 04           jmp +4             jumps to epilogue
0x4549ba: 8b 44 24 40     mov eax, [rsp+40]
0x4549be: 48 8b 8c 24 ... ; ← function epilogue

Without the call r10, execution falls through to jmp +4 which jumps directly to the function epilogue. hipLaunchKernel returns without ever dispatching the kernel. GPU memory allocations succeed (DMA path), but all compute kernels are silently skipped — leaving tensors at their initial zero state.

Loss during training: 0.000, nan, -8.5e+36. The model was “training” on all-zero tensors.


Correct Patch

Instead of patching the call site in hipLaunchKernel, patch the crashing function itself. hipProfilerRegisterChunkCallbackExt is a callback lookup — it should return 0 (not found) when the list is invalid. Making it immediately return 0 is safe: the profiler system simply finds no registered callbacks and skips notification. Kernel dispatch proceeds normally.

Locate the export via the PE export table:

hipProfilerRegisterChunkCallbackExt  RVA=0x4BC960  FileOffset=0x4BBD60

Patch the function prologue:

File offset 0x4BBD60:
  Before: 48 89 5c ...   (push rbx; function prologue)
  After:  33 c0 c3       (xor eax, eax; ret  →  return 0)

Verification:

1
2
3
4
5
6
7
8
t = torch.tensor([0.265625, -0.1, 0.5], dtype=torch.bfloat16)
print(t.cuda())
# tensor([ 0.2656, -0.1001,  0.5000], device='cuda:0')

model = AutoModelForCausalLM.from_pretrained('./foundation-sec-8b',
    torch_dtype=torch.bfloat16, device_map={'': 0})
print(model.model.embed_tokens.weight.float().abs().max().item())
# 0.265625

Patch Script

GitHub: JM00NJ/amdhip64-hipLaunchKernel-crash-fix

The script locates hipProfilerRegisterChunkCallbackExt dynamically via the PE export table (no hardcoded offset dependency), creates a backup, and reverts the old wrong patch automatically if present.

1
python patch_amdhip64.py "rocm_env\Lib\site-packages\_rocm_sdk_core\bin\amdhip64_7.dll"

Fine-Tuning Script

With the patch applied, standard BF16 training works. The script below fine-tunes Foundation-Sec-8B with LoRA. Modify the variables at the top for your setup — everything else can stay as-is.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""
Foundation-Sec-8B LoRA v8
Clean version — DLL correctly patched, standard BF16 training
No FP32LossTrainer hack, no manual label shift, no FP32 LoRA cast
"""

import os, json, math
import torch
import torch.nn.functional as F
from datasets import Dataset
from transformers import (
    AutoModelForCausalLM, AutoTokenizer,
    Trainer, TrainingArguments,
    DataCollatorForLanguageModeling,
)
from peft import LoraConfig, get_peft_model, TaskType

MODEL_PATH   = r".\foundation-sec-8b"
DATASET_PATH = r".\final_dataset.jsonl"
OUTPUT_DIR   = r".\finetuned-model"

LORA_R         = 8
LORA_ALPHA     = 16
LORA_DROPOUT   = 0.0
TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj",
                  "gate_proj", "up_proj", "down_proj"]

BATCH_SIZE    = 1
GRAD_ACCUM    = 8
LEARNING_RATE = 2e-5
NUM_EPOCHS    = 3
MAX_SEQ_LEN   = 1024
MAX_GRAD_NORM = 1.0
SAVE_STEPS    = 50
LOGGING_STEPS = 5


def load_dataset_from_jsonl(path):
    records = []
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                records.append(json.loads(line))
    print(f"[+] {len(records)} entries loaded")
    return Dataset.from_list(records)


def format_alpaca(example):
    msgs = example["messages"]
    parts = []
    system_msg = next((m["content"] for m in msgs if m["role"] == "system"), None)
    if system_msg:
        parts.append(f"### System:\n{system_msg}")
    for msg in msgs:
        if msg["role"] == "user":
            parts.append(f"### User:\n{msg['content']}")
        elif msg["role"] == "assistant":
            parts.append(f"### Assistant:\n{msg['content']}")
    return {"text": "\n\n".join(parts) + "\n"}


def main():
    print("=" * 60)
    print("  Foundation-Sec-8B LoRA v8 — JM00NJ")
    print("  Standard BF16 training (DLL patched correctly)")
    print("=" * 60)

    if not torch.cuda.is_available():
        print("[!] No GPU found!"); return

    print(f"[+] GPU  : {torch.cuda.get_device_name(0)}")
    print(f"[+] VRAM : {torch.cuda.get_device_properties(0).total_memory/1e9:.1f} GB")

    # Dataset
    print(f"\n[1/4] Dataset: {DATASET_PATH}")
    raw = load_dataset_from_jsonl(DATASET_PATH)
    text_dataset = raw.map(format_alpaca, remove_columns=raw.column_names)

    # Tokenizer
    print(f"[2/4] Tokenizer: {MODEL_PATH}")
    tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"

    def tokenize(example):
        return tokenizer(example["text"], truncation=True,
                         max_length=MAX_SEQ_LEN, padding=False)

    print("[+] Tokenizing...")
    tokenized = text_dataset.map(tokenize, remove_columns=["text"], desc="Tokenizing")
    tokenized.set_format("torch")
    print(f"[+] {len(tokenized)} examples ready")

    # Model — BF16, DLL now correctly patched
    print(f"\n[3/4] Loading model (BF16)...")
    model = AutoModelForCausalLM.from_pretrained(
        MODEL_PATH, dtype=torch.bfloat16, device_map={"": 0},
        trust_remote_code=True, low_cpu_mem_usage=True,
        attn_implementation="eager",
    )
    print(f"[+] VRAM: {torch.cuda.memory_allocated(0)/1e9:.1f} GB")

    # LoRA — stay in BF16, no FP32 cast
    model = get_peft_model(model, LoraConfig(
        r=LORA_R, lora_alpha=LORA_ALPHA, target_modules=TARGET_MODULES,
        lora_dropout=LORA_DROPOUT, bias="none",
        task_type=TaskType.CAUSAL_LM, init_lora_weights=True,
    ))
    model.print_trainable_parameters()
    model.gradient_checkpointing_enable(
        gradient_checkpointing_kwargs={"use_reentrant": False})
    model.enable_input_require_grads()

    # Sanity check
    print("\n[*] Sanity check...")
    model.eval()
    inp = tokenized[0]["input_ids"].unsqueeze(0).to("cuda")
    with torch.no_grad():
        out = model(input_ids=inp, labels=inp)
    print(f"    Loss (model internal): {out.loss.item():.4f}  (expected 1.5-4.0)")
    if out.loss.item() < 0.1:
        print("    [!] WARNING: near zero")
    model.train()

    # Training — standard Trainer, bf16=True
    total = (len(tokenized) // (BATCH_SIZE * GRAD_ACCUM)) * NUM_EPOCHS
    print(f"\n[4/4] Training...  steps:~{total}\n")

    trainer = Trainer(
        model=model,
        args=TrainingArguments(
            output_dir=OUTPUT_DIR,
            num_train_epochs=NUM_EPOCHS,
            per_device_train_batch_size=BATCH_SIZE,
            gradient_accumulation_steps=GRAD_ACCUM,
            learning_rate=LEARNING_RATE,
            fp16=False,
            bf16=True,         # standard BF16, DLL is now fixed
            max_grad_norm=MAX_GRAD_NORM,
            warmup_steps=10,
            logging_steps=LOGGING_STEPS,
            save_steps=SAVE_STEPS, save_total_limit=3,
            report_to="none", dataloader_num_workers=0,
            optim="adamw_torch", remove_unused_columns=False,
            lr_scheduler_type="cosine",
        ),
        train_dataset=tokenized,
        data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False),
    )

    try:
        trainer.train()
        print("\n[+] Training complete!")
    except torch.cuda.OutOfMemoryError:
        print("\n[!] OOM"); return
    except Exception as ex:
        print(f"\n[!] Error: {ex}")
        import traceback; traceback.print_exc(); return

    adapter_path = os.path.join(OUTPUT_DIR, "final-adapter")
    model.save_pretrained(adapter_path)
    tokenizer.save_pretrained(adapter_path)
    print(f"[+] Adapter saved: {adapter_path}")

    print("\n[*] Test...")
    model.eval()
    prompt = ("### User:\nI found a NULL pointer dereference in a Windows kernel "
              "driver at DISPATCH_LEVEL. What should I investigate next?\n\n"
              "### Assistant:\n")
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
    with torch.no_grad():
        out = model.generate(**inputs, max_new_tokens=200, temperature=0.7,
                             do_sample=True, pad_token_id=tokenizer.eos_token_id)
    print(tokenizer.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True))
    print("\nDone!")


if __name__ == "__main__":
    main()

Notes

  • Re-run the patcher after ROCm updates — the DLL will be overwritten
  • The patch disables ROCprofiler tracing. rocprof will not intercept HIP API calls on the patched binary. For profiling, wait for the upstream fix
  • Reported to AMD: ROCm/TheRock#7732, ROCm/rocm-systems#10924