You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_tracing.py 19 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. # -*- coding: utf-8 -*-
  2. import inspect
  3. import io
  4. import itertools
  5. import random
  6. from tempfile import mkstemp
  7. import numpy as np
  8. import pytest
  9. import megengine.core.tensor.megbrain_graph as G
  10. import megengine.functional as F
  11. import megengine.optimizer as optim
  12. import megengine.utils.comp_graph_tools as cgtools
  13. from megengine import Parameter, tensor
  14. from megengine.autodiff import GradManager
  15. from megengine.core.ops import builtin as ops
  16. from megengine.core.ops.builtin import Elemwise
  17. from megengine.core.tensor.utils import isscalar
  18. from megengine.functional import exp, log
  19. from megengine.jit import GraphOptimizationConfig, TraceError, exclude_from_trace, trace
  20. from megengine.module import Module
  21. from megengine.random import normal, uniform
  22. from megengine.utils.naming import AutoNaming
  23. @pytest.mark.parametrize("trace_mode", [False, True])
  24. @pytest.mark.parametrize("return_mode", ["Value", "Tuple", "List", "Dict"])
  25. def test_trace(trace_mode, return_mode):
  26. @trace(symbolic=trace_mode)
  27. def f(x):
  28. if return_mode == "Tuple":
  29. return (-x,)
  30. elif return_mode == "List":
  31. return [-x]
  32. elif return_mode == "Dict":
  33. return {"neg": -x}
  34. else:
  35. return -x
  36. def get_numpy(y):
  37. if return_mode == "Tuple" or return_mode == "List":
  38. return y[0].numpy()
  39. elif return_mode == "Dict":
  40. return y["neg"].numpy()
  41. return y.numpy()
  42. x = tensor([1])
  43. y = get_numpy(f(x))
  44. for i in range(3):
  45. np.testing.assert_equal(get_numpy(f(x)), y)
  46. def test_output_copy_trace():
  47. class Simple(Module):
  48. def __init__(self):
  49. super().__init__()
  50. self.a = Parameter([1.0], dtype=np.float32)
  51. def forward(self, x):
  52. x = x * self.a
  53. # will result into a copy of output in grad
  54. x = F.exp(x)
  55. return x
  56. ys = {False: [], True: []}
  57. for symbolic in [False, True]:
  58. net = Simple()
  59. gm = GradManager().attach(net.parameters())
  60. opt = optim.SGD(net.parameters(), 1e-3, momentum=0.9)
  61. data = tensor(np.arange(4).reshape(2, 2), dtype="float32")
  62. @trace(symbolic=symbolic)
  63. def train_func(d):
  64. with gm:
  65. loss = net(d)
  66. gm.backward(loss)
  67. opt.step().clear_grad()
  68. return loss
  69. for i in range(3):
  70. y = train_func(data).numpy()
  71. ys[symbolic].append(y)
  72. for i in range(3):
  73. np.testing.assert_equal(ys[False][i], ys[True][i])
  74. @pytest.mark.parametrize("trace_mode", [False, True])
  75. def test_tensor_detach(trace_mode):
  76. @trace(symbolic=True)
  77. def f(x):
  78. y = x.detach() ** 2
  79. z = y.detach() + 1
  80. return z.detach()
  81. x = tensor([1, 2, 3, 4])
  82. for _ in range(3):
  83. f(x).numpy()
  84. @pytest.mark.parametrize("trace_mode", [False, True])
  85. def test_exclude_from_trace(trace_mode):
  86. @trace(symbolic=trace_mode)
  87. def f(x):
  88. x = -x
  89. with exclude_from_trace():
  90. if i % 2:
  91. x = -x
  92. x = -x
  93. return x
  94. x = tensor([1])
  95. for i in range(3):
  96. y = f(x).numpy()
  97. np.testing.assert_equal(f(x).numpy(), y)
  98. @pytest.mark.parametrize("trace_mode", [False, True])
  99. def test_elemwise_fuse(trace_mode):
  100. # explicitly declare opt_level as 2
  101. @trace(symbolic=trace_mode, opt_level=2)
  102. def f(a, b):
  103. base = 0
  104. c = b - a
  105. _, idx = F.topk(c, 3)
  106. # internally, biased_idx will be idx as gopt will ignore the addition
  107. biased_idx = base + idx
  108. return biased_idx
  109. a = tensor(np.ones((7, 2)), dtype=np.int32)
  110. b = tensor(2 * np.ones((7, 2)), dtype=np.float32)
  111. for i in range(3):
  112. y = f(a, b)
  113. y.numpy()
  114. @pytest.mark.parametrize("trace_mode", [False, True])
  115. def test_elemwise_fuse_in_grad(trace_mode):
  116. w = Parameter(np.ones([4, 6]), dtype="float32")
  117. gm = GradManager().attach(w)
  118. opt = optim.SGD([w], lr=0.01, momentum=0.9, weight_decay=5e-4)
  119. # explicitly declare opt_level as 2
  120. @trace(symbolic=trace_mode, opt_level=2)
  121. def f():
  122. with gm:
  123. wm = F.sum(w ** 2, axis=1) ** 0.5
  124. loss = wm.mean()
  125. gm.backward(loss)
  126. opt.step().clear_grad()
  127. return loss
  128. for i in range(3):
  129. y = f()
  130. y.numpy()
  131. def test_print_in_trace():
  132. for symbolic in [False]: # cannot read value in symbolic mode
  133. @trace(symbolic=symbolic)
  134. def f(x):
  135. nonlocal buf
  136. x = -x
  137. buf = x.numpy()
  138. x = -x
  139. return x
  140. buf = None
  141. x = tensor([1])
  142. for i in range(3):
  143. y = f(x).numpy()
  144. z = buf
  145. buf = None
  146. np.testing.assert_equal(f(x).numpy(), y)
  147. np.testing.assert_equal(z, buf)
  148. @pytest.mark.parametrize(
  149. "dump_format",
  150. [
  151. "FBS",
  152. ],
  153. )
  154. def test_dump(dump_format):
  155. @trace(symbolic=True, capture_as_const=True)
  156. def f(a, b):
  157. return a + b
  158. # prevent from remaining scope from exception test
  159. AutoNaming.clear()
  160. a = tensor([2])
  161. b = tensor([4])
  162. y = f(a, b).numpy()
  163. for i in range(3):
  164. np.testing.assert_equal(f(a, b).numpy(), y)
  165. file = io.BytesIO()
  166. dump_info = f.dump(file, dump_format=dump_format)
  167. assert dump_info.nr_opr == 3
  168. np.testing.assert_equal(dump_info.inputs, ["arg_0", "arg_1"])
  169. np.testing.assert_equal(dump_info.outputs, ["ADD"])
  170. file.seek(0)
  171. infer_cg = cgtools.GraphInference(file)
  172. result = list((infer_cg.run(a, b)).values())[0]
  173. np.testing.assert_equal(result[0], y)
  174. def test_capture_dump():
  175. a = tensor([2])
  176. @trace(symbolic=True, capture_as_const=True)
  177. def f(x):
  178. return x * a
  179. x = tensor([3])
  180. y = f(x).numpy()
  181. for i in range(3):
  182. np.testing.assert_equal(f(x).numpy(), y)
  183. file = io.BytesIO()
  184. f.dump(file)
  185. file.seek(0)
  186. infer_cg = cgtools.GraphInference(file)
  187. result = list((infer_cg.run(x)).values())[0]
  188. np.testing.assert_equal(result[0], y)
  189. def test_dump_volatile():
  190. p = tensor([2])
  191. @trace(symbolic=True, capture_as_const=True)
  192. def f(x):
  193. return x * p
  194. x = tensor([3])
  195. y = f(x).numpy()
  196. for i in range(3):
  197. np.testing.assert_equal(f(x).numpy(), y)
  198. file = io.BytesIO()
  199. f.dump(file, optimize_for_inference=False)
  200. file.seek(0)
  201. (out,) = G.load_graph(file).output_vars_list
  202. assert (
  203. cgtools.get_owner_opr_type(cgtools.get_owner_opr_inputs(out)[1])
  204. == "ImmutableTensor"
  205. )
  206. def test_dump_backward_graph():
  207. x0 = tensor(np.random.randn(3, 4))
  208. x1 = tensor(np.random.randn(3, 4))
  209. gm = GradManager().attach(x0)
  210. @trace(symbolic=True, capture_as_const=True)
  211. def f(x0, x1):
  212. with gm:
  213. y = x0 * x1
  214. gm.backward(y, F.ones_like(y))
  215. dx0 = x0.grad
  216. return y, dx0
  217. y, dx0 = f(x0, x1)
  218. np.testing.assert_equal(dx0.numpy(), x1)
  219. file = io.BytesIO()
  220. f.dump(file, optimize_for_inference=False)
  221. file.seek(0)
  222. infer_cg = cgtools.GraphInference(file)
  223. results = list((infer_cg.run(x0, x1)).values())
  224. np.testing.assert_equal(results[0], y)
  225. np.testing.assert_equal(results[1], dx0)
  226. def test_dump_with_testcase():
  227. @trace(symbolic=True, capture_as_const=True)
  228. def f(x):
  229. return exp(x)
  230. f(tensor(1.0))
  231. file = io.BytesIO()
  232. f.dump(file, input_data=["#rand(0, 255, 1)"])
  233. @pytest.mark.parametrize("trace_mode", [False, True])
  234. def test_trace_profiler(trace_mode):
  235. @trace(symbolic=trace_mode, profiling=True)
  236. def f(x):
  237. return -x
  238. x = tensor([1])
  239. y = f(x).numpy()
  240. f(x)
  241. f(x) # XXX: has to run twice
  242. out = f.get_profile()
  243. assert out.get("profiler")
  244. def test_goptions():
  245. @trace(symbolic=True, opt_level=0, capture_as_const=True)
  246. def f(x):
  247. # directly return x / x will not trigger gopt
  248. # since there's no way to tell the two x are the same
  249. y = 2.0 * x
  250. return y / y
  251. @trace(symbolic=True, opt_level=1, capture_as_const=True)
  252. def g(x):
  253. y = 2.0 * x
  254. return y / y
  255. d = tensor(0.0)
  256. assert not np.isfinite(f(d).numpy())
  257. np.testing.assert_equal(g(d).numpy().item(), 1.0)
  258. def test_goptions_log_sum_exp():
  259. @trace(symbolic=True, opt_level=0, capture_as_const=True)
  260. def f(x, y):
  261. return log(exp(x) + exp(y))
  262. @trace(symbolic=True, opt_level=1, capture_as_const=True)
  263. def g(x, y):
  264. return log(exp(x) + exp(y))
  265. val = 1.0e4
  266. d = tensor(val)
  267. o = tensor(0.0)
  268. assert not np.isfinite(f(d, o).numpy())
  269. np.testing.assert_almost_equal(g(d, o), val)
  270. def test_goptions_log_exp():
  271. @trace(symbolic=True, opt_level=0, capture_as_const=True)
  272. def f(x):
  273. return log(exp(x))
  274. @trace(symbolic=True, opt_level=1, capture_as_const=True)
  275. def g(x):
  276. return log(exp(x))
  277. f(tensor(1.0))
  278. _, out = mkstemp()
  279. f.dump(out, optimize_for_inference=False)
  280. outputs = G.load_graph(out).output_vars_list
  281. oprs_1 = cgtools.get_oprs_seq(outputs)
  282. g(tensor(1.0))
  283. g.dump(out, optimize_for_inference=False)
  284. outputs = G.load_graph(out).output_vars_list
  285. oprs_2 = cgtools.get_oprs_seq(outputs)
  286. assert len(oprs_1) - len(oprs_2) == 2
  287. def test_optimize_for_inference():
  288. @trace(symbolic=True, capture_as_const=True)
  289. def f(x):
  290. return exp(x)
  291. _, out = mkstemp()
  292. f(tensor(5.0))
  293. f.dump(out, enable_io16xc32=True)
  294. res = G.load_graph(out)
  295. computing_input = res.output_vars_list[0].owner.inputs[0]
  296. assert computing_input.dtype == np.float16
  297. def test_optimize_for_inference_broadcast():
  298. a = tensor(np.ones(1, dtype=np.float32))
  299. @trace(capture_as_const=True, symbolic_shape=True)
  300. def f():
  301. return a._broadcast(tensor([1, 10], dtype=np.int32))
  302. f()
  303. f.dump(io.BytesIO())
  304. def test_trace_cvt_bool():
  305. x = tensor([0], dtype=np.int32)
  306. @trace(symbolic=True)
  307. def f(x):
  308. a = x.shape
  309. b = a[0]
  310. assert isscalar(b)
  311. return b == 0
  312. for i in range(3):
  313. np.testing.assert_equal(f(x).numpy(), False)
  314. @pytest.mark.parametrize("trace_mode", [False, True])
  315. def test_trace_reshape(trace_mode):
  316. x1 = tensor(np.random.randn(2, 10, 10))
  317. x2 = tensor(np.random.randn(4, 10, 10))
  318. x3 = tensor(np.random.randn(8, 10, 10))
  319. @trace(symbolic=trace_mode, capture_as_const=True)
  320. def f(x):
  321. y = x.reshape(x.shape[0], 100)
  322. return y
  323. f(x1)
  324. f(x2)
  325. f(x3)
  326. def test_trace_topk():
  327. x = tensor([5, 2, 7, 1, 0, 3, 2])
  328. @trace(symbolic=True)
  329. def f(x):
  330. y = F.topk(x, 3)
  331. np.testing.assert_equal(y[0].shape.numpy(), np.array([3,]))
  332. return y
  333. for i in range(3):
  334. f(x)
  335. def test_trace_warp_perspective():
  336. inp_shape = (1, 1, 4, 4)
  337. x = tensor(np.arange(16, dtype=np.float32).reshape(inp_shape))
  338. M_shape = (1, 3, 3)
  339. M = tensor(
  340. np.array(
  341. [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]], dtype=np.float32
  342. ).reshape(M_shape)
  343. )
  344. @trace(symbolic=True)
  345. def f(x, M):
  346. out = F.vision.warp_perspective(x, M, (2, 2))
  347. np.testing.assert_equal(out.shape.numpy(), np.array([1, 1, 2, 2]))
  348. return out
  349. for i in range(3):
  350. f(x, M)
  351. @pytest.mark.parametrize(
  352. "normal_expr, mismatch_expr, reason",
  353. [
  354. ("a + b + c", "a + b - c", "operator mismatch"),
  355. ("a + b + 1", "a + b + 2", "tensors not equals"),
  356. ("((a + b), (b + c))[0]", "a + b", "mismature end"),
  357. ("a + b + c", "c + (a + b)", "expect internal node, got external"),
  358. ("c + (a + b)", "a + b + c", "expect external node, got internal"),
  359. ("a + b + c", "a + b + c + c", "too many instructions"),
  360. ("((a + b), (b + c))[1]", "((a + b), (b + c))[0]", "data unreadable"),
  361. ("((a + b), (b + c))[1] + a", "((a + b), (b + c))[0] + a", "input id mismatch"),
  362. ],
  363. )
  364. def test_trace_mismatch(normal_expr, mismatch_expr, reason):
  365. a = tensor([1, 2, 3, 4])
  366. b = tensor([5, 6, 7, 8])
  367. c = tensor([9, 0, 1, 2])
  368. mismatch = False
  369. @trace(symbolic=True)
  370. def fn(a, b, c):
  371. if not mismatch:
  372. result = eval(normal_expr)
  373. else:
  374. result = eval(mismatch_expr)
  375. return result
  376. for i in range(20):
  377. try:
  378. d = fn(a, b, c)
  379. except TraceError as e:
  380. assert mismatch
  381. assert str(e) == "trace error because {}".format(reason)
  382. except:
  383. pytest.fail("unexpected trace error")
  384. else:
  385. assert not mismatch
  386. np.testing.assert_equal(d.numpy(), eval(normal_expr).numpy())
  387. mismatch = random.random() > 0.8
  388. def test_exception_in_trace():
  389. a = tensor([1, 2, 3, 4])
  390. b = tensor([5, 6, 7, 8])
  391. c = tensor([9, 0, 1, 2])
  392. mismatch = False
  393. exc = Exception()
  394. @trace(symbolic=True)
  395. def fn(a, b, c):
  396. result = a + b
  397. if not mismatch:
  398. result += c
  399. else:
  400. raise exc
  401. return result
  402. for i in range(20):
  403. try:
  404. d = fn(a, b, c)
  405. except TraceError as e:
  406. pytest.fail("unexpected trace error")
  407. except Exception as e:
  408. assert mismatch
  409. assert e is exc
  410. else:
  411. assert not mismatch
  412. np.testing.assert_equal(d.numpy(), (a + b + c).numpy())
  413. mismatch = random.random() > 0.8
  414. def test_graph_error():
  415. a = tensor(np.arange(8).reshape((2, 4)))
  416. b = tensor(np.arange(8).reshape((2, 4)))
  417. @trace(symbolic=True)
  418. def fn(a, b):
  419. return a + b
  420. fn(a, b)
  421. with pytest.raises(RuntimeError):
  422. fn(a, b.transpose())
  423. fn(a, b)
  424. @pytest.mark.parametrize("trace_mode", [False, True])
  425. def test_trace_broadcast(trace_mode):
  426. x1 = tensor(np.random.randn(3, 1, 1))
  427. x2 = tensor(np.random.randn(1, 4, 1))
  428. x3 = tensor(np.random.randn(1, 1, 5))
  429. @trace(symbolic=trace_mode, capture_as_const=True)
  430. def f(x):
  431. y = F.broadcast_to(x, (3, 4, 5))
  432. return y
  433. f(x1)
  434. f(x2)
  435. f(x3)
  436. def test_trace_nms():
  437. def make_inputs(n):
  438. boxes = np.zeros((n, 4))
  439. boxes[:, :2] = np.random.rand(n, 2) * 100
  440. boxes[:, 2:] = np.random.rand(n, 2) * 100 + 100
  441. scores = np.random.rand(n)
  442. return tensor(boxes), tensor(scores)
  443. @trace(symbolic=False)
  444. def f(boxes, scores):
  445. # with tracing, max_output must be specified
  446. results = F.vision.nms(boxes, scores=scores, iou_thresh=0.5, max_output=20)
  447. # without tracing, max output can be inferred inside nms
  448. with exclude_from_trace():
  449. _ = F.vision.nms(boxes, scores=scores, iou_thresh=0.5)
  450. return results
  451. f(*make_inputs(10))
  452. f(*make_inputs(20))
  453. f(*make_inputs(30))
  454. def test_trace_valid_broadcast():
  455. x1 = tensor(np.random.randn(1, 1))
  456. x2 = tensor(np.random.randn(1, 2))
  457. shape = (tensor([2]), tensor([2]))
  458. @trace(symbolic=False)
  459. def f(x, shape):
  460. y = F.broadcast_to(x, shape)
  461. return y
  462. f(x1, shape)
  463. f(x2, shape)
  464. @pytest.mark.parametrize("trace_mode", [False, True])
  465. def test_clip(trace_mode):
  466. x = tensor(np.random.randn(10, 10))
  467. @trace(symbolic=trace_mode)
  468. def f(x, lower, upper):
  469. y = F.clip(x, lower, upper)
  470. return y
  471. for i in range(3):
  472. f(x, tensor([0]), tensor([1]))
  473. for i in range(3):
  474. f(x, tensor([5]), tensor([4]))
  475. # test returning noncontiguous tensor from trace
  476. def test_slice():
  477. @trace
  478. def f(x):
  479. return x[:, 1::2]
  480. x = F.arange(8).reshape(2, 4)
  481. f(x)
  482. y = f(x)
  483. np.testing.assert_array_equal(y.numpy(), x.numpy()[:, 1::2])
  484. y + y
  485. @pytest.mark.parametrize("shape_mode", [False, True])
  486. def test_random(shape_mode):
  487. def run_test(op):
  488. @trace(symbolic=True, symbolic_shape=shape_mode)
  489. def f():
  490. out = op(size=[10, 10])
  491. out_shape = out.shape
  492. assert out_shape is not None
  493. if not isinstance(out_shape, tuple):
  494. assert out.shape.numpy() is not None
  495. return out
  496. for _ in range(3):
  497. f()
  498. run_test(uniform)
  499. run_test(normal)
  500. @pytest.mark.parametrize("shape_mode", [False, True])
  501. def test_trace_advance_indexing(shape_mode):
  502. funcs = [
  503. lambda x, i: x[i],
  504. lambda x, i, j: x[i, j],
  505. lambda x, i, j: x[i, :, j, ...],
  506. lambda x, start, end: x[start:end],
  507. lambda x, start, end: x[:, 0, start:end, ..., 1],
  508. lambda x, vec: x[vec],
  509. lambda x, vec: x[vec, ..., 0, 1:3],
  510. lambda x, vec: x[vec, vec[0], vec[1]],
  511. # lambda x, i, start, end, vec: x[i, ..., :, vec, start:end], # FIXME
  512. lambda x, mask: x[mask],
  513. ]
  514. inputs = {
  515. "x": np.random.randn(5, 5, 5, 5, 5).astype("float32"),
  516. "i": 4,
  517. "j": 2,
  518. "start": 1,
  519. "end": 3,
  520. "vec": [1, 2, 3],
  521. "mask": np.random.randn(5, 5, 5, 5, 5) >= 0,
  522. }
  523. for f in funcs:
  524. sig = inspect.signature(f)
  525. param_names = list(sig._parameters.keys())
  526. params = {}
  527. params_np = {}
  528. f_traced = trace(f, symbolic=False, symbolic_shape=shape_mode)
  529. for name in param_names:
  530. params[name] = tensor(inputs[name])
  531. params_np[name] = inputs[name]
  532. expected = f(**params_np)
  533. result_imperative = f(**params)
  534. np.testing.assert_equal(expected, result_imperative.numpy())
  535. for _ in range(3):
  536. result_trace = f_traced(**params)
  537. np.testing.assert_equal(expected, result_trace.numpy())
  538. @pytest.mark.require_ngpu(1) # nvrtc backend
  539. def test_trace_jit_config():
  540. def run(fuse_dimshuffle, fuse_reduce):
  541. config = GraphOptimizationConfig()
  542. config.jit_fuse_dimshuffle = fuse_dimshuffle
  543. config.jit_fuse_reduce = fuse_reduce
  544. # set opt_level = 1 to avoid fusing dimshuffle and reduce at the same time
  545. @trace(opt_level=1, graph_opt_config=config)
  546. def func(x):
  547. return x + 1
  548. x = tensor(2)
  549. y = func(x)
  550. y = func(x)
  551. # func._compile()
  552. options = func._trace.options
  553. mapping = {None: 0, False: 1, True: 2}
  554. assert options.graph_opt.jit == 0
  555. assert options.graph_opt.jit_config.fuse_dimshuffle == mapping[fuse_dimshuffle]
  556. assert options.graph_opt.jit_config.fuse_reduce == mapping[fuse_reduce]
  557. for fuse_dimshuffle in [None, False, True]:
  558. for fuse_reduce in [None, False, True]:
  559. run(fuse_dimshuffle, fuse_reduce)