pinokiod 7.5.13 → 7.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/kernel/gpu/amd.js +30 -43
  2. package/kernel/gpu/amd_gfx_targets.json +67 -0
  3. package/kernel/gpu/nvidia.js +90 -4
  4. package/kernel/index.js +4 -3
  5. package/kernel/sysinfo.js +16 -25
  6. package/package.json +2 -1
  7. package/script/update-amd-gfx-targets.js +119 -0
  8. package/server/lib/app_log_report.js +1 -2
  9. package/server/public/logs.js +230 -30
  10. package/server/public/style.css +28 -12
  11. package/server/views/index.ejs +7 -0
  12. package/test/amd-gpu-target.test.js +169 -0
  13. package/test/gpu-target-template-variable.test.js +69 -0
  14. package/test/logs-ask-ai.test.js +65 -10
  15. package/test/nvidia-gpu-target.test.js +98 -0
  16. package/test/sysinfo-gpu-driver.test.js +134 -1
  17. package/kernel/gpu/apple.js +0 -8
  18. package/kernel/gpu/intel.js +0 -47
  19. package/release_notes/8.0.0/assets/app-ask-ai-drawer.png +0 -0
  20. package/release_notes/8.0.0/assets/app-detail-header-actions.png +0 -0
  21. package/release_notes/8.0.0/assets/autolaunch-dependencies.png +0 -0
  22. package/release_notes/8.0.0/assets/connect-huggingface-login.png +0 -0
  23. package/release_notes/8.0.0/assets/home-app-actions.png +0 -0
  24. package/release_notes/8.0.0/assets/home-sidebar-apps.png +0 -0
  25. package/release_notes/8.0.0/assets/logs-terminal-reporting.png +0 -0
  26. package/release_notes/8.0.0/assets/mobile-home-footer-nav.png +0 -0
  27. package/release_notes/8.0.0/assets/network-phone-access.png +0 -0
  28. package/release_notes/8.0.0/assets/plugins-agent-tools.png +0 -0
  29. package/release_notes/8.0.0/assets/skills-management.png +0 -0
  30. package/release_notes/8.0.0/assets/tools-runtime-management.png +0 -0
  31. package/release_notes/8.0.0/ui.md +0 -91
package/kernel/gpu/amd.js CHANGED
@@ -1,4 +1,19 @@
1
1
  const { normalize_model } = require("./common")
2
+ const amd_gfx_targets = require("./amd_gfx_targets.json")
3
+
4
+ const canonical_model_key = (model) => {
5
+ let key = normalize_model(model)
6
+ let previous
7
+ do {
8
+ previous = key
9
+ key = key
10
+ .replace(/^advanced micro devices\s+/, "")
11
+ .replace(/^amd\s+/, "")
12
+ .replace(/^instinct\s+/, "")
13
+ .trim()
14
+ } while (key !== previous)
15
+ return key
16
+ }
2
17
 
3
18
  // Detect uninformative AMD APU names that need CPU-brand fallback matching.
4
19
  const is_generic_gpu_model = (model) => {
@@ -15,58 +30,30 @@ const resolve_cpu_brand = async (cpu_brand) => {
15
30
  }
16
31
  }
17
32
 
18
- // Match AMD GPU model names that Pinokio should route to PyTorch ROCm wheels.
19
- const matches_rocm_torch_model = (model) => {
20
- let normalized = normalize_model(model)
21
-
22
- // AMD PyTorch ROCm install policy. These checks are based on the
23
- // hardware families Pinokio should route to ROCm-flavored PyTorch wheels,
24
- // not on whether ROCm is already installed on the machine.
25
-
26
- // Radeon RX 7600+:
27
- // - RX 7600 / 7600 XT / 7600M / 7600M XT
28
- // - RX 7700 / 7800 / 7900 families
29
- // - RX 9000 families such as RX 9060 and RX 9070
30
- // - Future RX 8xxx/9xxx names are treated as ROCm-intended by policy.
31
- let rx = normalized.match(/\brx\s*([7-9]\d{3})(?!\d)/)
32
- if (rx && Number(rx[1]) >= 7600) {
33
- return true
33
+ const resolve_rocm_gfx_target = (model) => {
34
+ let key = canonical_model_key(model)
35
+ if (/^gfx[0-9a-f]+$/i.test(key)) {
36
+ return key.toLowerCase()
34
37
  }
35
-
36
- // Radeon PRO W7700+:
37
- // - PRO W7700 / W7800 / W7900
38
- // - Future PRO W 8xxx/9xxx names are treated as ROCm-intended by policy.
39
- let pro = normalized.match(/\bpro\s*w\s*([7-9]\d{3})(?!\d)/)
40
- if (pro && Number(pro[1]) >= 7700) {
41
- return true
38
+ if (amd_gfx_targets.entries[key]) {
39
+ return amd_gfx_targets.entries[key]
42
40
  }
43
41
 
44
- return (
45
- // Radeon PRO V710
46
- /\bpro\s*v\s*710\b/.test(normalized) ||
47
- // Radeon AI PRO R9600/R9700 variants, including suffixes like R9700S.
48
- /\bai\s*pro\s*r\s*9[67]\d{2}[a-z]?\b/.test(normalized) ||
49
- // Supported APUs and codenames seen in PyTorch ROCm wheel support.
50
- /\b(780m|820m|880m|890m|8050s|8060s|strix|phoenix|fire range)\b/.test(normalized)
51
- )
42
+ return null
52
43
  }
53
44
 
54
- // Decide AMD ROCm install intent, using CPU brand only for generic APU GPU names.
55
- const supports_torch_backend = async (model, cpu_brand) => {
56
- if (matches_rocm_torch_model(model)) {
57
- return true
45
+ const resolve_gpu_target = async (model, cpu_brand) => {
46
+ let target = resolve_rocm_gfx_target(model)
47
+ if (target) {
48
+ return target
58
49
  }
59
50
  if (!is_generic_gpu_model(model)) {
60
- return false
51
+ return null
61
52
  }
62
- // APU-only systems can report the GPU as just "AMD Radeon Graphics".
63
- // In that generic case, use the CPU brand as a fallback because it often
64
- // includes the Radeon model or codename, such as 8060S or Strix Halo.
65
- return matches_rocm_torch_model(await resolve_cpu_brand(cpu_brand))
53
+ return resolve_rocm_gfx_target(await resolve_cpu_brand(cpu_brand))
66
54
  }
67
55
 
68
56
  module.exports = {
69
- is_generic_gpu_model,
70
- matches_rocm_torch_model,
71
- supports_torch_backend
57
+ resolve_gpu_target,
58
+ resolve_rocm_gfx_target
72
59
  }
@@ -0,0 +1,67 @@
1
+ {
2
+ "source": "https://raw.githubusercontent.com/ROCm/ROCm/develop/docs/reference/gpu-arch-specs.rst",
3
+ "generated_by": "script/update-amd-gfx-targets.js",
4
+ "entries": {
5
+ "mi100": "gfx908",
6
+ "mi210": "gfx90a",
7
+ "mi25": "gfx900",
8
+ "mi250": "gfx90a",
9
+ "mi250x": "gfx90a",
10
+ "mi300a": "gfx942",
11
+ "mi300x": "gfx942",
12
+ "mi325x": "gfx942",
13
+ "mi350x": "gfx950",
14
+ "mi355x": "gfx950",
15
+ "mi50 16gb": "gfx906",
16
+ "mi50 32gb": "gfx906",
17
+ "mi6": "gfx803",
18
+ "mi60": "gfx906",
19
+ "mi8": "gfx803",
20
+ "radeon 780m": "gfx1103",
21
+ "radeon 8060s": "gfx1151",
22
+ "radeon 860m": "gfx1152",
23
+ "radeon 890m": "gfx1150",
24
+ "radeon ai pro r9600d": "gfx1201",
25
+ "radeon ai pro r9700": "gfx1201",
26
+ "radeon pro v620": "gfx1030",
27
+ "radeon pro v710": "gfx1101",
28
+ "radeon pro vii": "gfx906",
29
+ "radeon pro w5500": "gfx1012",
30
+ "radeon pro w6600": "gfx1032",
31
+ "radeon pro w6800": "gfx1030",
32
+ "radeon pro w7700": "gfx1101",
33
+ "radeon pro w7800": "gfx1100",
34
+ "radeon pro w7800 48gb": "gfx1100",
35
+ "radeon pro w7900": "gfx1100",
36
+ "radeon pro w7900 dual slot": "gfx1100",
37
+ "radeon rx 6600": "gfx1032",
38
+ "radeon rx 6600 xt": "gfx1032",
39
+ "radeon rx 6650 xt": "gfx1032",
40
+ "radeon rx 6700": "gfx1031",
41
+ "radeon rx 6700 xt": "gfx1031",
42
+ "radeon rx 6750 xt": "gfx1031",
43
+ "radeon rx 6800": "gfx1030",
44
+ "radeon rx 6800 xt": "gfx1030",
45
+ "radeon rx 6900 xt": "gfx1030",
46
+ "radeon rx 6950 xt": "gfx1030",
47
+ "radeon rx 7600": "gfx1102",
48
+ "radeon rx 7700": "gfx1101",
49
+ "radeon rx 7700 xt": "gfx1101",
50
+ "radeon rx 7800 xt": "gfx1101",
51
+ "radeon rx 7900 gre": "gfx1100",
52
+ "radeon rx 7900 xt": "gfx1100",
53
+ "radeon rx 7900 xtx": "gfx1100",
54
+ "radeon rx 9060": "gfx1200",
55
+ "radeon rx 9060 xt": "gfx1200",
56
+ "radeon rx 9060 xt lp": "gfx1200",
57
+ "radeon rx 9070": "gfx1201",
58
+ "radeon rx 9070 gre": "gfx1201",
59
+ "radeon rx 9070 xt": "gfx1201",
60
+ "radeon vii": "gfx906",
61
+ "ryzen 7 7840u": "gfx1103",
62
+ "ryzen 9 270": "gfx1103",
63
+ "ryzen ai 9 hx 375": "gfx1150",
64
+ "ryzen ai max pro 395": "gfx1151",
65
+ "ryzen al 7 350": "gfx1152"
66
+ }
67
+ }
@@ -1,8 +1,94 @@
1
- // NVIDIA routes to CUDA PyTorch wheels on supported non-macOS platforms.
2
- const supports_torch_backend = (controller, platform) => {
3
- return !!controller && platform !== "darwin"
1
+ const { execFile } = require("node:child_process")
2
+
3
+ const NVIDIA_SMI_COMPUTE_CAP_ARGS = [
4
+ "--query-gpu=pci.bus_id,compute_cap",
5
+ "--format=csv,noheader,nounits"
6
+ ]
7
+
8
+ let cuda_sm_targets_promise = null
9
+
10
+ const normalize_pci_bus = (bus) => {
11
+ let normalized = String(bus || "").trim().toLowerCase()
12
+ let match = /(?:[0-9a-f]{4,8}:)?([0-9a-f]{2}:[0-9a-f]{2}\.[0-7])$/.exec(normalized)
13
+ return match ? match[1] : normalized
14
+ }
15
+
16
+ const compute_cap_to_sm_target = (compute_cap) => {
17
+ let match = /^(\d+)\.(\d+)$/.exec(String(compute_cap || "").trim())
18
+ return match ? `sm_${match[1]}${match[2]}` : null
19
+ }
20
+
21
+ const parse_nvidia_smi_compute_caps = (stdout) => {
22
+ return String(stdout || "")
23
+ .split(/\r?\n/)
24
+ .map((line) => line.trim())
25
+ .filter(Boolean)
26
+ .map((line) => {
27
+ let parts = line.split(",").map((part) => part.trim())
28
+ if (parts.length < 2) return null
29
+ let target = compute_cap_to_sm_target(parts[1])
30
+ if (!target) return null
31
+ return {
32
+ pci_bus: normalize_pci_bus(parts[0]),
33
+ target
34
+ }
35
+ })
36
+ .filter(Boolean)
37
+ }
38
+
39
+ const query_cuda_sm_targets = (exec_file = execFile) => {
40
+ return new Promise((resolve) => {
41
+ exec_file(
42
+ "nvidia-smi",
43
+ NVIDIA_SMI_COMPUTE_CAP_ARGS,
44
+ { windowsHide: true, timeout: 5000 },
45
+ (error, stdout) => {
46
+ if (error) {
47
+ resolve([])
48
+ } else {
49
+ resolve(parse_nvidia_smi_compute_caps(stdout))
50
+ }
51
+ }
52
+ )
53
+ })
54
+ }
55
+
56
+ const cached_cuda_sm_targets = () => {
57
+ if (!cuda_sm_targets_promise) {
58
+ cuda_sm_targets_promise = query_cuda_sm_targets()
59
+ }
60
+ return cuda_sm_targets_promise
61
+ }
62
+
63
+ const select_cuda_sm_target = (controller, records) => {
64
+ let targets = Array.isArray(records) ? records : []
65
+ if (targets.length === 0) {
66
+ return null
67
+ }
68
+
69
+ let controller_bus = normalize_pci_bus(controller && (controller.pciBus || controller.busAddress))
70
+ if (controller_bus) {
71
+ let match = targets.find((record) => record.pci_bus === controller_bus)
72
+ if (match) {
73
+ return match.target
74
+ }
75
+ }
76
+
77
+ return targets.length === 1 ? targets[0].target : null
78
+ }
79
+
80
+ const resolve_cuda_sm_target = async (controller) => {
81
+ if (!controller) {
82
+ return null
83
+ }
84
+ let records = await cached_cuda_sm_targets()
85
+ return select_cuda_sm_target(controller, records)
4
86
  }
5
87
 
6
88
  module.exports = {
7
- supports_torch_backend
89
+ compute_cap_to_sm_target,
90
+ parse_nvidia_smi_compute_caps,
91
+ query_cuda_sm_targets,
92
+ resolve_cuda_sm_target,
93
+ select_cuda_sm_target
8
94
  }
package/kernel/index.js CHANGED
@@ -92,10 +92,10 @@ class Kernel {
92
92
  this.pinokio_configs = {}
93
93
  this.shellpath = shellPath.sync()
94
94
  this.favicon = new Favicon()
95
- this.torch_backend = "cpu"
96
95
  this.vram = 0
97
96
  this.ram = 0
98
97
  this.gpu_driver = null
98
+ this.gpu_target = null
99
99
  this.readyState = new ReadyState(this)
100
100
  this.app_ready_status = this.readyState.status
101
101
  this.launchRequirements = new LaunchRequirements(this)
@@ -824,6 +824,7 @@ class Kernel {
824
824
  procs: this.procs,
825
825
  gpu: this.gpu,
826
826
  gpus: this.gpus,
827
+ gpu_target: this.gpu_target,
827
828
  ...info
828
829
  }
829
830
 
@@ -1152,7 +1153,7 @@ class Kernel {
1152
1153
  system,
1153
1154
  platform: this.platform,
1154
1155
  arch: this.arch,
1155
- torch_backend: this.torch_backend,
1156
+ gpu_target: this.gpu_target,
1156
1157
  vram: this.vram,
1157
1158
  ram: this.ram,
1158
1159
  proxy: (port) => {
@@ -1349,7 +1350,7 @@ class Kernel {
1349
1350
  this.gpu = info.gpu
1350
1351
  this.gpu_model = info.gpu_model
1351
1352
  this.gpu_driver = info.gpu_driver || null
1352
- this.torch_backend = info.torch_backend || "cpu"
1353
+ this.gpu_target = info.gpu_target || null
1353
1354
  this.gpus = info.gpus
1354
1355
  this.vram = typeof info.vram === "number" ? info.vram : 0
1355
1356
  this.ram = typeof info.ram === "number" ? info.ram : 0
package/kernel/sysinfo.js CHANGED
@@ -3,8 +3,6 @@ const fs = require('fs')
3
3
  const path = require('path')
4
4
  const nvidia = require("./gpu/nvidia")
5
5
  const amd = require("./gpu/amd")
6
- const apple = require("./gpu/apple")
7
- const intel = require("./gpu/intel")
8
6
  class Sysinfo {
9
7
  async init(kernel) {
10
8
  this.kernel = kernel
@@ -74,7 +72,6 @@ class Sysinfo {
74
72
  let is_nvidia = controllers.find(x => /nvidia/i.test(x.vendor || ""))
75
73
  let is_amd = bestByVram(controllers.filter(x => /(amd|advanced micro devices)/i.test(x.vendor || "")))
76
74
  let is_apple = controllers.find(x => /apple/i.test(x.vendor || ""))
77
- let is_intel = controllers.find(x => /intel/i.test(x.vendor || ""))
78
75
 
79
76
  let gpu
80
77
  let gpu_model
@@ -108,14 +105,22 @@ class Sysinfo {
108
105
  let vramMB = primaryController && primaryController.vram ? primaryController.vram : 0
109
106
  let vramGB = vramMB > 0 ? Math.round(vramMB / 1024) : 0
110
107
  let gpu_driver = driver(primaryController)
111
- let torch_backend = await this.torch_backend({ is_nvidia, is_amd, is_apple, is_intel, gpu_model })
108
+ let cpu_brand_value
109
+ let cpu_brand = async () => {
110
+ if (cpu_brand_value === undefined) {
111
+ cpu_brand_value = await this.cpu_brand()
112
+ }
113
+ return cpu_brand_value
114
+ }
115
+ let detected = { is_nvidia, is_amd, gpu_model, primaryController, cpu_brand }
116
+ let gpu_target = await this.gpu_target(detected)
112
117
 
113
118
  this.info.graphics = g
114
119
  this.info.gpus = gpus
115
120
  this.info.gpu = gpu
116
121
  this.info.gpu_model = gpu_model
117
122
  this.info.gpu_driver = gpu_driver
118
- this.info.torch_backend = torch_backend
123
+ this.info.gpu_target = gpu_target
119
124
  this.info.vram = vramGB
120
125
  }
121
126
  // Read CPU brand only when generic iGPU names need CPU fallback matching.
@@ -130,27 +135,13 @@ class Sysinfo {
130
135
  }
131
136
  return cpu && cpu.brand
132
137
  }
133
- // Select the PyTorch backend install target from detected hardware identity.
134
- async torch_backend(detected) {
135
- // Do not require runtime probes such as rocminfo, hipInfo, ze_info, or
136
- // an existing torch install here.
137
- if (nvidia.supports_torch_backend(detected.is_nvidia, process.platform)) {
138
- return "cuda"
139
- } else if (apple.supports_torch_backend(detected.is_apple, process.platform)) {
140
- return "mps"
141
- } else if (
142
- detected.is_amd &&
143
- process.platform !== "darwin" &&
144
- await amd.supports_torch_backend(detected.gpu_model, () => this.cpu_brand())
145
- ) {
146
- return "rocm"
147
- } else if (
148
- detected.is_intel &&
149
- await intel.supports_torch_backend(detected.is_intel.model, () => this.cpu_brand())
150
- ) {
151
- return "xpu"
138
+ async gpu_target(detected) {
139
+ if (detected.is_nvidia) {
140
+ return await nvidia.resolve_cuda_sm_target(detected.primaryController)
141
+ } else if (detected.is_amd) {
142
+ return await amd.resolve_gpu_target(detected.gpu_model, detected.cpu_brand || (() => this.cpu_brand()))
152
143
  } else {
153
- return "cpu"
144
+ return null
154
145
  }
155
146
  }
156
147
  // async time() {
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.5.13",
3
+ "version": "7.5.15",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "download_readme": "wget -O kernel/proto/PINOKIO.md https://raw.githubusercontent.com/pinokiocomputer/home/refs/heads/main/docs/README.md",
8
+ "update:amd-gfx-targets": "node script/update-amd-gfx-targets.js",
8
9
  "start": "node script/index"
9
10
  },
10
11
  "author": "",
@@ -0,0 +1,119 @@
1
+ const fs = require('node:fs')
2
+ const https = require('node:https')
3
+ const path = require('node:path')
4
+
5
+ const { normalize_model } = require('../kernel/gpu/common')
6
+
7
+ const GPU_SPECS_URL = 'https://raw.githubusercontent.com/ROCm/ROCm/develop/docs/reference/gpu-arch-specs.rst'
8
+ const GENERATOR = 'script/update-amd-gfx-targets.js'
9
+
10
+ const repoRoot = path.resolve(__dirname, '..')
11
+
12
+ function fetchText(url) {
13
+ return new Promise((resolve, reject) => {
14
+ https.get(url, (response) => {
15
+ if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
16
+ const nextUrl = new URL(response.headers.location, url).toString()
17
+ response.resume()
18
+ fetchText(nextUrl).then(resolve, reject)
19
+ return
20
+ }
21
+ if (response.statusCode !== 200) {
22
+ response.resume()
23
+ reject(new Error(`${url} returned ${response.statusCode}`))
24
+ return
25
+ }
26
+ let body = ''
27
+ response.setEncoding('utf8')
28
+ response.on('data', (chunk) => {
29
+ body += chunk
30
+ })
31
+ response.on('end', () => resolve(body))
32
+ }).on('error', reject)
33
+ })
34
+ }
35
+
36
+ function canonicalKey(value) {
37
+ let key = normalize_model(value)
38
+ let previous
39
+ do {
40
+ previous = key
41
+ key = key
42
+ .replace(/^advanced micro devices\s+/, '')
43
+ .replace(/^amd\s+/, '')
44
+ .replace(/^instinct\s+/, '')
45
+ .trim()
46
+ } while (key !== previous)
47
+ return key
48
+ }
49
+
50
+ function addEntry(entries, name, target) {
51
+ const key = canonicalKey(name)
52
+ if (key && /^gfx[0-9a-f]+$/i.test(target)) {
53
+ entries[key] = target.toLowerCase()
54
+ }
55
+ }
56
+
57
+ function parseGpuSpecs(rst) {
58
+ const entries = {}
59
+ let headers = null
60
+ let cells = []
61
+ let inRow = false
62
+
63
+ const flushRow = () => {
64
+ if (!inRow || cells.length === 0) return
65
+ if (cells.includes('Name') && cells.includes('LLVM target name')) {
66
+ headers = cells
67
+ } else if (headers) {
68
+ const nameIndex = headers.indexOf('Name')
69
+ const graphicsIndex = headers.indexOf('Graphics model')
70
+ const targetIndex = headers.indexOf('LLVM target name')
71
+ const target = cells[targetIndex]
72
+ if (target) {
73
+ addEntry(entries, cells[nameIndex], target)
74
+ if (graphicsIndex >= 0) {
75
+ addEntry(entries, cells[graphicsIndex], target)
76
+ }
77
+ }
78
+ }
79
+ cells = []
80
+ }
81
+
82
+ for (const line of rst.split(/\r?\n/)) {
83
+ if (/^\s*\*\s*$/.test(line)) {
84
+ flushRow()
85
+ inRow = true
86
+ continue
87
+ }
88
+ const cell = line.match(/^\s*-\s*(.*)$/)
89
+ if (inRow && cell) {
90
+ cells.push(cell[1].trim())
91
+ }
92
+ }
93
+ flushRow()
94
+
95
+ return entries
96
+ }
97
+
98
+ function writeJson(relativePath, data) {
99
+ const file = path.join(repoRoot, relativePath)
100
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
101
+ }
102
+
103
+ async function main() {
104
+ const specs = await fetchText(GPU_SPECS_URL)
105
+ const entries = parseGpuSpecs(specs)
106
+
107
+ writeJson('kernel/gpu/amd_gfx_targets.json', {
108
+ source: GPU_SPECS_URL,
109
+ generated_by: GENERATOR,
110
+ entries: Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)))
111
+ })
112
+
113
+ console.log(`Wrote ${Object.keys(entries).length} AMD gfx target entries`)
114
+ }
115
+
116
+ main().catch((error) => {
117
+ console.error(error)
118
+ process.exit(1)
119
+ })
@@ -358,8 +358,7 @@ class AppLogReportService {
358
358
  version: this.readPinokioVersion(),
359
359
  node: process.version,
360
360
  platform: kernel.platform || process.platform,
361
- arch: kernel.arch || process.arch,
362
- torch_backend: kernel.torch_backend || info.torch_backend || null
361
+ arch: kernel.arch || process.arch
363
362
  },
364
363
  hardware: this.compactObject({
365
364
  gpu: kernel.gpu || info.gpu || null,